File Coverage

blib/lib/HealthCheck/Parallel.pm
Criterion Covered Total %
statement 96 96 100.0
branch 36 38 94.7
condition 20 22 90.9
subroutine 16 16 100.0
pod 1 1 100.0
total 169 173 97.6


line stmt bran cond sub pod time code
1             package HealthCheck::Parallel;
2              
3 16     16   4039773 use v5.10;
  16         65  
4 16     16   83 use strict;
  16         18  
  16         523  
5 16     16   68 use warnings;
  16         16  
  16         764  
6              
7 16     16   4808 use parent 'HealthCheck';
  16         4503  
  16         100  
8              
9 16     16   141989 use Carp;
  16         32  
  16         1220  
10 16     16   8028 use Parallel::ForkManager;
  16         1434448  
  16         635  
11 16     16   117 use Scalar::Util qw( weaken );
  16         18  
  16         826  
12              
13             # ABSTRACT: A HealthCheck that uses parallelization for running checks
14 16     16   59 use version;
  16         33  
  16         99  
15             our $VERSION = 'v0.2.0'; # VERSION
16              
17             sub new {
18 187     187 1 5290212 my ( $class, %params ) = @_;
19              
20 187   100     1984 $params{max_procs} //= 4;
21 187   100     1519 $params{timeout} //= 120;
22              
23 187         1195 my $self = $class->SUPER::new( %params );
24              
25 187         15689 $self->_validate_max_procs( $params{max_procs} );
26 175         1573 $self->_validate_child_init( $params{child_init} );
27 165         756 $self->_validate_timeout( $params{timeout} );
28              
29 133         543 return $self;
30             }
31              
32             sub _run_checks {
33 149     149   110763 my ( $self, $checks, $params ) = @_;
34              
35 149   100     1308 my $child_init = $params->{child_init} // $self->{child_init};
36 149   33     876 my $tempdir = $params->{tempdir} // $self->{tempdir};
37              
38 149 100       508 $self->_validate_child_init( $child_init ) if defined $child_init;
39              
40 139         705 my $max_procs = $self->_validate_max_procs( $params->{max_procs} );
41 128         632 my $timeout = $self->_validate_timeout( $params->{timeout} );
42              
43 117         594 my @results;
44             my $forker;
45 117         0 my $start_time;
46 117         192 my $timed_out = 0;
47 117         254 my %killed_idents;
48             my %pid_to_ident;
49 117         198 my $last_dispatched_ident = -1;
50              
51 117 100       663 if ( $max_procs > 1 ) {
52 101 50       5265 $forker = Parallel::ForkManager->new(
53             $max_procs,
54             $tempdir ? $tempdir : (),
55             );
56              
57             $forker->run_on_finish(sub {
58 176     176   69555519 my ( $pid, $exit_code, $ident, $exit_sig, $core_dump, $ret ) = @_;
59              
60 176         833 delete $pid_to_ident{ $pid };
61              
62             # Child process had some error.
63 176 100       845 if ( $exit_code != 0 ) {
64 22         1588 $results[ $ident ] = {
65             status => 'CRITICAL',
66             info => "Child process exited with code $exit_code.",
67             };
68             }
69             else {
70             # Keep results in the same order that they were provided.
71 154         1271 $results[ $ident ] = $ret->[0];
72             }
73 101         305165 });
74              
75             # Set up on_wait callback to check timeout during dispatch.
76             # This is called periodically when start() is in its wait loop.
77 101         1074 $start_time = time;
78              
79             # Use weak reference to avoid circular reference between
80             # $forker and the callback closure.
81 101         305 my $weak_forker = $forker;
82 101         288 weaken $weak_forker;
83              
84             $forker->run_on_wait(sub {
85 319     319   143293607 my $elapsed = time - $start_time;
86              
87             # Check if we've exceeded timeout.
88 319 100       2366 if ( $elapsed > $timeout ) {
89 77         611 $timed_out = 1;
90              
91             # Kill all children and make start() exit its wait loop.
92             # Capture the idents of processes being killed so we can
93             # report timeout results for them.
94 77         580 my @running_pids = $weak_forker->running_procs;
95 77         1500 for my $pid ( @running_pids ) {
96 127         1613 $killed_idents{ $pid_to_ident{ $pid } } = 1;
97             }
98 77         3043 kill 'TERM', @running_pids;
99             }
100 101         776 }, 1); # Check every 1 second
101             }
102              
103 117         1144 my $i = 0;
104 117         376 for my $check ( @$checks ) {
105             # Stop dispatching if timeout occurred.
106 239 100       865 last if $timed_out;
107              
108 229         1310 my $ident = $last_dispatched_ident = $i++;
109              
110 229 100       644 if ( $forker ) {
111 197         800 my $pid = $forker->start( $ident );
112              
113 197 100       649704 if ( $pid ) {
114             # In parent - track this PID.
115 183         5540 $pid_to_ident{ $pid } = $ident;
116 183         37873 next;
117             }
118              
119             # Need to at least call the init callback before exiting so that we
120             # make sure to deal with things like FCGI cleanup.
121 14 100       848 $child_init->() if $child_init;
122              
123             # In child - if timeout occurred while waiting to start, exit
124             # immediately without running the check (start() forked before we
125             # could prevent it).
126 13 50       357 $forker->finish if $timed_out;
127             }
128              
129 45         1045 my @r = $self->_run_check( $check, $params );
130              
131 44 100       2025338 $forker->finish( 0, \@r ) if $forker;
132              
133             # Non-forked process.
134 32         392 push @results, @r;
135             }
136              
137 103 100       3728 $forker->wait_all_children if $forker;
138              
139             # If timeout occurred, fill in timeout results for killed processes
140             # and checks that never started.
141 103 100       1553 if ( $timed_out ) {
142             # Add timeout results for killed processes.
143 37         523 for my $ident ( keys %killed_idents ) {
144 77         918 $results[ $ident ] = {
145             status => 'CRITICAL',
146             info => sprintf(
147             'Check killed due to global timeout of %d seconds.',
148             $timeout,
149             ),
150             };
151             }
152              
153             # Add timeout results for checks that were never dispatched.
154             # Only fill in idents greater than the last one we actually dispatched.
155 37         315 for my $ident ( $last_dispatched_ident + 1 .. @$checks - 1 ) {
156 10         160 $results[ $ident ] = {
157             status => 'CRITICAL',
158             info => sprintf(
159             'Check not started due to global timeout of %d seconds.',
160             $timeout,
161             ),
162             };
163             }
164             }
165              
166 103         9121 return @results;
167             }
168              
169             sub _resolve_value {
170 619     619   1297 my ( $self, $value ) = @_;
171              
172 619 100       1910 return ref $value eq 'CODE' ? $value->() : $value;
173             }
174              
175             sub _validate_max_procs {
176 326     326   1204 my ( $self, $max_procs ) = @_;
177              
178 326 100       993 $max_procs = $self->{max_procs} unless defined $max_procs;
179              
180 326         1023 my $value = $self->_resolve_value( $max_procs );
181              
182 326 100 100     11114 croak "max_procs must be a zero or positive integer!"
183             unless defined $value && $value =~ /^\d+$/;
184              
185 303         865 return $value;
186             }
187              
188             sub _validate_child_init {
189 198     198   734 my ( $self, $child_init ) = @_;
190              
191 198 100 100     6076 croak "child_init must be a code reference!"
192             if defined $child_init && ref( $child_init ) ne 'CODE';
193             }
194              
195             sub _validate_timeout {
196 293     293   705 my ( $self, $timeout ) = @_;
197              
198 293 100       936 $timeout = $self->{timeout} unless defined $timeout;
199              
200 293         752 my $value = $self->_resolve_value( $timeout );
201              
202 293 100 100     10672 croak "timeout must be a positive integer!"
      100        
203             unless defined $value && $value =~ /^\d+$/ && $value > 0;
204              
205 250         794 return $value;
206             }
207              
208             1;
209              
210             __END__