File Coverage

blib/lib/Stepford/Runner.pm
Criterion Covered Total %
statement 161 162 99.3
branch 33 36 91.6
condition 2 3 66.6
subroutine 29 29 100.0
pod 1 2 50.0
total 226 232 97.4


line stmt bran cond sub pod time code
1             package Stepford::Runner;
2              
3 39     39   4190751 use strict;
  39         400  
  39         1172  
4 39     39   197 use warnings;
  39         114  
  39         4319  
5 39     39   16629 use namespace::autoclean;
  39         576798  
  39         199  
6              
7             our $VERSION = '0.006001';
8              
9 39     39   27265 use List::AllUtils qw( first );
  39         414978  
  39         3678  
10 39     39   19262 use Module::Pluggable::Object;
  39         294150  
  39         1447  
11 39     39   19061 use MooseX::Params::Validate qw( validated_list );
  39         21350636  
  39         315  
12 39     39   34895 use Parallel::ForkManager;
  39         2604630  
  39         1620  
13 39     39   19590 use Stepford::Error;
  39         179  
  39         1551  
14 39     39   16571 use Stepford::GraphBuilder;
  39         517  
  39         2824  
15 39         390 use Stepford::Types qw(
16             ArrayOfClassPrefixes ArrayOfSteps Bool ClassName
17             HashRef Logger Maybe PositiveInt Step
18 39     39   387 );
  39         106  
19 39     39   431936 use Try::Tiny;
  39         135  
  39         2837  
20              
21 39     39   258 use Moose;
  39         97  
  39         353  
22 39     39   277390 use MooseX::StrictConstructor;
  39         120  
  39         401  
23              
24             has _step_namespaces => (
25             traits => ['Array'],
26             is => 'ro',
27             isa => ArrayOfClassPrefixes,
28             coerce => 1,
29             required => 1,
30             init_arg => 'step_namespaces',
31             handles => {
32             step_namespaces => 'elements',
33             },
34             );
35              
36             has logger => (
37             is => 'ro',
38             isa => Logger,
39             lazy => 1,
40             builder => '_build_logger',
41             );
42              
43             has jobs => (
44             is => 'ro',
45             isa => PositiveInt,
46             default => 1,
47             );
48              
49             has _memory_stats => (
50             is => 'ro',
51             isa => Maybe ['Memory::Stats'],
52             default => sub {
53             return try {
54             require Memory::Stats;
55             my $s = Memory::Stats->new;
56             $s->start;
57             $s;
58             }
59             catch {
60             undef;
61             };
62             },
63             );
64              
65             has _step_classes => (
66             is => 'ro',
67             isa => ArrayOfSteps,
68             init_arg => undef,
69             lazy => 1,
70             builder => '_build_step_classes',
71             );
72              
73             # We want to preload all the step classes so that the final_steps passed to
74             # run are recognized as valid classes.
75             sub BUILD {
76 145     145 0 649 my $self = shift;
77              
78 145         5522 $self->_step_classes;
79              
80 144         3684 return;
81             }
82              
83             sub run {
84 146     146 1 1136829 my $self = shift;
85 146         1444 my ( $final_steps, $config, $force_step_execution, $dry_run )
86             = validated_list(
87             \@_,
88             final_steps => {
89             isa => ArrayOfSteps,
90             coerce => 1,
91             },
92             config => {
93             isa => HashRef,
94             default => {},
95             },
96             force_step_execution => {
97             isa => Bool,
98             default => 0,
99             },
100             dry_run => {
101             isa => Bool,
102             default => 'none',
103             },
104             );
105              
106 146         214046 my $root_graph
107             = $self->_make_root_graph_builder( $final_steps, $config )->graph;
108              
109 143 100       5373 if ( $dry_run ne 'none' ) {
    100          
110             ## no critic (InputOutput::RequireCheckedSyscalls)
111 1         34 print $root_graph->as_string($dry_run);
112             }
113             elsif ( $self->jobs > 1 ) {
114 77         416 $self->_run_parallel( $root_graph, $force_step_execution );
115             }
116             else {
117 65         264 $self->_run_sequential( $root_graph, $force_step_execution );
118             }
119              
120 108         7613 return;
121             }
122              
123             sub _run_sequential {
124 65     65   160 my $self = shift;
125 65         95 my $root_graph = shift;
126 65         159 my $force_step_execution = shift;
127              
128             $root_graph->traverse( sub {
129 314     314   601 my $graph = shift;
130              
131             return
132 314 100       912 unless $self->_should_run_step(
133             $graph,
134             $force_step_execution,
135             );
136              
137 181         755 $self->_run_step_in_process($graph);
138 181         571 return;
139 65         782 } );
140             }
141              
142             sub _run_parallel {
143 77     77   325 my $self = shift;
144 77         187 my $root_graph = shift;
145 77         398 my $force_step_execution = shift;
146              
147 77         2253 my $pm = Parallel::ForkManager->new( $self->jobs );
148              
149 77         142076 my %graphs;
150 77         239 my $steps_finished_since_last_iteration = 0;
151              
152             $pm->run_on_finish( sub {
153 167     167   279453417 my ( $pid, $exit_code, $step_name, $signal, $message )
154             = @_[ 0 .. 3, 5 ];
155              
156 167 100       2813 if ($exit_code) {
    100          
    100          
157 1         42 _kill_all_children($pm);
158 1         686 die "Child process $pid failed while running step $step_name"
159             . " (exited with code $exit_code)";
160             }
161             elsif ( !$message ) {
162 3         102 _kill_all_children($pm);
163 3         255 my $error = "Child process $pid did not send back any data";
164 3         45 $error .= " while running step $step_name";
165 3 50       81 $error .= " (exited because of signal $signal)" if $signal;
166 3         333 die $error;
167             }
168             elsif ( $message->{error} ) {
169 4         148 _kill_all_children($pm);
170 4         496 die "Child process $pid died"
171             . " while running step $step_name"
172             . " with error:\n$message->{error}";
173             }
174             else {
175 159         728 my $graph = $graphs{$pid};
176 159 50       2878 die "Could not find step graph for $pid"
177             unless defined $graph;
178              
179 159         13580 $graph->set_last_run_time( $message->{last_run_time} );
180             $graph->set_step_productions_as_hashref(
181 159         10757 $message->{productions} );
182 159         6876 $graph->set_is_being_processed(0);
183 159         7185 $graph->set_has_been_processed(1);
184 159         1215 $steps_finished_since_last_iteration++;
185             }
186 77         1582 } );
187              
188 77         4025 while ( !$root_graph->has_been_processed ) {
189             $root_graph->traverse( sub {
190 901     901   1724 my $graph = shift;
191              
192 901         27548 my $class = $graph->step_class;
193              
194             return
195 901 100       5706 unless $self->_should_run_step(
196             $graph,
197             $force_step_execution,
198             );
199              
200 201 100       1848 unless ( $graph->is_serializable ) {
201 4         1312 $self->_run_step_in_process($graph);
202 4         12 $steps_finished_since_last_iteration++;
203 4         12 return;
204             }
205              
206 197 100       20709 if ( my $pid = $pm->start($class) ) {
207 170         460054 $graph->set_is_being_processed(1);
208              
209 170         6758 $graphs{$pid} = $graph;
210              
211 170         11411 $self->logger->debug("Forked child to run $class - pid $pid");
212 170         28485 return;
213             }
214              
215             # child
216 27         135000 my $error;
217             try {
218 27         8248 $self->_run_step_in_process($graph);
219             }
220             catch {
221 1         50 $error = $_;
222 27         4754 };
223              
224 26 100       3296 my %message
225             = defined $error
226             ? ( error => $error . q{} )
227             : (
228             last_run_time => scalar $graph->last_run_time,
229             productions => $graph->step_productions_as_hashref,
230             );
231              
232 26         1914 $pm->finish( 0, \%message );
233 195         4478 } );
234              
235 168         14768 $pm->reap_finished_children;
236              
237             # If none of the child processes have finished, there is no point in
238             # wasting a bunch of CPU checking for steps that can be worked on. We
239             # wait until at least one step has finished.
240 168 100       15671 $pm->wait_one_child unless $steps_finished_since_last_iteration;
241              
242 160         7014 $steps_finished_since_last_iteration = 0;
243             }
244              
245 42         1870 $self->logger->debug('Waiting for children');
246 42         2894 $pm->wait_all_children;
247             }
248              
249             sub _kill_all_children {
250 8     8   71 my $pm = shift;
251              
252 8         109 for my $pid ( $pm->running_procs ) {
253              
254             # This is a best-effort attempt to kill direct children.
255             ## no critic (RequireCheckedSyscalls)
256 0         0 kill 'TERM', $pid;
257             }
258             }
259              
260             sub _should_run_step {
261 1215     1215   2256 my $self = shift;
262 1215         1927 my $graph = shift;
263 1215         1841 my $force_step_execution = shift;
264              
265 1215 100       5325 return 0 unless $graph->can_run_step;
266              
267 619 100       4023 if ( $graph->step_is_up_to_date($force_step_execution) ) {
268 237         6400 $self->logger->info( 'Skipping ' . $graph->step_class );
269 237         13843 $graph->set_has_been_processed(1);
270 237         802 return 0;
271             }
272              
273 382         1829 return 1;
274             }
275              
276             sub _run_step_in_process {
277 212     212   669 my $self = shift;
278 212         1142 my $graph = shift;
279              
280 212         9527 $self->_log_memory_usage( 'Before running ' . $graph->step_class );
281 212         587603 $graph->run_step;
282 210         5832 $self->_log_memory_usage( 'After running ' . $graph->step_class );
283              
284 210         574146 return;
285             }
286              
287             sub _make_root_graph_builder {
288 151     151   3060 my $self = shift;
289 151         376 my $final_steps = shift;
290 151         314 my $config = shift;
291              
292 151         4995 return Stepford::GraphBuilder->new(
293             config => $config,
294             step_classes => $self->_step_classes,
295             final_steps => $final_steps,
296             logger => $self->logger,
297             );
298             }
299              
300             sub _build_step_classes {
301 145     145   388 my $self = shift;
302              
303             # Module::Pluggable does not document whether it returns class names in
304             # any specific order.
305 145         898 my $sorter = $self->_step_class_sorter;
306              
307 145         409 my @classes;
308              
309 145         6648 for my $class (
310 653         1248930 sort { $sorter->() } Module::Pluggable::Object->new(
311             search_path => [ $self->step_namespaces ],
312             require => 1,
313             )->plugins
314             ) {
315              
316             # We need to skip roles
317 602 100       7981 next unless $class->isa('Moose::Object');
318              
319 601 100       3843 unless ( $class->does('Stepford::Role::Step') ) {
320 1         287 Stepford::Error->throw( message =>
321             qq{Found a class which doesn't do the Stepford::Role::Step role: $class}
322             );
323             }
324              
325 600         118873 $self->logger->debug("Found step class $class");
326 600         22176 push @classes, $class;
327             }
328              
329 144         6771 return \@classes;
330             }
331              
332             sub _step_class_sorter {
333 145     145   317 my $self = shift;
334              
335 145         316 my $x = 0;
336 145         6619 my @namespaces = $self->step_namespaces;
337 145         450 my %order = map { $_ => $x++ } @namespaces;
  146         840  
338              
339             return sub {
340 653     653   3416 my $a_prefix = first { $a =~ /^\Q$_/ } @namespaces;
  664         4919  
341 653         3073 my $b_prefix = first { $b =~ /^\Q$_/ } @namespaces;
  662         3149  
342              
343 653   66     4238 return ( $order{$a_prefix} <=> $order{$b_prefix} or $a cmp $b );
344 145         1364 };
345             }
346              
347             sub _build_logger {
348 130     130   310 my $self = shift;
349              
350 130         20352 require Log::Dispatch;
351 130         6528122 require Log::Dispatch::Null;
352 130         1159944 return Log::Dispatch->new(
353             outputs => [ [ Null => min_level => 'emerg' ] ] );
354             }
355              
356             sub _log_memory_usage {
357 422     422   3786 my $self = shift;
358 422         796 my $checkpoint = shift;
359              
360 422 50       12271 return unless $self->_memory_stats;
361              
362 422         10972 $self->_memory_stats->checkpoint($checkpoint);
363              
364             # There's no way to get the total use so far without calling stop(), which
365             # is quite annoying. See
366             # https://github.com/celogeek/perl-memory-stats/issues/3.
367              
368             ## no critic (Subroutines::ProtectPrivateSubs)
369 422         1245863 $self->logger->info( sprintf(
370             'Total memory use since Stepford started is %d',
371             $self->_memory_stats->_memory_usage->[-1][1]
372             - $self->_memory_stats->_memory_usage->[0][1],
373             ) );
374 422         27906 $self->logger->info( sprintf(
375             'Memory since last checked is %d',
376             $self->_memory_stats->delta_usage,
377             ) );
378             }
379              
380             __PACKAGE__->meta->make_immutable;
381              
382             1;
383              
384             # ABSTRACT: Takes a set of steps and figures out what order to run them in
385              
386             __END__
387              
388             =pod
389              
390             =encoding UTF-8
391              
392             =head1 NAME
393              
394             Stepford::Runner - Takes a set of steps and figures out what order to run them in
395              
396             =head1 VERSION
397              
398             version 0.006001
399              
400             =head1 SYNOPSIS
401              
402             use Stepford::Runner;
403              
404             my $runner = Stepford::Runner->new(
405             step_namespaces => 'My::Step',
406             );
407              
408             $runner->run(
409             final_steps => [
410             'My::Step::DeployCatDatabase',
411             'My::Step::DeployDogDatabase',
412             ],
413             config => {...},
414             );
415              
416             =head1 DESCRIPTION
417              
418             This class takes a set of objects which do the L<Stepford::Role::Step> role
419             and determines what order they should be run so as to get to one or more final
420             steps.
421              
422             Steps which are up to date are skipped during the run, so no unnecessary work
423             is done.
424              
425             =for Pod::Coverage BUILD add_step
426              
427             =head1 METHODS
428              
429             This class provides the following methods:
430              
431             =head2 Stepford::Runner->new(...)
432              
433             This method returns a new runner object. It accepts the following arguments:
434              
435             =over 4
436              
437             =item * step_namespaces
438              
439             This argument is required.
440              
441             This can either be a string or an array reference of strings. Each string
442             should contain a namespace which contains step classes.
443              
444             For example, if your steps are named C<My::Step::CreateFoo>,
445             C<My::Step::MergeFooAndBar>, and C<My::Step::DeployMergedFooAndBar>, the
446             namespace you'd provide is C<'My::Step'>.
447              
448             The order of the step namespaces I<is> relevant. If more than one step has a
449             production of the same name, then the first step "wins". Stepford sorts step
450             class names based on the order of the namespaces provided to the constructor,
451             and then the full name of the class. You can take advantage of this feature to
452             provide different steps in different environments (for example, for testing).
453              
454             The runner object assumes that every B<class> it finds in a step namespace is
455             a step. Specifically, if it finds a package that is a subclass of
456             L<Moose::Object>, then it throws an error if that package does not also
457             consume the L<Stepford::Role::Step> role.
458              
459             This means you can have utility packages and roles in a step namespace, but
460             not Moose objects which aren't steps.
461              
462             =item * jobs
463              
464             This argument defaults to 1.
465              
466             The number of jobs to run at a time. By default, all steps are run
467             sequentially. However, if you set this to a value greater than 1 then the
468             runner will run steps in parallel, up to the value you set.
469              
470             =item * logger
471              
472             This argument is optional.
473              
474             This should be an object that provides C<debug>, C<info>, C<notice>,
475             C<warning>, and C<error> methods.
476              
477             This object will receive log messages from the runner and (possibly your
478             steps).
479              
480             If this is not provided, Stepford will create a L<Log::Dispatch> object with a
481             single L<Log::Dispatch::Null> output (which silently eats all the logging
482             messages).
483              
484             Note that if you I<do> provide a logger object of your own, Stepford will not
485             load L<Log::Dispatch> into memory.
486              
487             =back
488              
489             =head2 $runner->run
490              
491             When this method is called, the runner comes up with a plan of the steps
492             needed to get to the requested final steps.
493              
494             When this method is called, we check for circular dependencies among the steps
495             and will throw a L<Stepford::Error> exception if it finds one. We also check
496             for unsatisfied dependencies for steps in the plan. Finally, we check to make
497             sure that no step provides its own dependencies as productions.
498              
499             For each step, the runner checks if it is up to date compared to its
500             dependencies (as determined by the C<< $step->last_run_time >> method. If the
501             step is up to date, it is skipped, otherwise the runner calls C<< $step->run
502             >> on the step. You can avoid this check and force all steps to be executed
503             with the C< force_step_execution > parameter (documented below.)
504              
505             Note that the step objects are always I<constructed>, so you should avoid
506             doing a lot of work in your constructor. Save that for the C<run> method.
507              
508             This method accepts the following parameters:
509              
510             =over 4
511              
512             =item * final_steps
513              
514             This argument is required.
515              
516             This can either be a string or an array reference of strings. Each string
517             should be a step's class name. These classes must do the
518             L<Stepford::Role::Step> role.
519              
520             These are the final steps run when the C<< $runner->run >> method is
521             called.
522              
523             =item * config
524              
525             This is an optional hash reference. For each step constructed, the runner
526             looks at the attributes that the step accepts. If they match any of the keys
527             in this hash reference, the key/value pair from this hash reference will be
528             passed to the step constructor. This matching is done based on attribute
529             C<init_arg> values.
530              
531             Note that values generated as productions from previous steps will override
532             the corresponding key in the config hash reference.
533              
534             =item * force_step_execution
535              
536             This argument defaults to false.
537              
538             This controls if we should force all steps to be executed rather than checking
539             which steps are up to date and do not need re-executing. Even with this set
540             each step will only be executed once per run regardless of how many other
541             steps depend on it during execution.
542              
543             =item * dry_run
544              
545             The argument is a boolean that defaults to false. When set to a true value,
546             Stepford prints out the steps that need to be executed and exits without
547             executing them.
548              
549             =back
550              
551             =head2 $runner->step_namespaces
552              
553             This method returns the step namespaces passed to the constructor as a list
554             (not an arrayref).
555              
556             =head2 $runner->logger
557              
558             This method returns the C<logger> used by the runner, either what you passed
559             to the constructor or a default.
560              
561             =head1 PARALLEL RUNS AND SERIALIZATION
562              
563             When running steps in parallel, the results of a step (its productions) are
564             sent from a child process to the parent by serializing them. This means that
565             productions which can't be serialized (like a filehandle or L<DBI> handle)
566             will cause the runner to die when it tries to serialize their productions.
567              
568             You can force a step class to be run in the same process as the runner itself
569             by having that step consume the L<Stepford::Role::Step::Unserializable>
570             role. Note that the runner may still fork I<after> a production has been
571             generated, so the values returned for a production must be able to survive a
572             fork, even if they cannot be serialized.
573              
574             You can also work around this by changing the production entirely. For
575             example, instead of passing a DBI handle you could pass a data structure with
576             a DSN, username, password, and connection options.
577              
578             =head1 SUPPORT
579              
580             Bugs may be submitted through L<https://github.com/maxmind/Stepford/issues>.
581              
582             =head1 AUTHOR
583              
584             Dave Rolsky <drolsky@maxmind.com>
585              
586             =head1 COPYRIGHT AND LICENSE
587              
588             This software is copyright (c) 2014 - 2023 by MaxMind, Inc.
589              
590             This is free software; you can redistribute it and/or modify it under
591             the same terms as the Perl 5 programming language system itself.
592              
593             =cut