File Coverage

blib/lib/Algorithm/DependencySolver/Solver.pm
Criterion Covered Total %
statement 40 40 100.0
branch n/a
condition n/a
subroutine 22 22 100.0
pod n/a
total 62 62 100.0


line stmt bran cond sub pod time code
1             package Algorithm::DependencySolver::Solver;
2             $Algorithm::DependencySolver::Solver::VERSION = '1.01';
3 3     3   23878 use Moose;
  3         382060  
  3         23  
4 3     3   23565 use MooseX::FollowPBP;
  3         38366  
  3         17  
5 3     3   26021 use MooseX::Method::Signatures;
  3         3269100  
  3         16  
6              
7 3     3   3401 use List::Compare;
  3         55069  
  3         122  
8 3     3   28 use List::MoreUtils qw(any);
  3         4  
  3         27  
9              
10 3     3   2481 use Graph::Directed;
  3         261005  
  3         77  
11 3     3   2181 use Graph::Easy;
  3         293709  
  3         137  
12 3     3   1756 use Graph::Convert;
  3         3094  
  3         417  
13              
14              
15             =head1 NAME
16              
17             Algorithm::DependencySolver - A dependency solver for scheduling access to a shared resource
18              
19             =head1 VERSION
20              
21             version 1.01
22              
23             =head1 SYNOPSIS
24              
25             use Algorithm::DependencySolver::Solver;
26             use Algorithm::DependencySolver::Traversal;
27             use Algorithm::DependencySolver::Operation;
28              
29             my @operations = (
30             Algorithm::DependencySolver::Operation->new(
31             id => 1,
32             depends => [qw(z)],
33             affects => [qw(x)],
34             prerequisites => ["3"],
35             ),
36             Algorithm::DependencySolver::Operation->new(
37             id => 2,
38             depends => [qw(x)],
39             affects => [qw(y)],
40             prerequisites => [],
41             ),
42             Algorithm::DependencySolver::Operation->new(
43             id => 3,
44             depends => [qw(y)],
45             affects => [qw(z)],
46             prerequisites => [],
47             ),
48             );
49              
50             my $solver =
51             Algorithm::DependencySolver::Solver->new(nodes => \@operations);
52              
53             $solver->to_png("pretty-graph.png");
54              
55              
56              
57             my $traversal = Algorithm::DependencySolver::Traversal->new(
58             Solver => $solver,
59             visit => sub {
60             my $operation = shift;
61             print "Visited operation: ", $operation->id, "\n";
62             },
63             );
64              
65             $traversal->run;
66              
67             =head1 DESCRIPTION
68              
69             This dependency solver is somewhat different to the existing
70             L<Algorithm::Dependency> module.
71              
72             L<Algorithm::Dependency> creates a heirarchy where each node depends
73             on a set of other nodes. In L<Algorithm::DependencySolver>, there
74             exists a set of operations and a set of resources, with a set of edges
75             from operations to resources (the dependencies), and a set of edges
76             from resources to operations (the affects). Given this input, the
77             module outputs a directed acyclic graph (DAG) containing just the
78             operations as its nodes.
79              
80             Aditionally, L<Algorithm::DependencySolver> allows for input which
81             whould have resulted in a cyclic output graph to be resolved by means
82             of explicit sequencing. This is done by marking nodes as depending on
83             other nodes. See
84             L<Algorithm::DependencySolver::Operation::prerequisites>.
85              
86              
87             =head1 METHODS
88              
89             =cut
90              
91              
92              
93             has 'nodes' => (
94             is => 'ro',
95             # isa => 'ArrayRef[Operation]',
96             required => 1,
97             );
98              
99             has 'nodes_index' => (
100             is => 'ro',
101             # isa => 'HashRef[Operation]',
102             builder => 'build_nodes_index',
103             lazy => 1,
104             init_arg => undef,
105             );
106              
107             has 'relations' => (
108             is => 'ro',
109             builder => 'build_relations',
110             lazy => 1,
111             init_arg => undef,
112             );
113              
114             has 'affects_index' => (
115             is => 'ro',
116             builder => 'build_affects_index',
117             lazy => 1,
118             init_arg => undef,
119             );
120              
121             =head2 get_Graph
122              
123             Returns the dependency graph as a L<Graph> object. Note that only
124             operations are included in the graph, not resources. This is of most
125             use to the L<Algorithm::DependencySolver::Traversal> module, and the
126             C<to_dot> and C<to_png> methods.
127              
128             =cut
129              
130             has 'Graph' => (
131             is => 'ro',
132             builder => 'build_Graph',
133             lazy => 1,
134             init_arg => undef,
135             );
136              
137             has 'GraphEasy' => (
138             is => 'ro',
139             builder => 'build_GraphEasy',
140             lazy => 1,
141             init_arg => undef,
142             );
143              
144              
145 3     3   581670 method build_nodes_index() {
146             return { map { $_->id => $_ } @{$self->get_nodes} };
147             }
148              
149 3     3   57619 method build_relations() {
150              
151             my @relations;
152              
153             for my $node (@{$self->get_nodes()}) {
154             for my $resource (@{$node->depends}) {
155             for my $other (@{$self->get_affects_index->{$resource}}) {
156             next if $node->id eq $other->id;
157             push @relations, [$other, $node];
158             }
159             }
160             }
161              
162             return \@relations;
163             }
164              
165              
166 3     3   58514 method build_Graph() {
167              
168             my @vertices = keys %{$self->get_nodes_index};
169             my @edges = map {
170             [ $_->[0]->id, $_->[1]->id ]
171             } @{$self->get_relations};
172              
173             # Ensure that each explicit ordering (node.prerequisites) has an edge.
174             for my $nodeB (@{$self->get_nodes}) {
175             for my $nodeA_id (@{$nodeB->prerequisites}) {
176             push @edges, [$nodeA_id, $nodeB->id];
177             }
178             }
179              
180              
181             my $G = Graph::Directed->new(
182             vertices => \@vertices,
183             edges => \@edges,
184             # refvertexed => 1, # refvertexed is broken!
185             );
186              
187             # Note: Graph::Traversal has a bug in it where noderefs are
188             # sometimes stringified, even though they mustn't be with
189             # refvertexed! Therefore, assume all of Graph is broken in this
190             # respect, and never pass in addresses to references, but never
191             # references themselves.
192              
193             $self->_apply_orderings($G);
194             $self->_remove_redundancy($G);
195              
196             return $G;
197             }
198              
199 3     3   57757 method _get_nondeterministic_attributes() {
200             my %nondep_affects;
201              
202             AFFECT:
203             for my $affect (keys %{$self->get_affects_index}) {
204             my @node_ids = map {
205             $_->id
206             } @{$self->get_affects_index->{$affect}};
207              
208             next AFFECT unless @node_ids;
209              
210             my @sequentials;
211              
212             for my $node_id (@node_ids) {
213             my @pred_ids = $self->get_Graph->all_predecessors($node_id);
214             push @pred_ids, $node_id;
215             my $C = List::Compare->new(\@node_ids, \@pred_ids);
216             if ($C->is_LsubsetR) {
217             # We're good; we have a nice linear ordering
218             next AFFECT;
219             } else {
220             my @intersection = $C->get_intersection;
221             if (@intersection > @sequentials) {
222             @sequentials = @intersection;
223             }
224             }
225             }
226              
227             # Nondeterministic affect!
228             my @nondeps = List::Compare->new(\@node_ids, \@sequentials)->get_unique;
229             $nondep_affects{$affect} = {
230             sequentials => \@sequentials,
231             nondeps => \@nondeps,
232             };
233             }
234             return keys(%nondep_affects) ? \%nondep_affects : undef;
235             }
236              
237 3     3   59165 method _get_undepended_affects() {
238             my %undeped_affects;
239              
240             AFFECT:
241             for my $affect (keys %{$self->get_affects_index}) {
242             my @nodes = @{$self->get_affects_index->{$affect}};
243              
244             next AFFECT unless @nodes;
245              
246             for my $node (@nodes) {
247             my $f;
248             $f = sub {
249             my $suc_id = shift;
250             my $suc = $self->get_nodes_index->{$suc_id};
251             if ($suc->depends($affect)) {
252             # This path is good
253             return [];
254             } elsif ($suc->affects($affect)) {
255             # woah
256             return [$suc];
257             } else {
258             return [map { @{$f->($_)} } $self->get_Graph->successors($suc_id)];
259             }
260             };
261             my @bad = map { @{$f->($_)} } $self->get_Graph->successors($node->id);
262             $undeped_affects{$affect}{$node->id} = \@bad if @bad;
263             }
264             }
265             return keys(%undeped_affects) ? \%undeped_affects : undef;
266             }
267              
268 3     3   60327 method is_invalid() {
269             my $cyclic = $self->get_Graph->is_cyclic;
270             my $nondeterministic = $self->_get_nondeterministic_attributes;
271             my $undeped_affects = $self->_get_undepended_affects;
272              
273             my %r;
274             $r{cyclic} = $cyclic if $cyclic;
275             $r{nondeterministic} = $nondeterministic if $nondeterministic;
276             $r{undeped_affects} = $undeped_affects if $undeped_affects;
277              
278             if (keys %r) {
279             return \%r;
280             } else {
281             return;
282             }
283             }
284              
285             # Safe to call on cyclic graphs. Will not fail early if cycle
286             # encountered
287 3     3   154432 method _apply_orderings($G) {
288              
289             for my $nodeB (@{$self->get_nodes}) {
290             for my $nodeA_id (@{$nodeB->prerequisites}) {
291             my %seen;
292             my $recurse;
293             $recurse = sub {
294             my $node_id = shift;
295             return if $seen{$node_id}++;
296             for my $to_id ($G->successors($node_id)) {
297             if ($to_id eq $nodeA_id) {
298             $G->delete_edge($node_id, $to_id);
299             }
300             else {
301             $recurse->($to_id);
302             }
303             }
304             };
305             $recurse->($nodeB->id);
306             }
307             }
308             }
309              
310             =head2 _remove_redundancy
311              
312             $self->_remove_redundancy($G); # Ignore the return value
313              
314             Applied to a graph object, removes redundant edges. An edge is
315             redundant if it can be removed without invalidating the graph.
316              
317             The fundamental law of the dependency graph is that a node can only be
318             traversed when all of its predecessors have been traversed.
319              
320             Given some node, C<$n>, and a predecessor of C<$n>, C<$a>, then it is
321             safe to remove C<$a> if and only if another node exists, C<$b>, which
322             is a predecessor of C<$n>, and there is a path from C<$a> to C<$b>
323             (i.e., traversal of C<$b> requires that C<$a> has been visited).
324              
325             Note that cycles may cause this algorithm to behave unexpectedly
326             (depending on what one expects). Consider what happens if C<$n> has
327             two successors, C<$a> and C<$b>, such that there is a cycle between
328             C<$a> and C<$b> (i.e., there is an edge from C<$a> to C<$b>, and
329             vice-versa). Suppose that the edge from C<$n> to C<$a> has been
330             removed. Can the edge from C<$n> to C<$b> safely be removed?
331              
332             Using the algorithm described above, yes! This is because there is
333             another path from C<$n> to C<$b>: C<$n -&gt; $b -&gt; $a -&gt; b>. We
334             can, of course, detect such occurrences; however, I choose not to,
335             because it's not clear to me what the most elegant result should be in
336             these situations. Semantically, it does not matter whether the edge
337             from C<$n> to the C<$a,$b>-cycle is from C<$n> to C<$a>, or C<$n> to
338             C<$b>. Which should it be? Both, or one-or-the-other (presumably
339             decided arbitrarily)?
340              
341             Properties:
342              
343             * This method can be safely called on cyclic graphs (i.e., it will not
344             enter a non-terminating loop)
345              
346             * This method will not fail early if a cycle is encountered (i.e., it
347             will do as much work as it can, even though the graph is probably
348             invalid)
349              
350             * If C<_apply_orderings> is to be called on the graph object, it
351             I<must> be done I<before> calling C<_remove_redundancy>
352              
353             =cut
354              
355 3     3   110756 method _remove_redundancy($G) {
356              
357             for my $node ($G->vertices) {
358             for my $pred ($G->predecessors($node)) {
359             next unless $G->has_edge($pred, $node);
360              
361             my @other_predecessors =
362             grep { $_ ne $pred } $G->predecessors($node);
363              
364             my $other_paths_to_pred = grep {
365             # Returns true only if the edge from $pred to $node can
366             # safely be removed
367             any { $_ eq $pred } $G->all_predecessors($_);
368             } @other_predecessors;
369              
370             if ($other_paths_to_pred) {
371             $G->delete_edge($pred, $node);
372             }
373             }
374             }
375             }
376              
377              
378              
379 3     3   56377 method build_affects_index() {
380             my %index;
381             for my $node (@{$self->get_nodes}) {
382             for my $resource (@{$node->affects}) {
383             push @{$index{$resource}}, $node;
384             }
385             }
386             return \%index;
387             }
388              
389 3     3   56928 method to_s() {
390             return $self->get_GraphEasy->as_ascii();
391             }
392              
393             =head2 to_png
394              
395             $solver->to_png($file)
396              
397             Outputs a dependency graph (showing only operations) to the given file
398             in PNG format
399              
400             =cut
401              
402 3     3   112792 method to_png($file) {
403             die "Only sane file names, please (you gave: $file)" unless
404             $file =~ m/^[a-z0-9_\-\.\/]+$/i;
405             open my $dot, "|dot -Tpng -o'$file'" or die ("Cannot open pipe to dot (-o $file): $!");
406             print $dot $self->get_GraphEasy->as_graphviz;
407             }
408              
409              
410             =head2 to_dot
411              
412             $solver->to_dot($file)
413              
414             Outputs a dependency graph (showing only operations) to the given file
415             in Graphviz's dot format
416              
417             =cut
418              
419 3     3   111071 method to_dot($file) {
420             die "Only sane file names, please (you gave: $file)" unless
421             $file =~ m/^[a-z0-9_\-\.\/]+$/i;
422             open my $fh, ">", $file or die ("Cannot open to $file: $!");
423             print $fh $self->get_GraphEasy->as_graphviz;
424             }
425              
426              
427              
428 3     3   58575 method build_GraphEasy() {
429             return Graph::Convert->as_graph_easy($self->get_Graph);
430             }
431              
432              
433 3     3   378 no Moose;
  3         5  
  3         20  
434             __PACKAGE__->meta->make_immutable;