File Coverage

blib/lib/Mojolicious/Plugin/ForkCart.pm
Criterion Covered Total %
statement 22 74 29.7
branch 0 16 0.0
condition 1 20 5.0
subroutine 8 12 66.6
pod n/a
total 31 122 25.4


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::ForkCart;
2 1     1   443 use Mojo::Base 'Mojolicious::Plugin';
  1         1  
  1         5  
3              
4 1     1   139 use Time::HiRes qw(usleep);
  1         1  
  1         4  
5              
6             our $VERSION = '0.02';
7             our $pkg = __PACKAGE__;
8              
9             our $caddy_pkg = "${pkg}::Caddy";
10             our $plugin_pkg = "${pkg}::Plugin";
11             our $count = 0;
12              
13 1   50 1   151 use constant DEBUG => $ENV{MOJOLICIOUS_PLUGIN_FORKCART_DEBUG} || 0;
  1         2  
  1         623  
14              
15             sub register {
16 0     0     my ($cart, $app, $ops) = @_;
17              
18 0           my $caddy = $caddy_pkg->new(app => $app);
19              
20 0 0 0       if ($caddy->is_alive && $ENV{HYPNOTOAD_STOP}) {
21 0           my $state = $caddy->state;
22 0           $state->{shutdown} = 1;
23 0           $caddy->state($state);
24              
25 0           return;
26             }
27              
28 0 0 0       if ($caddy->is_alive && !$ENV{MOJOLICIOUS_PLUGIN_FORKCART_ADD}) {
    0 0        
    0 0        
    0          
29 0   0       $app->log->info("$$: " . ($caddy->state->{caddy_pid} // "") . " is alive: shutdown");
30              
31 0           my $state = $caddy->state;
32 0           $state->{shutdown} = 1;
33 0           $caddy->state($state);
34              
35 0           while ($caddy->is_alive) {
36 0   0       $app->log->info("$$: " . ($caddy->state->{caddy_pid} // "") . " is alive: waiting");
37              
38 0           usleep(50000);
39             }
40              
41 0           unlink($caddy->state_file);
42             } elsif ($caddy->is_alive) {
43 0   0       $app->log->info("$$: " . ($caddy->state->{caddy_pid} // "") . " is alive: $ENV{MOJOLICIOUS_PLUGIN_FORKCART_ADD}");
44              
45             } elsif ($ARGV[0] && $ARGV[0] =~ m/^(daemon|prefork)$/) {
46 0           my $state_file = $caddy->state_file;
47              
48 0           $app->log->info("$$: $ARGV[0]: unlink($state_file)");
49              
50 0           unlink($state_file);
51             } elsif ($ENV{HYPNOTOAD_REV} && 2 <= $ENV{HYPNOTOAD_REV}) {
52 0           my $state_file = $caddy->state_file;
53              
54 0           $app->log->info("$$: hypnotoad: unlink($state_file)");
55              
56 0           unlink($state_file);
57             }
58              
59             $app->helper(forked => sub {
60 0     0     ++$count;
61              
62 0           Mojo::IOLoop->next_tick($caddy->add(pop));
63 0           });
64              
65 0 0         if ($ops->{process}) {
66 0           $plugin_pkg->$_($caddy) for @{ $ops->{process} };
  0            
67             }
68             }
69              
70             package Mojolicious::Plugin::ForkCart::Plugin;
71 1     1   4 use Mojo::Base -base;
  1         1  
  1         5  
72              
73 1     1   83 use constant DEBUG => Mojolicious::Plugin::ForkCart::DEBUG;
  1         2  
  1         254  
74              
75             sub minion {
76 0     0     my $caddy = pop;
77              
78 0           my $app = $caddy->app;
79              
80 0 0         $app->plugin(qw(Mojolicious::Plugin::ForkCall))
81             unless $app->can("fork_call");
82              
83             $app->forked(sub {
84 0     0     my $app = shift;
85              
86 0           $app->log->info("$$: Child forked: " . getppid);
87              
88             $app->fork_call(
89             sub {
90 0           $app->log->info("$$: Child fork_call: " . getppid);
91              
92             # I dunno why I have (or if I have) to do this for hypnotoad
93 0           delete($ENV{HYPNOTOAD_APP});
94 0           delete($ENV{HYPNOTOAD_EXE});
95 0           delete($ENV{HYPNOTOAD_FOREGROUND});
96 0           delete($ENV{HYPNOTOAD_REV});
97 0           delete($ENV{HYPNOTOAD_STOP});
98 0           delete($ENV{HYPNOTOAD_TEST});
99 0           delete($ENV{MOJO_APP_LOADER});
100            
101 0           my @cmd = (
102             $^X,
103             $0,
104             "minion",
105             "worker"
106             );
107 0           $0 = join(" ", @cmd);
108              
109 0           $app->log->debug("$$: ForkCart minion worker") if DEBUG;
110 0 0         system(@cmd) == 0
111             or die("0: $?");
112              
113 0           return 1;
114             },
115             sub {
116 0           exit;
117             }
118 0           );
119 0           });
120             }
121              
122             package Mojolicious::Plugin::ForkCart::Caddy;
123 1     1   4 use Mojo::Base -base;
  1         1  
  1         4  
124              
125 1     1   74 use Mojo::IOLoop;
  1         1  
  1         7  
126 1     1   239 use Devel::Refcount qw(refcount);
  0            
  0            
127             use File::Spec::Functions qw(catfile tmpdir);
128             use IO::Handle;
129             use Fcntl qw(O_RDWR O_CREAT O_EXCL LOCK_EX SEEK_SET LOCK_UN :flock);
130             use Mojo::Util qw(slurp spurt steady_time);
131             use Mojo::JSON qw(encode_json decode_json);
132             use POSIX qw(:sys_wait_h);
133             use Time::HiRes qw(usleep);
134              
135             our %code = ();
136             our $created = 0;
137              
138             has qw(app);
139             has state_file => sub { catfile tmpdir, 'forkngo.state_file' };
140              
141             use constant DEBUG => Mojolicious::Plugin::ForkCart::DEBUG;
142              
143             sub watchdog {
144             my $caddy = shift;
145              
146             return sub {
147             my $state = $caddy->state;
148              
149             # exit unless kill("SIGZERO", $caddy->state->{caddy_manager}) || $caddy->state->{shutdown};
150             kill("-KILL", getpgrp) if $caddy->state->{shutdown};
151              
152             $caddy->app->log->info("$$: Caddy recurring: " . scalar(keys %{$state->{slots}}));
153             };
154             };
155              
156             sub is_alive {
157             my $caddy = shift;
158              
159             return $caddy->state->{caddy_pid} ? kill("SIGZERO", $caddy->state->{caddy_pid}) : 0;
160             }
161              
162             sub lock {
163             my $fh = pop;
164             flock($fh, LOCK_EX) or die "Cannot lock ? - $!\n";
165              
166             # and, in case someone appended while we were waiting...
167             seek($fh, 0, SEEK_SET) or die "Cannot seek - $!\n";
168             }
169              
170             sub unlock {
171             my $fh = pop;
172             flock($fh, LOCK_UN) or die "Cannot unlock ? - $!\n";
173             }
174              
175             sub state {
176             my $caddy = shift;
177             my $new_state = shift;
178              
179             # Should be created by sysopen
180             my $fh;
181             if (-f $caddy->state_file && -s $caddy->state_file) {
182             open($fh, ">>", $caddy->state_file)
183             or die(sprintf("Can't open %s", $caddy->state_file));
184              
185             $caddy->lock($fh);
186             }
187             elsif (!$new_state) {
188             return {};
189             }
190              
191             if ($new_state) {
192             spurt(encode_json($new_state), $caddy->state_file);
193              
194             $caddy->unlock($fh);
195              
196             return $new_state;
197             }
198             elsif (-f $caddy->state_file) {
199             my $ret = decode_json(slurp($caddy->state_file));
200              
201             $caddy->unlock($fh);
202              
203             return $ret;
204             }
205             else {
206             $caddy->unlock($fh);
207              
208             return {};
209             }
210             }
211              
212             sub is_me {
213             my $state = shift->state;
214             return 0 if !defined $state->{caddy_pid};
215             return $state->{caddy_pid} == $$;
216             }
217              
218             sub add {
219             my $caddy = shift;
220              
221             my $code_key = steady_time;
222             $code{$code_key} = shift;
223              
224             return sub {
225             my $state_file = $caddy->state_file;
226            
227             my $app = $caddy->app;
228            
229             eval {
230             $app->log->info("$$: Worker next_tick");
231            
232             sysopen(my $fh, $state_file, O_RDWR|O_CREAT|O_EXCL) or die("$state_file: $$: $!\n");
233             spurt(encode_json({ shutdown => 0, caddy_pid => $$, caddy_manager => $ARGV[0] && $ARGV[0] =~ m/daemon/ ? $$ : getppid }), $state_file);
234             close($fh);
235             };
236            
237             # Outside the caddy
238             if ($@ && !$caddy->is_me) {
239             chomp(my $err = $@);
240            
241             $app->log->info("$$: sysopen($state_file): $err");
242            
243             return sub { };
244             }
245            
246             return if !$caddy->is_me;
247            
248             # Inside the caddy
249             $app->log->info("$state_file: sysopen($$) <-- caddy: " . ($ENV{MOJOLICIOUS_PLUGIN_FORKCART_ADD} // 'undef'));
250            
251             my $state = $caddy->state;
252             my $slots = $state->{slots} //= {};
253            
254             $slots->{$code_key} = {};
255             $slots->{$code_key}{created} = $created;
256            
257             ++$ENV{MOJOLICIOUS_PLUGIN_FORKCART_ADD};
258             spurt(encode_json($state), $state_file);
259            
260             $app->log->info("$$-->: $created: $Mojolicious::Plugin::ForkCart::count") if DEBUG;
261            
262             # Create the slots in the caddy
263             Mojo::IOLoop->next_tick($caddy->create) if ++$created == $Mojolicious::Plugin::ForkCart::count;
264             };
265             }
266              
267             sub create {
268             my $caddy = shift;
269              
270             $caddy->app->log->info("$$: Caddy create");
271              
272             return(sub {
273             my $state = $caddy->state;
274             my $app = $caddy->app;
275              
276             # Belt and suspenders error checking, shouldn't be reached (I think)
277             if ($state->{caddy} && $$ != $state->{caddy}) {
278             my $msg = "We are not the caddy";
279              
280             $app->log->error($msg);
281              
282             die($msg);
283             }
284              
285             $app->log->info("$$: caddy->state->{caddy_manager}: " . $caddy->state->{caddy_manager});
286              
287             # Watchdog
288             Mojo::IOLoop->recurring(1 => $caddy->watchdog);
289              
290             foreach my $code_key (keys %{ $state->{slots} }) {
291             $app->log->info("$$: $code_key: $code{$code_key}");
292              
293             my $pid = $caddy->fork($code{$code_key});
294              
295             $state->{slots}{$code_key}{pid} = $pid if $$ != $pid;
296             $caddy->state($state) if $$ != $pid;
297             }
298             });
299             }
300              
301             sub fork {
302             my $caddy = shift;
303             my $code = shift;
304            
305             my $app = $caddy->app;
306              
307             my $pgroup = getpgrp;
308              
309             die "Can't fork: $!" unless defined(my $pid = fork);
310             if ($pid) { # Parent
311              
312             $app->log->info("$$: Parent return");
313              
314             $SIG{CHLD} = sub {
315             while ((my $child = waitpid(-1, WNOHANG)) > 0) {
316             $app->log->info("$$: Parent waiting: $child");
317             }
318             };
319              
320             return $pid;
321             }
322              
323             $app->log->info("$$: Slot running: $$: " . getppid);
324              
325             setpgrp($pid, $pgroup);
326              
327             # Caddy's Child
328             Mojo::IOLoop->reset;
329              
330             Mojo::IOLoop->recurring(1 => sub {
331             my $loop = shift;
332              
333             my $str = sprintf("%s", join(", ", @{ $caddy->state }{'caddy_manager', 'shutdown'}));
334             $app->log->info("$$: Caddy slot monitor: $str");
335              
336             # TODO: Do a graceful stop
337             kill("-KILL", $pgroup) if $caddy->state->{shutdown} || !$caddy->is_alive;
338             });
339              
340             $code->($app);
341              
342             Mojo::IOLoop->start;
343              
344             return $$;
345             }
346              
347             sub pid_wait {
348             my ($pid, $timeout) = @_;
349              
350             my $ret;
351              
352             my $done = steady_time + $timeout;
353             do {
354             $ret = kill("SIGZERO", $pid);
355              
356             usleep 50000 if $ret;
357              
358             } until(!$ret || $done < steady_time);
359              
360             return !$ret;
361             }
362              
363             1;
364              
365             __END__