X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FDS.pm;h=cea25d90e37516c354e94410148f8299fa64cb15;hb=eb9c415ba4421cecb5157967c843dc7f8720e916;hp=586c47cdc8cef94e05509dfbf0a82136a147f870;hpb=ecea327e3d4386a22652fc08f71ac7d65b8f9b70;p=public-inbox.git diff --git a/lib/PublicInbox/DS.pm b/lib/PublicInbox/DS.pm index 586c47cd..cea25d90 100644 --- a/lib/PublicInbox/DS.pm +++ b/lib/PublicInbox/DS.pm @@ -3,20 +3,20 @@ # # This license differs from the rest of public-inbox # -# This is a fork of the (for now) unmaintained Danga::Socket 1.61. -# Unused features will be removed, and updates will be made to take -# advantage of newer kernels. +# This is a fork of the unmaintained Danga::Socket (1.61) with +# significant changes. See Documentation/technical/ds.txt in our +# source for details. # -# API changes to diverge from Danga::Socket will happen to better -# accomodate new features and improve scalability. Do not expect -# this to be a stable API like Danga::Socket. -# Bugs encountered (and likely fixed) are reported to -# bug-Danga-Socket@rt.cpan.org and visible at: +# Do not expect this to be a stable API like Danga::Socket, +# but it will evolve to suite our needs and to take advantage of +# newer Linux and *BSD features. +# Bugs encountered were reported to bug-Danga-Socket@rt.cpan.org, +# fixed in Danga::Socket 1.62 and visible at: # https://rt.cpan.org/Public/Dist/Display.html?Name=Danga-Socket package PublicInbox::DS; use strict; use bytes; -use POSIX (); +use POSIX qw(WNOHANG); use IO::Handle qw(); use Fcntl qw(SEEK_SET :DEFAULT); use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC); @@ -24,8 +24,10 @@ use parent qw(Exporter); our @EXPORT_OK = qw(now msg_more); use warnings; use 5.010_001; +use Scalar::Util qw(blessed); use PublicInbox::Syscall qw(:epoll); +use PublicInbox::Tmpfile; use fields ('sock', # underlying socket 'rbuf', # scalarref, usually undef @@ -33,11 +35,15 @@ use fields ('sock', # underlying socket 'wbuf_off', # offset into first element of wbuf to start writing at ); -use Errno qw(EAGAIN EINVAL EEXIST); -use Carp qw(croak confess carp); -require File::Spec; +use Errno qw(EAGAIN EINVAL); +use Carp qw(confess carp); -my $nextq = []; # queue for next_tick +my $nextq; # queue for next_tick +my $WaitPids; # list of [ pid, callback, callback_arg ] +my $later_queue; # callbacks +my $EXPMAP; # fd -> [ idle_time, $self ] +our $EXPTIME = 180; # 3 minutes +my ($later_timer, $reap_timer, $exp_timer); our ( %DescriptorMap, # fd (num) -> PublicInbox::DS object $Epoll, # Global epoll fd (or DSKQXS ref) @@ -49,6 +55,7 @@ our ( $LoopTimeout, # timeout of event loop in milliseconds $DoneInit, # if we've done the one-time module init yet @Timers, # timers + $in_loop, ); Reset(); @@ -64,6 +71,11 @@ Reset all state =cut sub Reset { %DescriptorMap = (); + $nextq = []; + $WaitPids = []; + $later_queue = []; + $EXPMAP = {}; + $reap_timer = $later_timer = $exp_timer = undef; @ToClose = (); $LoopTimeout = -1; # no timeout by default @Timers = (); @@ -89,20 +101,18 @@ sub SetLoopTimeout { return $LoopTimeout = $_[1] + 0; } -=head2 C<< CLASS->AddTimer( $seconds, $coderef ) >> +=head2 C<< PublicInbox::DS::add_timer( $seconds, $coderef ) >> Add a timer to occur $seconds from now. $seconds may be fractional, but timers are not guaranteed to fire at the exact time you ask for. -Returns a timer object which you can call C<< $timer->cancel >> on if you need to. - =cut -sub AddTimer { - my ($class, $secs, $coderef) = @_; +sub add_timer ($$) { + my ($secs, $coderef) = @_; my $fire_time = now() + $secs; - my $timer = bless [$fire_time, $coderef], "PublicInbox::DS::Timer"; + my $timer = [$fire_time, $coderef]; if (!@Timers || $fire_time >= $Timers[-1][0]) { push @Timers, $timer; @@ -173,10 +183,12 @@ sub next_tick () { my $q = $nextq; $nextq = []; for (@$q) { - if (ref($_) eq 'CODE') { - $_->(); - } else { + # we avoid "ref" on blessed refs to workaround a Perl 5.16.3 leak: + # https://rt.perl.org/Public/Bug/Display.html?id=114340 + if (blessed($_)) { $_->event_step; + } else { + $_->(); } } } @@ -215,8 +227,36 @@ sub RunTimers { return $timeout; } +# We can't use waitpid(-1) safely here since it can hit ``, system(), +# and other things. So we scan the $WaitPids list, which is hopefully +# not too big. +sub reap_pids { + my $tmp = $WaitPids; + $WaitPids = []; + $reap_timer = undef; + foreach my $ary (@$tmp) { + my ($pid, $cb, $arg) = @$ary; + my $ret = waitpid($pid, WNOHANG); + if ($ret == 0) { + push @$WaitPids, $ary; + } elsif ($cb) { + eval { $cb->($arg, $pid) }; + } + } + if (@$WaitPids) { + # we may not be donea, and we may miss our + $reap_timer = add_timer(1, \&reap_pids); + } +} + +# reentrant SIGCHLD handler (since reap_pids is not reentrant) +sub enqueue_reap ($) { push @$nextq, \&reap_pids }; + +sub in_loop () { $in_loop } + sub EpollEventLoop { - while (1) { + local $in_loop = 1; + do { my @events; my $i; my $timeout = RunTimers(); @@ -230,8 +270,8 @@ sub EpollEventLoop { # in that event. $DescriptorMap{$events[$i]->[0]}->event_step; } - return unless PostEventLoop(); - } + } while (PostEventLoop()); + _run_later(); } =head2 C<< CLASS->SetPostLoopCallback( CODEREF ) >> @@ -297,9 +337,6 @@ sub new { $self->{sock} = $sock; my $fd = fileno($sock); - Carp::cluck("undef sock and/or fd in PublicInbox::DS->new. sock=" . ($sock || "") . ", fd=" . ($fd || "")) - unless $sock && $fd; - _InitPoller(); if (epoll_ctl($Epoll, EPOLL_CTL_ADD, $fd, $ev)) { @@ -309,11 +346,10 @@ sub new { } die "couldn't add epoll watch for $fd: $!\n"; } - Carp::cluck("PublicInbox::DS::new blowing away existing descriptor map for fd=$fd ($DescriptorMap{$fd})") - if $DescriptorMap{$fd}; + confess("DescriptorMap{$fd} defined ($DescriptorMap{$fd})") + if defined($DescriptorMap{$fd}); $DescriptorMap{$fd} = $self; - return $self; } @@ -458,15 +494,8 @@ sub drop { # PerlIO::mmap or PerlIO::scalar if needed sub tmpio ($$$) { my ($self, $bref, $off) = @_; - my $fh; # open(my $fh, '+>>', undef) doesn't set O_APPEND - do { - my $fn = File::Spec->tmpdir . '/wbuf-' . rand; - if (sysopen($fh, $fn, O_RDWR|O_CREAT|O_EXCL|O_APPEND, 0600)) { # likely - unlink($fn) or return drop($self, "unlink($fn) $!"); - } elsif ($! != EEXIST) { # EMFILE/ENFILE/ENOSPC/ENOMEM - return drop($self, "open: $!"); - } - } until (defined $fh); + my $fh = tmpfile('wbuf', $self->{sock}, 1) or + return drop($self, "tmpfile $!"); $fh->autoflush(1); my $len = bytes::length($$bref) - $off; $fh->write($$bref, $len, $off) or return drop($self, "write ($len): $!"); @@ -540,20 +569,25 @@ use constant MSG_MORE => ($^O eq 'linux') ? 0x8000 : 0; sub msg_more ($$) { my $self = $_[0]; my $sock = $self->{sock} or return 1; + my $wbuf = $self->{wbuf}; - if (MSG_MORE && !$self->{wbuf} && ref($sock) ne 'IO::Socket::SSL') { + if (MSG_MORE && (!defined($wbuf) || !scalar(@$wbuf)) && + ref($sock) ne 'IO::Socket::SSL') { my $n = send($sock, $_[1], MSG_MORE); if (defined $n) { my $nlen = bytes::length($_[1]) - $n; return 1 if $nlen == 0; # all done! # queue up the unwritten substring: my $tmpio = tmpio($self, \($_[1]), $n) or return 0; - $self->{wbuf} = [ $tmpio ]; + $self->{wbuf} //= $wbuf //= []; + push @$wbuf, $tmpio; epwait($sock, EPOLLOUT|EPOLLONESHOT); return 0; } } - $self->write(\($_[1])); + + # don't redispatch into NNTPdeflate::write + PublicInbox::DS::write($self, \($_[1])); } sub epwait ($$) { @@ -595,10 +629,65 @@ sub shutdn ($) { $self->close; } } -package PublicInbox::DS::Timer; -# [$abs_float_firetime, $coderef]; -sub cancel { - $_[0][1] = undef; + +# must be called with eval, PublicInbox::DS may not be loaded (see t/qspawn.t) +sub dwaitpid ($$$) { + my ($pid, $cb, $arg) = @_; + if ($in_loop) { + push @$WaitPids, [ $pid, $cb, $arg ]; + + # We could've just missed our SIGCHLD, cover it, here: + requeue(\&reap_pids); + } else { + die "Not in EventLoop\n"; + } +} + +sub _run_later () { + my $run = $later_queue; + $later_timer = undef; + $later_queue = []; + $_->() for @$run; +} + +sub later ($) { + my ($cb) = @_; + push @$later_queue, $cb; + $later_timer //= add_timer(60, \&_run_later); +} + +sub expire_old () { + my $now = now(); + my $exp = $EXPTIME; + my $old = $now - $exp; + my %new; + while (my ($fd, $v) = each %$EXPMAP) { + my ($idle_time, $ds_obj) = @$v; + if ($idle_time < $old) { + if (!$ds_obj->shutdn) { + $new{$fd} = $v; + } + } else { + $new{$fd} = $v; + } + } + $EXPMAP = \%new; + $exp_timer = scalar(keys %new) ? later(\&expire_old) : undef; +} + +sub update_idle_time { + my ($self) = @_; + my $sock = $self->{sock} or return; + $EXPMAP->{fileno($sock)} = [ now(), $self ]; + $exp_timer //= later(\&expire_old); +} + +sub not_idle_long { + my ($self, $now) = @_; + my $sock = $self->{sock} or return; + my $ary = $EXPMAP->{fileno($sock)} or return; + my $exp_at = $ary->[0] + $EXPTIME; + $exp_at > $now; } 1;