From: James E Jurach Jr Date: Wed, 31 Jan 2001 07:30:26 +0000 (-0800) Subject: import FCGI-ProcManager 0.15 from CPAN X-Git-Tag: 0.15 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=catagits%2FFCGI-ProcManager.git;a=commitdiff_plain;h=518709edb620494ace324da50ad3494a351d1e02 import FCGI-ProcManager 0.15 from CPAN git-cpan-module: FCGI-ProcManager git-cpan-version: 0.15 git-cpan-authorid: JURACH git-cpan-file: authors/id/J/JU/JURACH/FCGI-ProcManager-0.15.tar.gz --- diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..9543b5c --- /dev/null +++ b/ChangeLog @@ -0,0 +1,80 @@ +2001-01-31 01:13 James Jurach + + * README: more documentation + + * ProcManager.pm: re-organized method layout. separated manager + vs. server vs. common moved more intialization into managing_init() + and handling_init() created pm_die() to be called when manager + should die. cleaned up error messages and notifications. + + * t/: exporter.t, procmanager.t: revised test messages + +2001-01-30 16:35 James Jurach + + * t/: exporter.t, procmanager.t: removed call to pm_state() + + * ProcManager.pm: removed sleep() call after testing + signal-during-fork-loop + +2001-01-30 12:49 James Jurach + + * ProcManager.pm: Cleaned up pm_manage(). removed signal + registration routines. replaced by direct %SIG access. there is + now a manager signal handler and a handler signal handler. added + checks to die when getppid() changes. + +2001-01-20 02:06 James Jurach + + * MANIFEST: removed Changes from MANIFEST + +2001-01-13 00:44 James Jurach + + * t/procmanager.t: corrected various recent method name changes. + removed calls to want_to_die(). added request loop hooks to + pm_pre_dispatch(), pm_post_dispatch(). renamed sample_handler() to + sample_request_loop(). + + * ProcManager.pm: renamed request loop hooks to pm_pre_dispatch(), + pm_post_dispatch(). when pm_manage() is called with + n_processes==0, return through goto. renamed pre_manage_init() to + managing_init(). renamed post_manage_init() to handling_init(). + added received_signal() to remember that a signal was received. + signal handler simpler: now only notes signal and propagates + signal. + + * t/exporter.t: this uses non-OO calling mode. + + * MANIFEST: added t/exporter.t. + + * Changes: ChangeLog is where per-file changes are listed. + +2000-12-22 05:58 James Jurach + + * ProcManager.pm: corrected logic error in self_or_default(). + changed all occurances of write_pid_file() to pm_write_pid_file(). + changed all occurances of remove_pid_file() to + pm_remove_pid_file(). + +2000-12-14 17:54 James Jurach + + * ProcManager.pm: corrected state() -> pm_state() method call. + +2000-12-10 17:25 James Jurach + + * ProcManager.pm: corrected some method renaming issues. + +2000-12-09 19:48 James Jurach + + * t/procmanager.t: effected the changes to these subroutine names. + + * ProcManager.pm: made this module OO/Exporter hybrid. removed + treatment of $ENV{PROCMANAGER_PROCESSES} from constructor. changed + several subroutine names. + +2000-12-05 22:23 James Jurach + + * ProcManager.pm: made SIGHUP's do what SIGTERM's do + +2000-11-20 James Jurach + + * Released first public version. diff --git a/MANIFEST b/MANIFEST index ffd614e..9d66c4e 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,6 +1,7 @@ COPYING MANIFEST MANIFEST.SKIP +ChangeLog Makefile.PL ProcManager.pm README diff --git a/ProcManager.pm b/ProcManager.pm index 5d90a82..d11835b 100644 --- a/ProcManager.pm +++ b/ProcManager.pm @@ -5,23 +5,22 @@ package FCGI::ProcManager; # Public License, Version 2.1. Please read the important licensing and # disclaimer information included below. -# $Id: ProcManager.pm,v 1.12 2001/01/13 06:44:34 muaddib Exp $ +# $Id: ProcManager.pm,v 1.15 2001/01/31 07:00:55 muaddib Exp $ use strict; use Exporter; -use vars qw($VERSION @ISA @EXPORT_OK %EXPORT_TAGS $Q @valid_states); +use vars qw($VERSION @ISA @EXPORT_OK %EXPORT_TAGS $Q); BEGIN { - $VERSION = '0.14'; + $VERSION = '0.15'; @ISA = qw(Exporter); - @EXPORT_OK = qw(pm_manage pm_parameter pm_state pm_warn pm_abort pm_exit + @EXPORT_OK = qw(pm_manage pm_die pm_reap_server pm_write_pid_file pm_remove_pid_file pm_pre_dispatch pm_post_dispatch - pm_register_sig_handler pm_unregister_sig_handler); + pm_change_process_name pm_received_signal pm_parameter + pm_warn pm_notify pm_abort pm_exit); $EXPORT_TAGS{all} = \@EXPORT_OK; $FCGI::ProcManager::Default = 'FCGI::ProcManager'; - - @valid_states = qw(managing handling idle); } =head1 NAME @@ -30,19 +29,49 @@ BEGIN { =head1 SYNOPSIS - # In its simplest form. +{ + # In Object-oriented style. use CGI::Fast; use FCGI::ProcManager; - my $proc_manager = FCGI::ProcManager->new({n_processes=>10}); - $proc_manager->manage(); + my $proc_manager = FCGI::ProcManager->new({ n_processes => 10 }); + $proc_manager->pm_manage(); while (my $cgi = CGI::Fast->new()) { + $proc_manager->pm_pre_dispatch(); + # ... handle the request here ... + $proc_manager->pm_post_dispatch(); + } + + # This style is also supported: + use CGI::Fast; + use FCGI::ProcManager qw(pm_manage pm_pre_dispatch pm_post_dispatch); + pm_manage( n_processes => 10 ); + while (my $cgi = CGI::Fast->new()) { + pm_pre_dispatch(); #... + pm_post_dispatch(); + } =head1 DESCRIPTION -FCGI::ProcManager is used to serve as a FastCGI process manager. -The parent uses fork(2) and wait(2) to manage a set of FastCGI application -servers. more later. +FCGI::ProcManager is used to serve as a FastCGI process manager. By +re-implementing it in perl, developers can more finely tune performance in +their web applications, and can take advantage of copy-on-write semantics +prevalent in UNIX kernel process management. The process manager should +be invoked before the caller''s request loop + +The primary routine, C, enters a loop in which it maintains a +number of FastCGI servers (via fork(2)), and which reaps those servers +when they die (via wait(2)). + +C provides too hooks: + + C - called just before the manager enters the manager loop. + C - called just before a server is returns from C + +It is necessary for the caller, when implementing its request loop, to +insert a call to C at the top of the loop, and then +7C at the end of the loop. + =head1 METHODS @@ -53,7 +82,11 @@ servers. more later. sub new { my ($proto,$init) = @_; - my $this = {}; + my $this = { + role => "manager", + start_delay => 0, + die_timeout => 60 + }; $init and %$this = %$init; bless $this, ref($proto)||$proto; @@ -63,27 +96,7 @@ sub new { return $this; } -=head2 self_or_default - - private global - (ProcManager, @args) self_or_default([ ProcManager, ] @args); - -DESCRIPTION: - -This is a helper subroutine to acquire or otherwise create a singleton -default object if one is not passed in, e.g., a method call. - -=cut - -sub self_or_default { - return @_ if defined $_[0] and !ref $_[0] and $_[0] eq 'FCGI::ProcManager'; - if (!defined $_[0] or (ref($_[0]) ne 'FCGI::ProcManager' and - !UNIVERSAL::isa($_[0],'FCGI::ProcManager'))) { - $Q or $Q = $FCGI::ProcManager::Default->new; - unshift @_, $Q; - } - return wantarray ? @_ : $Q; -} +=head1 Manager methods =head2 pm_manage @@ -97,73 +110,62 @@ When this is called by a FastCGI script to manage application servers. =cut sub pm_manage { - my ($this) = self_or_default(@_); - - # initialize state and begin to handle signals. - $this->pm_register_sig_handler(); + my ($this,%values) = self_or_default(@_); + map { $this->pm_parameter($_,$values{$_}) } keys %values; - # switch to handling state right away if we are not managing any processes. + # skip to handling now if we won't be managing any processes. $this->n_processes() or goto HANDLING; - # begin the managing sequence. - $this->pm_state("managing"); - - # call the (possibly overloaded) managing initialization. + # call the (possibly overloaded) management initialization hook. + $this->role("manager"); $this->managing_init(); + $this->pm_notify("initialized"); - # write out the pid file. - $this->pm_write_pid_file(); + my $manager_pid = $$; - my ($pid); - MANAGE: while (1) { - - # do things that we only do when we're not already managing processes.. - if (! %{$this->{PIDS}}) { - if ($this->received_signal()) { - $this->pm_remove_pid_file(); - $this->pm_exit("Manager $$ dying from death request.\n"); - } elsif ($this->n_processes() < 0) { - $this->pm_remove_pid_file(); - $this->pm_abort("Manager $$ dying from processes number exception: ". - $this->n_processes(), -( 1 + $this->n_processes())); - } - } + MANAGING_LOOP: while (1) { + + # if the calling process goes away, perform cleanup. + getppid() == 1 and + return $this->pm_die("calling process has died"); - # if we have fewer children than we want.. - PIDS: while (keys(%{$this->{PIDS}}) < $this->n_processes()) { + $this->n_processes() > 0 or + return $this->pm_die(); - # fork. - if ($pid = fork()) { - # the parent notes the child. - $this->pm_warn("started process $pid\n"); + # while we have fewer servers than we want. + PIDS: while (keys(%{$this->{PIDS}}) < $this->n_processes()) { + + if (my $pid = fork()) { + # the manager remembers the server. $this->{PIDS}->{$pid} = { pid=>$pid }; + $this->pm_notify("server (pid $pid) started"); } elsif (! defined $pid) { - # handle errors um gracefully. - $this->pm_abort("fork: $!\n"); + return $this->pm_abort("fork: $!"); } else { - # the child returns to the calling application. - last MANAGE; + $this->role("server"); + $this->{MANAGER_PID} = $manager_pid; + # the server exits the managing loop. + last MANAGING_LOOP; } - } - # wait on the next child to die. - $this->pm_abort("wait: $!\n") if ($pid = wait()) < 0; + for (my $s = $this->start_delay(); $s; $s = sleep $s) {}; + } - # notify when one of our children have died. - delete $this->{PIDS}->{$pid} and - $this->pm_warn("Child process $pid died with exit status $?\n"); + # this should block until the next server dies. + $this->pm_reap_server(); }# while 1 - HANDLING: - $this->pm_state("handling"); +HANDLING: - # call the (possibly overloaded) handling initialization. + # call the (possibly overloaded) handling init hook + $this->role("server"); $this->handling_init(); + $this->pm_notify("initialized"); - # children and parent with n_processes == 0 return to calling app. + # server returns return 1; } @@ -173,35 +175,62 @@ sub pm_manage { sub managing_init { my ($this) = self_or_default(@_); -} -=head2 handling_init + # begin to handle signals. + $SIG{TERM} = sub { $this->sig_manager(@_) }; + $SIG{HUP} = sub { $this->sig_manager(@_) }; -=cut + # change the name of this process as it appears in ps(1) output. + $this->pm_change_process_name("perl-fcgi-pm"); -sub handling_init { - my ($this) = self_or_default(@_); + $this->pm_write_pid_file(); } -=head2 pm_pre_dispatch + +=head2 pm_die =cut -sub pm_pre_dispatch { - my ($this) = self_or_default(@_); +sub pm_die { + my ($this,$msg,$n) = self_or_default(@_); + + # stop handling signals. + $SIG{HUP} = 'DEFAULT'; + $SIG{TERM} = 'DEFAULT'; + + $this->pm_remove_pid_file(); + + # prepare to die no matter what. + if (defined $this->die_timeout()) { + $SIG{ARLM} = sub { $this->pm_abort("reap timeout") }; + alarm $this->die_timeout(); + } + + # send a TERM to each of the servers. + kill "TERM", keys %{$this->{PIDS}}; + + # wait for the servers to die. + while (%{$this->{PIDS}}) { + $this->pm_reap_server(); + } + + # die already. + $this->pm_exit("dying: ".$msg,$n); } -=head2 pm_post_dispatch +=head2 pm_reap_server =cut -sub pm_post_dispatch { +sub pm_reap_server { my ($this) = self_or_default(@_); - if (my $name = $this->received_signal()) { - if ($name eq "HUP" or $name eq "TERM") { - $this->pm_exit("Process $$ responding to $name death request.\n"); - } - } + + # wait for the next server to die. + next if (my $pid = wait()) < 0; + + # notify when one of our servers have died. + delete $this->{PIDS}->{$pid} and + $this->pm_notify("server (pid $pid) exited with status $?"); } =head2 pm_write_pid_file @@ -212,7 +241,7 @@ sub pm_write_pid_file { my ($this,$fname) = self_or_default(@_); $fname ||= $this->pid_fname() or return; if (!open PIDFILE, ">$fname") { - $this->pm_warn("open: $fname: $!\n"); + $this->pm_warn("open: $fname: $!"); return; } print PIDFILE "$$\n"; @@ -226,93 +255,166 @@ sub pm_write_pid_file { sub pm_remove_pid_file { my ($this,$fname) = self_or_default(@_); $fname ||= $this->pid_fname() or return; - my $ret = unlink($fname) or $this->pm_warn("unlink: $fname: $!\n"); + my $ret = unlink($fname) or $this->pm_warn("unlink: $fname: $!"); return $ret; } -=head2 pm_parameter +=head2 sig_manager =cut -sub pm_parameter { - my ($this,$key,$value) = self_or_default(@_); - defined $value and $this->{$key} = $value; - return $this->{$key}; +sub sig_manager { + my ($this,$name) = @_; + if ($name eq "TERM" or $name eq "HUP") { + $this->pm_die("received signal $name"); + } else { + $this->pm_notify("ignoring signal $name"); + } } -=head2 n_processes +=head1 Handler methods -=head2 no_signals - -=head2 pid_fname +=head2 handling_init =cut -sub n_processes { shift->pm_parameter("n_processes", @_); } -sub pid_fname { shift->pm_parameter("pid_fname", @_); } -sub received_signal { shift->pm_parameter("received_signal", @_); } -sub no_signals { shift->pm_parameter("no_signals", @_); } - -=head2 pm_state +sub handling_init { + my ($this) = self_or_default(@_); -=cut + # begin to handle signals. + $SIG{TERM} = sub { $this->sig_handler(@_) }; + $SIG{HUP} = sub { $this->sig_handler(@_) }; -sub pm_state { - my ($this,$new_state) = self_or_default(@_); - if (defined $new_state) { - if (!grep {$new_state eq $_} @valid_states) { - $this->pm_abort("Invalid state: $new_state\n"); - } - $this->{state} = $new_state; - } - defined $this->{state} or $this->{state} = "idle"; - return $this->{state}; + # change the name of this process as it appears in ps(1) output. + $this->pm_change_process_name("perl-fcgi"); } -=head2 pm_register_sig_handler +=head2 pm_pre_dispatch =cut -sub pm_register_sig_handler { +sub pm_pre_dispatch { my ($this) = self_or_default(@_); - return if $this->no_signals(); - $SIG{TERM} = sub { $this->sig_method(@_) }; - $SIG{HUP} = sub { $this->sig_method(@_) }; } -=head2 pm_unregister_sig_handler +=head2 pm_post_dispatch =cut -sub pm_unregister_sig_handler { +sub pm_post_dispatch { my ($this) = self_or_default(@_); - return if $this->no_signals(); - undef $SIG{TERM}; - undef $SIG{HUP}; + if ($this->pm_received_signal("TERM")) { + $this->pm_exit("safe exit after SIGTERM"); + } + if ($this->pm_received_signal("HUP")) { + $this->pm_exit("safe exit after SIGHUP"); + } + if (getppid() != $this->{MANAGER_PID}) { + $this->pm_exit("safe exit: manager has died"); + } } -=head2 sig_method +=head2 sig_handler =cut -sub sig_method { +sub sig_handler { my ($this,$name) = @_; - # note which signal we've received. - $this->{received_signal} = $name; - $this->n_processes(0); - # propagate this signal to children. (is this necessary?) - if (%{$this->{PIDS}}) { - kill $name, keys %{$this->{PIDS}}; + $this->pm_received_signal($name,1); +} + +=head1 Common methods and routines + +=head2 self_or_default + + private global + (ProcManager, @args) self_or_default([ ProcManager, ] @args); + +DESCRIPTION: + +This is a helper subroutine to acquire or otherwise create a singleton +default object if one is not passed in, e.g., a method call. + +=cut + +sub self_or_default { + return @_ if defined $_[0] and !ref $_[0] and $_[0] eq 'FCGI::ProcManager'; + if (!defined $_[0] or (ref($_[0]) ne 'FCGI::ProcManager' and + !UNIVERSAL::isa($_[0],'FCGI::ProcManager'))) { + $Q or $Q = $FCGI::ProcManager::Default->new; + unshift @_, $Q; } + return wantarray ? @_ : $Q; +} + +=head2 pm_change_process_name + +=cut + +sub pm_change_process_name { + my ($this,$name) = self_or_default(@_); + $0 = $name; +} + +=head2 pm_received_signal + +=cut + +sub pm_received_signal { + my ($this,$sig,$received) = self_or_default(@_); + $sig or return $this->{SIG_RECEIVED}; + $received and $this->{SIG_RECEIVED}->{$sig}++; + return $this->{SIG_RECEIVED}->{$sig}; +} + +=head2 pm_parameter + +=cut + +sub pm_parameter { + my ($this,$key,$value) = self_or_default(@_); + defined $value and $this->{$key} = $value; + return $this->{$key}; } +=head2 n_processes + +=head2 no_signals + +=head2 pid_fname + +=head2 die_timeout + +=head2 role + +=head2 start_delay + +=cut + +sub n_processes { shift->pm_parameter("n_processes", @_); } +sub pid_fname { shift->pm_parameter("pid_fname", @_); } +sub no_signals { shift->pm_parameter("no_signals", @_); } +sub die_timeout { shift->pm_parameter("die_timeout", @_); } +sub role { shift->pm_parameter("role", @_); } +sub start_delay { shift->pm_parameter("start_delay", @_); } + =head2 pm_warn =cut sub pm_warn { my ($this,$msg) = self_or_default(@_); - print STDERR $msg; + $this->pm_notify($msg); +} + +=head2 pm_notify + +=cut + +sub pm_notify { + my ($this,$msg) = self_or_default(@_); + $msg =~ s/\s*$/\n/; + print STDERR "FastCGI: ".$this->role()." (pid $$): ".$msg; } =head2 exit diff --git a/README b/README index 7ca6c0c..fbe7f8c 100644 --- a/README +++ b/README @@ -1,11 +1,13 @@ -$Id: README,v 1.3 2000/11/15 20:14:56 muaddib Exp $ +$Id: README,v 1.4 2001/01/31 07:13:44 muaddib Exp $ General Information ------------------- -FCGI-ProcManager is a process manager for FCGI. +FCGI-ProcManager is a process manager for FCGI. By implementing the +process manager in perl, we can more finely tune FastCGI performance, and +we can take CPU and memory advantages of fast forks and copy-on-right UNIX +process management characteristics. -more info later. Installation ------------ diff --git a/t/exporter.t b/t/exporter.t index 553cd31..6580997 100644 --- a/t/exporter.t +++ b/t/exporter.t @@ -5,17 +5,15 @@ # General Public License, Version 2.1, a copy of which can be # found in the "COPYING" file of this distribution. -# $Id: exporter.t,v 1.1 2001/01/13 06:44:33 muaddib Exp $ +# $Id: exporter.t,v 1.3 2001/01/31 06:57:09 muaddib Exp $ use strict; use Test; -BEGIN { plan tests => 5; } +BEGIN { plan tests => 4; } use FCGI::ProcManager qw(:all); -ok pm_state() eq "idle"; - ok pm_parameter('n_processes',100) == 100; ok pm_parameter('n_processes',2) == 2; ok pm_parameter('n_processes',0) == 0; @@ -27,7 +25,7 @@ ok pm_manage(); #ok $@ =~ /dying from number of processes exception: -3/; #undef $@; -pm_parameter('n_processes',20); +pm_parameter('n_processes',10); #pm_manage(); #sample_request_loop(); @@ -39,15 +37,15 @@ sub sample_request_loop { while (1) { # Simulate blocking for a request. my $t1 = int(rand(2)+1); - print "$$ waiting for $t1..\n"; + print "TEST: simulating blocking for request: $t1 seconds.\n"; sleep $t1; # (Here is where accept-fail-on-intr would exit request loop.) pm_pre_dispatch(); # Simulate a request dispatch. - my $t = int(rand(3)+1); - print "$$ sleeping $t..\n"; + my $t = int(rand(3)+2); + print "TEST: simulating request: sleeping $t seconds.\n"; while (my $nslept = sleep $t) { $t -= $nslept; last unless $t; diff --git a/t/procmanager.t b/t/procmanager.t index 28131c3..e4b0076 100644 --- a/t/procmanager.t +++ b/t/procmanager.t @@ -5,19 +5,18 @@ # General Public License, Version 2.1, a copy of which can be # found in the "COPYING" file of this distribution. -# $Id: procmanager.t,v 1.4 2001/01/13 06:44:35 muaddib Exp $ +# $Id: procmanager.t,v 1.6 2001/01/31 06:57:28 muaddib Exp $ use strict; use Test; -BEGIN { plan tests => 6; } +BEGIN { plan tests => 5; } use FCGI::ProcManager; my $m; ok $m = FCGI::ProcManager->new(); -ok $m->pm_state() eq "idle"; ok $m->n_processes(100) == 100; ok $m->n_processes(2) == 2; @@ -30,7 +29,7 @@ ok $m->pm_manage(); #ok $@ =~ /dying from number of processes exception: -3/; #undef $@; -$m->n_processes(20); +$m->n_processes(10); #$m->pm_manage(); #sample_request_loop($m); @@ -42,16 +41,16 @@ sub sample_request_loop { while (1) { # Simulate blocking for a request. - my $t1 = int(rand(2)+1); - print "$$ waiting for $t1..\n"; + my $t1 = int(rand(2)+2); + print "TEST: simulating blocking for request: $t1 seconds.\n"; sleep $t1; # (Here is where accept-fail-on-intr would exit request loop.) $m->pm_pre_dispatch(); # Simulate a request dispatch. - my $t = int(rand(3)+1); - print "$$ sleeping $t..\n"; + my $t = int(rand(3)+2); + print "TEST: simulating new request: $t seconds.\n"; while (my $nslept = sleep $t) { $t -= $nslept; last unless $t;