1package FCGI::Engine::ProcManager;
2use Moose;
3
4use constant DEBUG => 0;
5
6use POSIX qw(SA_RESTART SIGTERM SIGHUP);
7
8use FCGI::Engine::Types;
9use MooseX::Daemonize::Pid::File;
10
11our $VERSION   = '0.22';
12our $AUTHORITY = 'cpan:STEVAN';
13
14has 'role' => (
15    is      => 'rw',
16    isa     => 'FCGI::Engine::ProcManager::Role',
17    default => sub { 'manager' }
18);
19
20has 'start_delay' => (
21    is      => 'rw',
22    isa     => 'Int',
23    default => sub { 0 }
24);
25
26has 'die_timeout' => (
27    is      => 'rw',
28    isa     => 'Int',
29    default => sub { 60 }
30);
31
32has 'n_processes' => (
33    is       => 'rw',
34    isa      => 'Int',
35    default  => sub { 0 }
36);
37
38has 'pidfile' => (
39    is       => 'rw',
40    isa      => 'MooseX::Daemonize::Pid::File',
41#    coerce   => 1,
42);
43
44has 'no_signals' => (
45    is      => 'rw',
46    isa     => 'Bool',
47    default => sub { 0 }
48);
49
50has 'sigaction_no_sa_restart' => (is => 'rw', isa => 'POSIX::SigAction');
51has 'sigaction_sa_restart'    => (is => 'rw', isa => 'POSIX::SigAction');
52
53has 'signals_received' => (
54    is      => 'rw',
55    isa     => 'HashRef',
56    default => sub { +{} }
57);
58
59has 'manager_pid' => (
60    is  => 'rw',
61    isa => 'Int',
62);
63
64has 'server_pids' => (
65    traits  => [ 'Hash' ],
66    is      => 'rw',
67    isa     => 'HashRef',
68    clearer => 'forget_all_pids',
69    default => sub { +{} },
70    handles => {
71        '_add_pid'     => 'set',
72        'get_all_pids' => 'keys',
73        'remove_pid'   => 'delete',
74        'has_pids'     => 'count',
75        'pid_count'    => 'count',
76    }
77);
78
79sub add_pid { (shift)->_add_pid( @_, 1 ) }
80
81has 'process_name'         => (is => 'ro', isa => 'Str', default => sub { 'perl-fcgi'    });
82has 'manager_process_name' => (is => 'ro', isa => 'Str', default => sub { 'perl-fcgi-pm' });
83
84## methods ...
85
86sub BUILD {
87    my $self = shift;
88    unless ($self->no_signals()) {
89        $self->sigaction_no_sa_restart(
90            POSIX::SigAction->new(
91                'FCGI::Engine::ProcManager::sig_sub'
92            )
93        );
94        $self->sigaction_sa_restart(
95            POSIX::SigAction->new(
96                'FCGI::Engine::ProcManager::sig_sub',
97                undef,
98                POSIX::SA_RESTART
99            )
100        );
101    }
102}
103
104# this is the signal handler ...
105{
106    my $SIG_CODEREF;
107
108    sub sig_sub { $SIG_CODEREF->(@_) if ref $SIG_CODEREF }
109
110    sub clear_signal_handler { undef $SIG_CODEREF }
111
112    sub setup_signal_handler {
113        my $self = shift;
114        $SIG_CODEREF = $self->role eq 'manager'
115            ? sub { defined $self && $self->manager_sig_handler(@_) }
116            : sub { defined $self && $self->server_sig_handler(@_)  };
117    }
118}
119
120## main loop ...
121
122sub manage {
123    my $self = shift;
124
125    # skip to handling now if we won't be managing any processes.
126    $self->n_processes or return;
127
128    # call the (possibly overloaded) management initialization hook.
129    $self->role("manager");
130    $self->manager_init;
131    $self->notify("initialized");
132
133    my $manager_pid = $$;
134
135    MANAGING_LOOP: while (1) {
136
137        # FIXME
138        # we should tell the process that it is being
139        # run under some kind of daemon, which will mean
140        # that getppid will usually then return 1
141        # - SL
142        #getppid() == 1 and
143        #  return $self->die("calling process has died");
144
145        $self->n_processes > 0 or
146            return $self->die;
147
148        # while we have fewer servers than we want.
149        PIDS: while ($self->pid_count < $self->n_processes) {
150
151            if (my $pid = fork) {
152               # the manager remembers the server.
153               $self->add_pid($pid);
154               $self->notify("server (pid $pid) started");
155
156            }
157            elsif (! defined $pid) {
158                return $self->abort("fork: $!");
159            }
160            else {
161                $self->manager_pid($manager_pid);
162                # the server exits the managing loop.
163                last MANAGING_LOOP;
164            }
165
166            for (my $s = $self->start_delay; $s; $s = sleep $s) {};
167        }
168
169        # this should block until the next server dies.
170        $self->wait;
171
172    }# while 1
173
174    SERVER:
175
176    # forget any children we had been collecting.
177    $self->forget_all_pids;
178
179    # call the (possibly overloaded) handling init hook
180    $self->role("server");
181    $self->server_init;
182    $self->notify("initialized");
183
184    # server returns
185    return 1;
186}
187
188## initializers ...
189
190sub manager_init {
191    my $self = shift;
192
193    unless ($self->no_signals) {
194        $self->setup_signal_actions(with_sa_restart => 0);
195        $self->setup_signal_handler;
196    }
197
198    $self->change_process_name;
199
200    eval { $self->pidfile->write };
201    $self->notify("Could not write the PID file because: $@") if $@;
202
203    inner();
204}
205
206sub server_init {
207    my $self = shift;
208
209    unless ($self->no_signals) {
210        $self->setup_signal_actions(with_sa_restart => 0);
211        $self->setup_signal_handler;
212    }
213
214    $self->change_process_name;
215
216    inner();
217}
218
219
220## hooks ...
221
222sub pre_dispatch {
223    my $self = shift;
224
225    $self->setup_signal_actions(with_sa_restart => 1)
226        unless $self->no_signals;
227
228    inner();
229}
230
231sub post_dispatch {
232    my $self = shift;
233
234    $self->exit("safe exit after SIGTERM")
235        if $self->received_signal("TERM");
236
237    $self->exit("safe exit after SIGHUP")
238        if $self->received_signal("HUP");
239
240    if ($self->manager_pid and getppid() != $self->manager_pid) {
241        $self->exit("safe exit: manager has died");
242    }
243
244    $self->setup_signal_actions(with_sa_restart => 0)
245        unless $self->no_signals;
246
247    inner();
248}
249
250## utils ...
251
252# sig-handlers
253
254sub manager_sig_handler {
255    my ($self, $name) = @_;
256    if ($name eq "TERM") {
257        $self->notify("received signal $name");
258        $self->die("safe exit from signal $name");
259    }
260    elsif ($name eq "HUP") {
261        # send a TERM to each of the servers,
262        # and pretend like nothing happened..
263        if (my @pids = $self->get_all_pids) {
264            $self->notify("sending TERM to PIDs, @pids");
265            kill TERM => @pids;
266        }
267    }
268    else {
269        $self->notify("ignoring signal $name");
270    }
271}
272
273sub server_sig_handler {
274    my ($self, $name) = @_;
275    $self->received_signal($name, 1);
276}
277
278sub received_signal {
279    my ($self, $sig, $received) = @_;
280    return $self->signals_received unless $sig;
281    $self->signals_received->{$sig}++ if $received;
282    return $self->signals_received->{$sig};
283}
284
285sub change_process_name {
286    my $self = shift;
287    $0 = ($self->role eq 'manager' ? $self->manager_process_name : $self->process_name);
288}
289
290sub wait : method {
291    my $self = shift;
292
293    # wait for the next server to die.
294    return if (my $pid = CORE::wait()) < 0;
295
296    # notify when one of our servers have died.
297    $self->remove_pid($pid)
298        and $self->notify("server (pid $pid) exited with status $?");
299
300    return $pid;
301}
302
303## signal handling stuff ...
304
305sub setup_signal_actions {
306    my $self = shift;
307    my %args = @_;
308
309    my $sig_action = (exists $args{with_sa_restart} && $args{with_sa_restart})
310        ? $self->sigaction_sa_restart
311        : $self->sigaction_no_sa_restart;
312
313    POSIX::sigaction(POSIX::SIGTERM, $sig_action)
314        || $self->notify("sigaction: SIGTERM: $!");
315    POSIX::sigaction(POSIX::SIGHUP,  $sig_action)
316        || $self->notify("sigaction: SIGHUP: $!");
317}
318
319## notification ...
320
321sub notify {
322    my ($self, $msg) = @_;
323    $msg =~ s/\s*$/\n/;
324    print STDERR "FastCGI: " . $self->role() . " (pid $$): " . $msg;
325}
326
327## error/exit handlers ...
328
329sub die : method {
330    my ($self, $msg, $n) = @_;
331
332    # stop handling signals.
333    $self->clear_signal_handler;
334    $SIG{HUP}  = 'DEFAULT';
335    $SIG{TERM} = 'DEFAULT';
336
337    $self->pidfile->remove
338        || $self->notify("Could not remove PID file: $!");
339
340    # prepare to die no matter what.
341    if (defined $self->die_timeout) {
342        $SIG{ALRM} = sub { $self->abort("wait timeout") };
343        alarm $self->die_timeout;
344    }
345
346    # send a TERM to each of the servers.
347    if (my @pids = $self->get_all_pids) {
348        $self->notify("sending TERM to PIDs, @pids");
349        kill TERM => @pids;
350    }
351
352    # wait for the servers to die.
353    while ($self->has_pids) {
354        $self->wait;
355    }
356
357    # die already.
358    $self->exit("dying: $msg", $n);
359}
360
361sub abort {
362    my ($self, $msg, $n) = @_;
363    $n ||= 1;
364    $self->exit($msg, 1);
365}
366
367sub exit : method {
368    my ($self, $msg, $n) = @_;
369    $n ||= 0;
370
371    # if we still have children at this point,
372    # something went wrong. SIGKILL them now.
373    kill KILL => $self->get_all_pids
374        if $self->has_pids;
375
376    $self->notify($msg);
377    $@ = $msg;
378    CORE::exit $n;
379}
380
3811;
382
383__END__
384
385=pod
386
387=head1 NAME
388
389FCGI::Engine::ProcManager - module for managing FastCGI applications.
390
391=head1 DESCRIPTION
392
393This module is a refactoring of L<FCGI::ProcManager>, it behaves exactly the
394same, but the API is a little different. The function-oriented API has been
395removed in favor of object-oriented API. The C<pm_> prefix has been removed
396from  the hook routines and instead they now use the C<augment> and C<inner>
397functionality from L<Moose>. More docs will come eventually.
398
399=head2 Signal Handling
400
401FCGI::Engine::ProcManager attempts to do the right thing for proper shutdowns.
402
403When it receives a SIGHUP, it sends a SIGTERM to each of its children, and
404then resumes its normal operations.
405
406When it receives a SIGTERM, it sends a SIGTERM to each of its children, sets
407an alarm(3) "die timeout" handler, and waits for each of its children to
408die.  If all children die before this timeout, process manager exits with
409return status 0.  If all children do not die by the time the "die timeout"
410occurs, the process manager sends a SIGKILL to each of the remaining
411children, and exists with return status 1.
412
413FCGI::Engine::ProcManager uses POSIX::sigaction() to override the default
414SA_RESTART policy used for perl's %SIG behavior.  Specifically, the process
415manager never uses SA_RESTART, while the child FastCGI servers turn off
416SA_RESTART around the accept loop, but re-enstate it otherwise.
417
418The desired (and implemented) effect is to give a request as big a chance as
419possible to succeed and to delay their exits until after their request,
420while allowing the FastCGI servers waiting for new requests to die right
421away.
422
423=head1 METHODS
424
425I will fill this in more eventually, but for now if you really wanna know,
426read the source.
427
428=head1 SEE ALSO
429
430=over 4
431
432=item L<FCGI::ProcManager>
433
434This module is a fork of the FCGI::ProcManager code, with lots of
435code cleanup as well as general Moosificaition.
436
437=back
438
439=head1 BUGS
440
441All complex software has bugs lurking in it, and this module is no
442exception. If you find a bug please either email me, or add the bug
443to cpan-RT.
444
445=head1 AUTHOR
446
447Stevan Little E<lt>stevan@iinteractive.comE<gt>
448
449=head1 COPYRIGHT AND LICENSE
450
451Copyright 2007-2010 by Infinity Interactive, Inc.
452
453L<http://www.iinteractive.com>
454
455This library is free software; you can redistribute it and/or modify
456it under the same terms as Perl itself.
457
458=cut
459