File Coverage

blib/lib/Mojolicious/Plugin/ForkCart.pm
Criterion Covered Total %
statement 81 221 36.6
branch 11 62 17.7
condition 6 34 17.6
subroutine 24 40 60.0
pod 1 1 100.0
total 123 358 34.3


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