+++ /dev/null
-Index: MANIFEST
-===================================================================
---- MANIFEST (revision 103454)
-+++ MANIFEST (working copy)
-@@ -52,6 +52,8 @@
- lib/Mail/SpamAssassin/Plugin/URIDNSBL.pm
- lib/Mail/SpamAssassin/PluginHandler.pm
- lib/Mail/SpamAssassin/Reporter.pm
-+lib/Mail/SpamAssassin/SpamdForkScaling.pm
-+lib/Mail/SpamAssassin/SubProcBackChannel.pm
- lib/Mail/SpamAssassin/SQLBasedAddrList.pm
- lib/Mail/SpamAssassin/TextCat.pm
- lib/Mail/SpamAssassin/Util.pm
-Index: lib/Mail/SpamAssassin/SubProcBackChannel.pm
-===================================================================
---- lib/Mail/SpamAssassin/SubProcBackChannel.pm (revision 0)
-+++ lib/Mail/SpamAssassin/SubProcBackChannel.pm (revision 0)
-@@ -0,0 +1,155 @@
-+# back-channel for communication between a master and multiple slave processes.
-+#
-+# <@LICENSE>
-+# Copyright 2004 Apache Software Foundation
-+#
-+# Licensed under the Apache License, Version 2.0 (the "License");
-+# you may not use this file except in compliance with the License.
-+# You may obtain a copy of the License at
-+#
-+# http://www.apache.org/licenses/LICENSE-2.0
-+#
-+# Unless required by applicable law or agreed to in writing, software
-+# distributed under the License is distributed on an "AS IS" BASIS,
-+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-+# See the License for the specific language governing permissions and
-+# limitations under the License.
-+# </@LICENSE>
-+
-+package Mail::SpamAssassin::SubProcBackChannel;
-+
-+use strict;
-+use warnings;
-+use bytes;
-+
-+use IO::Socket;
-+use Mail::SpamAssassin::Util;
-+use Mail::SpamAssassin::Constants qw(:sa);
-+
-+use vars qw {
-+};
-+
-+my @ISA = qw();
-+
-+=head1 NAME
-+
-+Mail::SpamAssassin::SubProcBackChannel - back-channel for communication between a master and multiple slave processes
-+
-+=head1 METHODS
-+
-+=over 4
-+
-+=cut
-+
-+
-+###########################################################################
-+
-+sub new {
-+ my $class = shift;
-+ $class = ref($class) || $class;
-+
-+ my $self = shift;
-+ if (!defined $self) { $self = { }; }
-+ bless ($self, $class);
-+
-+ $self->{kids} = { };
-+ $self->{fileno_to_fh} = { };
-+
-+ $self;
-+}
-+
-+###########################################################################
-+
-+sub set_selector {
-+ my ($self, $sel) = @_;
-+ $self->{selector} = $sel;
-+}
-+
-+sub setup_backchannel_parent_pre_fork {
-+ my ($self) = @_;
-+
-+ my $io = IO::Socket->new();
-+ ($self->{latest_kid_fh}, $self->{parent}) =
-+ $io->socketpair(AF_UNIX,SOCK_STREAM,PF_UNSPEC)
-+ or die "backchannel: socketpair failed: $!";
-+}
-+
-+sub setup_backchannel_parent_post_fork {
-+ my ($self, $pid) = @_;
-+
-+ my $fh = $self->{latest_kid_fh};
-+
-+ close $self->{parent}; # because it's us!
-+
-+ # disable caching for parent<->child relations
-+ my ($old) = select($fh);
-+ $|++;
-+ select($old);
-+
-+ $self->{kids}->{$pid} = $fh;
-+ $self->add_to_selector($fh);
-+}
-+
-+sub add_to_selector {
-+ my ($self, $fh) = @_;
-+ my $fno = fileno($fh);
-+ $self->{fileno_to_fh}->{$fno} = $fh;
-+ vec (${$self->{selector}}, $fno, 1) = 1;
-+}
-+
-+sub select_vec_to_fh_list {
-+ my ($self, $vec) = @_;
-+ my $i = -1;
-+
-+ # grotesque hackery alert! ;) turn the vec() map of fds into a list of
-+ # filehandles. note that filenos that don't have a filehandle in the
-+ # {fileno_to_fh} hash will be ignored; this is by design, so that other fhs
-+ # can be selected on using the same vec, and the caller can just check for
-+ # those in their own code, before they fall back to using this method.
-+
-+ return grep {
-+ defined
-+ } map {
-+ $i++;
-+ ($_ ? $self->{fileno_to_fh}->{$i} : undef);
-+ } split (//, unpack ("b*", $vec));
-+}
-+
-+sub get_socket_for_child {
-+ my ($self, $pid) = @_;
-+ return $self->{kids}->{$pid};
-+}
-+
-+###########################################################################
-+
-+sub setup_backchannel_child_post_fork {
-+ my ($self) = @_;
-+
-+ close $self->{latest_kid_fh}; # because it's us!
-+
-+ my $old = select($self->{parent});
-+ $| = 1; # print to parent by default, turn off buffering
-+ select($old);
-+}
-+
-+sub get_parent_socket {
-+ my ($self) = @_;
-+ return $self->{parent};
-+}
-+
-+############################################################################
-+
-+1;
-+
-+__END__
-+
-+=back
-+
-+=head1 SEE ALSO
-+
-+C<Mail::SpamAssassin>
-+C<Mail::SpamAssassin::ArchiveIterator>
-+C<Mail::SpamAssassin::SpamdPreforkScaling>
-+C<spamassassin>
-+C<spamd>
-+C<mass-check>
-Index: lib/Mail/SpamAssassin/SpamdForkScaling.pm
-===================================================================
---- lib/Mail/SpamAssassin/SpamdForkScaling.pm (revision 0)
-+++ lib/Mail/SpamAssassin/SpamdForkScaling.pm (revision 0)
-@@ -0,0 +1,383 @@
-+# spamd prefork scaling, using an Apache-based algorithm
-+#
-+# <@LICENSE>
-+# Copyright 2004 Apache Software Foundation
-+#
-+# Licensed under the Apache License, Version 2.0 (the "License");
-+# you may not use this file except in compliance with the License.
-+# You may obtain a copy of the License at
-+#
-+# http://www.apache.org/licenses/LICENSE-2.0
-+#
-+# Unless required by applicable law or agreed to in writing, software
-+# distributed under the License is distributed on an "AS IS" BASIS,
-+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-+# See the License for the specific language governing permissions and
-+# limitations under the License.
-+# </@LICENSE>
-+
-+package Mail::SpamAssassin::SpamdForkScaling;
-+
-+*dbg=\&Mail::SpamAssassin::dbg;
-+
-+use strict;
-+use warnings;
-+use bytes;
-+
-+use Mail::SpamAssassin::Util;
-+
-+use vars qw {
-+ @PFSTATE_VARS %EXPORT_TAGS @EXPORT_OK
-+};
-+
-+use base qw( Exporter );
-+
-+@PFSTATE_VARS = qw(
-+ PFSTATE_ERROR PFSTATE_STARTING PFSTATE_IDLE PFSTATE_BUSY PFSTATE_KILLED
-+ PFORDER_ACCEPT
-+);
-+
-+%EXPORT_TAGS = (
-+ 'pfstates' => [ @PFSTATE_VARS ]
-+);
-+@EXPORT_OK = ( @PFSTATE_VARS );
-+
-+use constant PFSTATE_ERROR => -1;
-+use constant PFSTATE_STARTING => 0;
-+use constant PFSTATE_IDLE => 1;
-+use constant PFSTATE_BUSY => 2;
-+use constant PFSTATE_KILLED => 3;
-+
-+use constant PFORDER_ACCEPT => 10;
-+
-+###########################################################################
-+
-+sub new {
-+ my $class = shift;
-+ $class = ref($class) || $class;
-+
-+ my $self = shift;
-+ if (!defined $self) { $self = { }; }
-+ bless ($self, $class);
-+
-+ $self->{kids} = { };
-+ $self->{overloaded} = 0;
-+ $self->{min_children} ||= 1;
-+
-+ $self;
-+}
-+
-+###########################################################################
-+# Parent methods
-+
-+sub add_child {
-+ my ($self, $pid) = @_;
-+ $self->set_child_state ($pid, PFSTATE_STARTING);
-+}
-+
-+sub child_exited {
-+ my ($self, $pid) = @_;
-+ delete $self->{kids}->{$pid};
-+}
-+
-+sub set_child_state {
-+ my ($self, $pid, $state) = @_;
-+ $self->{kids}->{$pid} = $state;
-+ dbg ("prefork: child $pid: entering state $state");
-+ $self->compute_lowest_child_pid();
-+}
-+
-+sub compute_lowest_child_pid {
-+ my ($self) = @_;
-+
-+ my @pids = grep { $self->{kids}->{$_} == PFSTATE_IDLE }
-+ keys %{$self->{kids}};
-+
-+ my $l = shift @pids;
-+ foreach my $p (@pids) {
-+ if ($l > $p) { $l = $p };
-+ }
-+ $self->{lowest_idle_pid} = $l;
-+}
-+
-+###########################################################################
-+
-+sub set_server_fh {
-+ my ($self, $fh) = @_;
-+ $self->{server_fh} = $fh;
-+ $self->{server_fileno} = $fh->fileno();
-+}
-+
-+sub main_server_poll {
-+ my ($self, $tout) = @_;
-+
-+ my $rin = ${$self->{backchannel}->{selector}};
-+ if ($self->{overloaded}) {
-+ # don't select on the server fh -- we already KNOW that's ready,
-+ # since we're overloaded
-+ vec($rin, $self->{server_fileno}, 1) = 0;
-+ }
-+
-+ my ($rout, $eout);
-+ my ($nfound, $timeleft) = select($rout=$rin, undef, $eout=$rin, $tout);
-+
-+ # any action?
-+ return unless ($nfound);
-+
-+ # were the kids ready, or did we get signal?
-+ if (vec ($rout, $self->{server_fileno}, 1)) {
-+ # dbg("prefork: server fh ready");
-+ # the server socket: new connection from a client
-+ if (!$self->order_idle_child_to_accept()) {
-+ # dbg("prefork: no idle kids, noting overloaded");
-+ # there are no idle kids! we're overloaded, mark that
-+ $self->{overloaded}++;
-+ }
-+ return;
-+ }
-+
-+ foreach my $fh ($self->{backchannel}->select_vec_to_fh_list($rout))
-+ {
-+ # otherwise it's a status report from a child.
-+ # just read one line. if there's more lines, we'll get them
-+ # when we re-enter the can_read() select call above...
-+ if ($self->read_one_line_from_child_socket($fh) == PFSTATE_IDLE)
-+ {
-+ dbg("prefork: child reports idle");
-+ if ($self->{overloaded}) {
-+ # if we were overloaded, then now that this kid is idle,
-+ # we can use it to handle the waiting connection. zero
-+ # the overloaded flag, anyway; if there's >1 waiting
-+ # conn, they'll show up next time we do the select.
-+
-+ dbg("prefork: overloaded, immediately telling kid to accept");
-+ if (!$self->order_idle_child_to_accept()) {
-+ # this should not happen
-+ warn "prefork: oops! still overloaded?";
-+ }
-+ dbg("prefork: no longer overloaded");
-+ $self->{overloaded} = 0;
-+ }
-+ }
-+ }
-+
-+ # now that we've ordered some kids to accept any new connections,
-+ # increase/decrease the pool as necessary
-+ $self->adapt_num_children();
-+}
-+
-+sub read_one_line_from_child_socket {
-+ my ($self, $sock) = @_;
-+
-+ my $line = $sock->getline();
-+ if (!defined $line) {
-+ dbg ("prefork: child closed connection");
-+
-+ # stop it being select'd
-+ vec(${$self->{backchannel}->{selector}}, $sock->fileno, 1) = 0;
-+ $sock->close();
-+ return PFSTATE_ERROR;
-+ }
-+
-+ chomp $line;
-+ if ($line =~ /^I(\d+)/) {
-+ $self->set_child_state ($1, PFSTATE_IDLE);
-+ return PFSTATE_IDLE;
-+ }
-+ elsif ($line =~ /^B(\d+)/) {
-+ $self->set_child_state ($1, PFSTATE_BUSY);
-+ return PFSTATE_BUSY;
-+ }
-+ else {
-+ die "unknown message from child: '$line'";
-+ return PFSTATE_ERROR;
-+ }
-+}
-+
-+###########################################################################
-+
-+# we use the following protocol between the master and child processes to
-+# control when they accept/who accepts: server tells a child to accept with a
-+# "A\n", child responds with "B$pid\n" when it's busy, and "I$pid\n" once it's
-+# idle again. Very simple, line-based protocol.
-+
-+sub order_idle_child_to_accept {
-+ my ($self) = @_;
-+
-+ my $kid = $self->{lowest_idle_pid};
-+ if (defined $kid) {
-+ my $sock = $self->{backchannel}->get_socket_for_child($kid);
-+ $sock->syswrite ("A\n");
-+ dbg ("prefork: ordered $kid to accept");
-+
-+ # now wait for it to say it's done that
-+ return $self->wait_for_child_to_accept($sock);
-+
-+ }
-+ else {
-+ dbg ("prefork: no spare children to accept, waiting for one to complete");
-+ return undef;
-+ }
-+}
-+
-+sub wait_for_child_to_accept {
-+ my ($self, $sock) = @_;
-+
-+ while (1) {
-+ my $state = $self->read_one_line_from_child_socket($sock);
-+ if ($state == PFSTATE_BUSY) {
-+ return 1; # 1 == success
-+ }
-+ if ($state == PFSTATE_ERROR) {
-+ return undef;
-+ }
-+ else {
-+ die "prefork: ordered child to accept, but child reported state '$state'";
-+ }
-+ }
-+}
-+
-+sub child_now_ready_to_accept {
-+ my ($self, $kid) = @_;
-+ if ($self->{waiting_for_idle_child}) {
-+ my $sock = $self->{backchannel}->get_socket_for_child($kid);
-+ $sock->syswrite ("A\n");
-+ $self->{waiting_for_idle_child} = 0;
-+ }
-+}
-+
-+###########################################################################
-+# Child methods
-+
-+sub set_my_pid {
-+ my ($self, $pid) = @_;
-+ $self->{pid} = $pid; # save calling $$ all the time
-+}
-+
-+sub update_child_status_idle {
-+ my ($self) = @_;
-+ $self->report_backchannel_socket("I".$self->{pid}."\n");
-+}
-+
-+sub update_child_status_busy {
-+ my ($self) = @_;
-+ $self->report_backchannel_socket("B".$self->{pid}."\n");
-+}
-+
-+sub report_backchannel_socket {
-+ my ($self, $str) = @_;
-+ my $sock = $self->{backchannel}->get_parent_socket();
-+ syswrite ($sock, $str)
-+ or write "syswrite() to parent failed: $!";
-+}
-+
-+sub wait_for_orders {
-+ my ($self) = @_;
-+
-+ my $sock = $self->{backchannel}->get_parent_socket();
-+ while (1) {
-+ my $line = $sock->getline();
-+ chomp $line if defined $line;
-+ if (index ($line, "A") == 0) { # string starts with "A" = accept
-+ return PFORDER_ACCEPT;
-+ }
-+ else {
-+ die "unknown order from parent: '$line'";
-+ return undef;
-+ }
-+ }
-+}
-+
-+###########################################################################
-+# Master server code again
-+
-+# this is pretty much the algorithm from perform_idle_server_maintainance() in
-+# Apache's "prefork" MPM. However: we don't do exponential server spawning,
-+# since our servers are a lot more heavyweight than theirs is.
-+
-+sub adapt_num_children {
-+ my ($self) = @_;
-+
-+ my $kids = $self->{kids};
-+ my $statestr = '';
-+ my $num_idle = 0;
-+ my @pids = sort { $a <=> $b } keys %{$kids};
-+ my $num_servers = scalar @pids;
-+
-+ foreach my $pid (@pids) {
-+ my $k = $kids->{$pid};
-+ if ($k == PFSTATE_IDLE) {
-+ $statestr .= 'I';
-+ $num_idle++;
-+ }
-+ elsif ($k == PFSTATE_BUSY) {
-+ $statestr .= 'B';
-+ }
-+ elsif ($k == PFSTATE_KILLED) {
-+ $statestr .= 'K';
-+ }
-+ elsif ($k == PFSTATE_ERROR) {
-+ $statestr .= 'E';
-+ }
-+ elsif ($k == PFSTATE_STARTING) {
-+ $statestr .= 'S';
-+ }
-+ else {
-+ $statestr .= '?';
-+ }
-+ }
-+ dbg ("prefork: child states: ".$statestr);
-+
-+ # just kill off/add one at a time, to avoid swamping stuff and
-+ # reacting too quickly; Apache emulation
-+ if ($num_idle < $self->{min_idle}) {
-+ if ($num_servers < $self->{max_children}) {
-+ $self->need_to_add_server($num_idle);
-+ } else {
-+ warn "prefork: server reached --max-clients setting, consider raising it\n";
-+ }
-+ }
-+ elsif ($num_idle > $self->{max_idle} && $num_servers > $self->{min_children}) {
-+ $self->need_to_del_server($num_idle);
-+ }
-+}
-+
-+sub need_to_add_server {
-+ my ($self, $num_idle) = @_;
-+ my $cur = ${$self->{cur_children_ref}};
-+ $cur++;
-+ dbg ("prefork: adjust: increasing, not enough idle children ($num_idle < $self->{min_idle})");
-+ main::spawn();
-+ # servers will be started once main_server_poll() returns
-+}
-+
-+sub need_to_del_server {
-+ my ($self, $num_idle) = @_;
-+ my $cur = ${$self->{cur_children_ref}};
-+ $cur--;
-+ my $pid;
-+ foreach my $k (keys %{$self->{kids}}) {
-+ my $v = $self->{kids}->{$k};
-+ if ($v == PFSTATE_IDLE)
-+ {
-+ # kill the highest; Apache emulation, exploits linux scheduler
-+ # behaviour (and is predictable)
-+ if (!defined $pid || $k > $pid) {
-+ $pid = $k;
-+ }
-+ }
-+ }
-+
-+ if (!defined $pid) {
-+ # this should be impossible. assert it
-+ die "oops! no idle kids in need_to_del_server?";
-+ }
-+
-+ kill 'INT' => $pid;
-+ $self->set_child_state ($pid, PFSTATE_KILLED);
-+ dbg ("prefork: adjust: decreasing, too many idle children ($num_idle > $self->{max_idle}), killed $pid");
-+}
-+
-+1;
-+
-+__END__
-Index: spamd/spamd.raw
-===================================================================
---- spamd/spamd.raw (revision 103454)
-+++ spamd/spamd.raw (working copy)
-@@ -39,6 +39,8 @@
-
- use Mail::SpamAssassin;
- use Mail::SpamAssassin::NetSet;
-+use Mail::SpamAssassin::SubProcBackChannel;
-+use Mail::SpamAssassin::SpamdForkScaling qw(:pfstates);
-
- use Getopt::Long;
- use Pod::Usage;
-@@ -82,7 +84,9 @@
- EX_CONFIG => 78, # configuration error
- );
-
-+*dbg = \&Mail::SpamAssassin::dbg;
-
-+
- sub print_version {
- printf("%s version %s\n", "SpamAssassin Server", Mail::SpamAssassin::Version());
- printf(" running on Perl %s\n", join(".", map { $_*1 } ($] =~ /(\d)\.(\d{3})(\d{3})/)));
-@@ -109,8 +113,14 @@
- my %opt = (
- 'user-config' => 1,
- 'ident-timeout' => 5.0,
-+ # scaling settings; some of these aren't actually settable via cmdline
-+ 'server-scale-period' => 2, # how often to scale the # of kids, secs
-+ 'min-children' => 1, # min kids to have running
-+ 'min-spare' => 1, # min kids that must be spare
-+ 'max-spare' => 2, # max kids that should be spare
- );
-
-+
- # Untaint all command-line options and ENV vars, since spamd is launched
- # as a daemon from a known-safe environment. Also store away some of the
- # vars we need for a SIGHUP later on.
-@@ -153,7 +163,11 @@
- 'listen-ip|ip-address|i:s' => \$opt{'listen-ip'},
- 'local!' => \$opt{'local'},
- 'L' => \$opt{'local'},
-+ 'round-robin!' => \$opt{'round-robin'},
-+ 'min-children=i' => \$opt{'min-children'},
- 'max-children|m=i' => \$opt{'max-children'},
-+ 'min-spare=i' => \$opt{'min-spare'},
-+ 'max-spare=i' => \$opt{'max-spare'},
- 'max-conn-per-child=i' => \$opt{'max-conn-per-child'},
- 'nouser-config|x' => sub { $opt{'user-config'} = 0 },
- 'paranoid!' => \$opt{'paranoid'},
-@@ -515,7 +529,23 @@
- $childlimit ||= 5;
- $clients_per_child ||= 200;
-
-+# ensure scaling parameters are logical
-+if ($opt{'min-children'} < 1) {
-+ $opt{'min-children'} = 1;
-+}
-+if ($opt{'min-spare'} < 0) {
-+ $opt{'min-spare'} = 0;
-+}
-+if ($opt{'min-spare'} > $childlimit) {
-+ $opt{'min-spare'} = $childlimit-1;
-+}
-+if ($opt{'max-spare'} < $opt{'min-spare'}) {
-+ # emulate Apache behaviour:
-+ # http://httpd.apache.org/docs-2.0/mod/prefork.html#maxspareservers
-+ $opt{'max-spare'} = $opt{'min-spare'}+1;
-+}
-
-+
- my $dontcopy = 1;
- if ( $opt{'create-prefs'} ) { $dontcopy = 0; }
-
-@@ -571,6 +601,29 @@
- $listeninfo = "port $port/tcp";
- }
-
-+my $backchannel = Mail::SpamAssassin::SubProcBackChannel->new();
-+my $scaling;
-+if (!$opt{'round-robin'})
-+{
-+ my $max_children = $childlimit;
-+
-+ # change $childlimit to avoid churn when we startup and create loads
-+ # of spare servers; when we're using scaling, it's not as important
-+ # as it was with the old algorithm.
-+ if ($childlimit > $opt{'max-spare'}) {
-+ $childlimit = $opt{'max-spare'};
-+ }
-+
-+ $scaling = Mail::SpamAssassin::SpamdForkScaling->new({
-+ backchannel => $backchannel,
-+ min_children => $opt{'min-children'},
-+ max_children => $max_children,
-+ min_idle => $opt{'min-spare'},
-+ max_idle => $opt{'max-spare'},
-+ cur_children_ref => \$childlimit
-+ });
-+}
-+
- # Be a well-behaved daemon
- my $server;
- if ( $opt{'socketpath'} ) {
-@@ -747,6 +800,10 @@
- my $got_sighup;
- setup_parent_sig_handlers();
-
-+my $select_mask = '';
-+vec($select_mask, $server->fileno, 1) = 1;
-+$backchannel->set_selector(\$select_mask);
-+
- # log server started, but processes watching the log to wait for connect
- # should wait until they see the pid, after signal handlers are in place
- if ( defined $opt{'debug'} ) {
-@@ -781,8 +838,16 @@
- warn "server pid: $$\n";
- }
-
-+if ($scaling) {
-+ $scaling->set_server_fh($server);
-+}
-+
- while (1) {
-- sleep; # wait for a signal (ie: child's death)
-+ if (!$scaling) {
-+ sleep; # wait for a signal (ie: child's death)
-+ } else {
-+ $scaling->main_server_poll($opt{'server-scale-period'});
-+ }
-
- if ( defined $got_sighup ) {
- if (defined($opt{'pidfile'})) {
-@@ -803,7 +868,7 @@
- . ": $!\n";
- }
-
-- for ( my $i = keys %children ; $i < $childlimit ; $i++ ) {
-+ for (my $i = keys %children; $i < $childlimit; $i++) {
- spawn();
- }
- }
-@@ -812,6 +877,8 @@
- sub spawn {
- my $pid;
-
-+ $backchannel->setup_backchannel_parent_pre_fork();
-+
- # block signal for fork
- my $sigset = POSIX::SigSet->new( POSIX::SIGINT() );
- sigprocmask( POSIX::SIG_BLOCK(), $sigset )
-@@ -826,6 +893,10 @@
- or die "Can't unblock SIGINT for fork: $!\n";
- $children{$pid} = 1;
- logmsg("server successfully spawned child process, pid $pid");
-+ $backchannel->setup_backchannel_parent_post_fork($pid);
-+ if ($scaling) {
-+ $scaling->add_child($pid);
-+ }
- return;
- }
- else {
-@@ -864,9 +935,23 @@
- # this will help make it clear via process listing which is child/parent
- $0 = 'spamd child';
-
-+ $backchannel->setup_backchannel_child_post_fork();
-+ if ($scaling) { # only do this once, for efficiency; $$ is a syscall
-+ $scaling->set_my_pid($$);
-+ }
-+
- # handle $clients_per_child connections, then die in "old" age...
-+ my $orders;
- for ( my $i = 0 ; $i < $clients_per_child ; $i++ ) {
-+ if ($scaling) {
-+ $scaling->update_child_status_idle();
-+ $orders = $scaling->wait_for_orders(); # and sleep...
-
-+ if ($orders != PFORDER_ACCEPT) {
-+ logmsg ("unknown order: $orders");
-+ }
-+ }
-+
- # use a large eval scope to catch die()s and ensure they
- # don't kill the server.
- my $evalret = eval { accept_a_conn(); };
-@@ -934,6 +1019,10 @@
-
- $client->autoflush(1);
-
-+ if ($scaling) {
-+ $scaling->update_child_status_busy();
-+ }
-+
- # keep track of start time
- my $start = time;
-
-@@ -1503,7 +1592,7 @@
-
- sub handle_user_ldap {
- my $username = shift;
-- Mail::SpamAssassin::dbg("handle_user_ldap($username)");
-+ dbg("handle_user_ldap($username)");
- $spamtest->load_scoreonly_ldap($username);
- $spamtest->signal_user_changed(
- {
-@@ -1626,6 +1715,7 @@
- $SIG{CHLD} = \&child_handler;
- $SIG{INT} = \&kill_handler;
- $SIG{TERM} = \&kill_handler;
-+ $SIG{PIPE} = 'IGNORE';
- }
-
- # sig handlers: child processes
-@@ -1775,6 +1865,10 @@
- # remove them from our child listing
- delete $children{$pid};
-
-+ if ($scaling) {
-+ $scaling->child_exited($pid);
-+ }
-+
- unless ($main::INHIBIT_LOGGING_IN_SIGCHLD_HANDLER) {
- logmsg("handled cleanup of child pid $pid");
- }
-@@ -1919,8 +2013,12 @@
- -i [ipaddr], --listen-ip=ipaddr Listen on the IP ipaddr
- -p port, --port Listen on specified port
- -m num, --max-children=num Allow maximum num children
-+ --min-children=num Allow minimum num children
-+ --min-spare=num Lower limit for number of spare children
-+ --max-spare=num Upper limit for number of spare children
- --max-conn-per-child=num Maximum connections accepted by child
- before it is respawned
-+ --round-robin Use traditional prefork algorithm
- -q, --sql-config Enable SQL config (only useful with -x)
- -Q, --setuid-with-sql Enable SQL config (only useful with -x,
- enables use of -H)
-@@ -2236,12 +2334,46 @@
- Please note that there is a OS specific maximum of connections that can be
- queued (Try C<perl -MSocket -e'print SOMAXCONN'> to find this maximum).
-
-+Note that if you run too many servers for the amount of free RAM available, you
-+run the danger of hurting performance by causing a high swap load as server
-+processes are swapped in and out continually.
-+
-+=item B<--min-children>=I<number>
-+
-+The minimum number of children that will be kept running. The minimum value is
-+C<1>, the default value is C<1>. If you have lots of free RAM, you may want to
-+increase this.
-+
-+=item B<--min-spare>=I<number>
-+
-+The lower limit for the number of spare children allowed to run. A
-+spare, or idle, child is one that is not handling a scan request. If
-+there are too few spare children available, a new server will be started
-+every second or so. The default value is C<1>.
-+
-+=item B<--max-spare>=I<number>
-+
-+The upper limit for the number of spare children allowed to run. If there
-+are too many spare children, one will be killed every second or so until
-+the number of idle children is in the desired range. The default value
-+is C<2>.
-+
- =item B<--max-conn-per-child>=I<number>
-
- This option specifies the maximum number of connections each child
- should process before dying and letting the master spamd process spawn
- a new child. The minimum value is C<1>, the default value is C<200>.
-
-+=item B<--round-robin>
-+
-+By default, C<spamd> will attempt to keep a small number of "hot" child
-+processes as busy as possible, and keep any others as idle as possible, using
-+something similar to the Apache httpd server scaling algorithm. This is
-+accomplished by the master process coordinating the activities of the children.
-+This switch will disable this scaling algorithm, and the behaviour seen in
-+versions 3.0.0 and 3.0.1 will be used instead, where all processes receive an
-+equal load and no scaling takes place.
-+
- =item B<-H> I<directory>, B<--helper-home-dir>=I<directory>
-
- Specify that external programs such as Razor, DCC, and Pyzor should have