File Coverage

blib/lib/Resource/Silo/Metadata/DAG.pm
Criterion Covered Total %
statement 83 83 100.0
branch 20 28 71.4
condition 4 9 44.4
subroutine 13 13 100.0
pod 9 9 100.0
total 129 142 90.8


line stmt bran cond sub pod time code
1             package Resource::Silo::Metadata::DAG;
2              
3 50     50   93189 use strict;
  50         86  
  50         1648  
4 50     50   264 use warnings;
  50         82  
  50         2959  
5             our $VERSION = '0.17';
6              
7             =head1 NAME
8              
9             Resource::Silo::Metadata::DAG - Generic directed (acyclic) graph for dependency tracking
10              
11             =head1 DESCRIPTION
12              
13             This class is an internal part of L and is subject to change.
14             Its main purpose is to track incomplete resources and detect dependency loops.
15              
16             =cut
17              
18 50     50   21413 use Moo;
  50         336965  
  50         348  
19 50     50   72613 use Carp;
  50         113  
  50         46674  
20              
21             =head1 ATTRIBUTES
22              
23             =over
24              
25             =item * edges_out
26              
27             =item * edges_in
28              
29             =back
30              
31             =cut
32              
33             # use directed graph: "consumer -> producer"
34             # edges_out { consumer } { producer } = 1;
35             # edges_in { producer } { consumer } = 1;
36              
37             has edges_out => is => 'ro', default => sub { {} };
38             has edges_in => is => 'ro', default => sub { {} };
39              
40             =head1 METHODS
41              
42             =cut
43              
44             =head2 size
45              
46             Number of vertices in the graph.
47              
48             =cut
49              
50             sub size {
51             # should be true when resource declaration is complete
52 1     1 1 754 my $self = shift;
53 1         3 return scalar $self->list;
54             }
55              
56             =head2 list
57              
58             Lists vertices.
59              
60             =cut
61              
62             sub list {
63 3     3 1 5 my $self = shift;
64 3         4 my %uniq;
65 3         9 @uniq{ keys %{$self->edges_out}, keys %{$self->edges_in} } = ();
  3         9  
  3         10  
66 3         23 return keys %uniq;
67             }
68              
69             =head2 list_sinks
70              
71             List only vertices with no outgoing edges.
72              
73             =cut
74              
75             sub list_sinks {
76 75     75 1 130 my $self = shift;
77              
78 75         122 return grep { !$self->edges_out->{$_} } keys %{$self->edges_in};
  9         37  
  75         417  
79             }
80              
81             =head2 list_predecessors(\@list)
82              
83             Given a list of vertices, return the list of all their predecessors
84             without the vertices themselves.
85              
86             =cut
87              
88             sub list_predecessors {
89 6     6 1 15 my ($self, $list) = @_;
90 6         9 my %uniq;
91 6         14 foreach my $node (@$list) {
92 8 50       27 next unless $self->edges_in->{$node};
93 8         11 @uniq{ keys %{$self->edges_in->{$node}} } = ();
  8         52  
94             };
95 6         16 delete $uniq{$_} for @$list; # remove self-references
96 6         31 return keys %uniq;
97             }
98              
99             =head2 contains($name)
100              
101             Returns true if a vertex named C<$name> is present.
102              
103             =cut
104              
105             sub contains {
106 13     13 1 29 my ($self, $name) = @_;
107             return exists $self->edges_out->{$name}
108 13   66     108 || exists $self->edges_in->{$name};
109             }
110              
111             =head2 add_edges (\@from, \@to)
112              
113             Add edges from first vertex to the following ones.
114              
115             =cut
116              
117             sub add_edges {
118 9     9 1 25 my ($self, $from, $to) = @_;
119              
120 9         13 foreach my $consumer (@$from) {
121 9         14 foreach my $producer (@$to) {
122 11 50       26 next if $consumer eq $producer; # self-dependency is ignored
123 11         29 $self->edges_out->{$consumer}->{$producer} = 1;
124 11         27 $self->edges_in->{$producer}->{$consumer} = 1;
125             }
126             }
127 9         14 return;
128             }
129              
130             =head2 drop_sink_cascade($name)
131              
132             If $name is a sink, remove it along with any vertex which becomes
133             a sink as a result of the operation, propagating along the edges.
134              
135             Otherwise do nothing.
136              
137             =cut
138              
139             sub drop_sink_cascade {
140 115     115 1 300 my ($self, $arriving) = @_;
141              
142 115         233 my @queue = ($arriving);
143 115         299 while (@queue) {
144 117         247 my $producer = shift @queue;
145 117 50       637 next if $self->edges_out->{$producer}; # producer is not independent => skip
146 117         457 my $node = delete $self->edges_in->{$producer};
147 117 100       533 next unless $node; # no one is waiting => skip
148              
149 3         8 foreach my $consumer (keys %$node) {
150 3         7 my $still_waiting = $self->edges_out->{$consumer};
151 3         5 delete $still_waiting->{$producer};
152 3 100       15 if (keys %$still_waiting == 0) {
153 2         5 delete $self->edges_out->{$consumer};
154 2         8 push @queue, $consumer;
155             }
156             }
157             }
158             }
159              
160             =head2 find_loop ($start, \@list, \%seen)
161              
162             Find out whether calling C<< $self->add_dependency([$start], $list) >>
163             would cause a loop in the graph.
164              
165             Due to the usage scenario, it's disjoint from adding vertices/edges.
166              
167             =cut
168              
169             sub find_loop {
170             # before inserting a new edge, check if it would create a loop
171 17     17 1 4228 my ($self, $start, $list, $seen) = @_;
172              
173 17         29 foreach my $next (@$list) {
174 20 100       42 return [$start] if $next eq $start; # loop found
175 18 100       43 next if $seen->{$next}++;
176 17 100       64 my $out = $self->edges_out->{$next} or next;
177 7         23 my $loop = $self->find_loop($start, [ keys %$out ], $seen);
178 7 100       32 return [$next, @$loop] if $loop;
179             }
180              
181 10         25 return;
182             }
183              
184             =head2 self_check
185              
186             Check the internal structure of the graph, returning C if its intact,
187             or an arrayref containing the list of discrepancies otherwise.
188              
189             =cut
190              
191             sub self_check {
192 2     2 1 7 my $self = shift;
193              
194 2         3 my @mismatch; # "consumer -> producer" or "producer <- consumer"
195              
196 2         3 foreach my $consumer (keys %{$self->edges_out}) {
  2         9  
197 5         6 foreach my $producer (keys %{$self->edges_out->{$consumer}}) {
  5         10  
198             push @mismatch, "$consumer <- $producer"
199             unless $self->edges_in->{$producer}
200 8 50 33     32 && $self->edges_in->{$producer}->{$consumer};
201             }
202             }
203              
204 2         3 foreach my $producer (keys %{$self->edges_in}) {
  2         5  
205 6         7 foreach my $consumer (keys %{$self->edges_in->{$producer}}) {
  6         10  
206             push @mismatch, "$consumer -> $producer"
207             unless $self->edges_out->{$consumer}
208 8 50 33     24 && $self->edges_out->{$consumer}->{$producer};
209             }
210             }
211              
212             # hunt down empty nodes as "produces <- ?" or "consumer -> ?"
213 2         3 foreach my $name (keys %{$self->edges_out}) {
  2         4  
214             push @mismatch, "$name -> ?"
215 5 50       6 if keys %{$self->edges_out->{$name}} == 0;
  5         26  
216             }
217 2         4 foreach my $name (keys %{$self->edges_in}) {
  2         7  
218             push @mismatch, "$name <- ?"
219 6 50       6 if keys %{$self->edges_in->{$name}} == 0;
  6         13  
220             }
221              
222 2 50       9 return @mismatch ? \@mismatch : undef;
223             }
224              
225             =head1 SEE ALSO
226              
227             L.
228              
229             =cut
230              
231             1;