File Coverage

blib/lib/Resource/Silo/Metadata/DAG.pm
Criterion Covered Total %
statement 83 83 100.0
branch 19 28 67.8
condition 4 9 44.4
subroutine 13 13 100.0
pod 9 9 100.0
total 128 142 90.1


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