X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FDS.pm;h=7eb0aadad2a3db1ee3e4f4d58a1b395536c1f91f;hb=0541761b0558317be1825710c1545f6e718103f2;hp=4947192f8e4d604f1c1563595890326b8b31b174;hpb=01d8132315e855bf35fa655f19741c2206f97ddc;p=public-inbox.git diff --git a/lib/PublicInbox/DS.pm b/lib/PublicInbox/DS.pm index 4947192f..7eb0aada 100644 --- a/lib/PublicInbox/DS.pm +++ b/lib/PublicInbox/DS.pm @@ -16,7 +16,7 @@ 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,18 +24,25 @@ 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 'wbuf', # arrayref of coderefs or GLOB refs 'wbuf_off', # offset into first element of wbuf to start writing at ); -use Errno qw(EAGAIN EINVAL EEXIST); +use Errno qw(EAGAIN EINVAL); use Carp qw(croak confess carp); require File::Spec; +my $nextq; # queue for next_tick +my $WaitPids; # list of [ pid, callback, callback_arg ] +my $later_queue; # callbacks +my ($later_timer, $reap_timer); our ( %DescriptorMap, # fd (num) -> PublicInbox::DS object $Epoll, # Global epoll fd (or DSKQXS ref) @@ -47,6 +54,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(); @@ -62,6 +70,10 @@ Reset all state =cut sub Reset { %DescriptorMap = (); + $nextq = []; + $WaitPids = []; + $later_queue = []; + $reap_timer = $later_timer = undef; @ToClose = (); $LoopTimeout = -1; # no timeout by default @Timers = (); @@ -98,12 +110,6 @@ Returns a timer object which you can call C<< $timer->cancel >> on if you need t sub AddTimer { my ($class, $secs, $coderef) = @_; - if (!$secs) { - my $timer = bless([0, $coderef], 'PublicInbox::DS::Timer'); - unshift(@Timers, $timer); - return $timer; - } - my $fire_time = now() + $secs; my $timer = bless [$fire_time, $coderef], "PublicInbox::DS::Timer"; @@ -151,7 +157,7 @@ sub _InitPoller $cls = "PublicInbox::$_"; last if eval "require $cls"; } - $cls->import; + $cls->import(qw(epoll_ctl epoll_wait)); $Epoll = $cls->new; } *EventLoop = *EpollEventLoop; @@ -173,9 +179,25 @@ sub FirstTimeEventLoop { sub now () { clock_gettime(CLOCK_MONOTONIC) } +sub next_tick () { + my $q = $nextq; + $nextq = []; + for (@$q) { + # 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 { + $_->(); + } + } +} + # runs timers and returns milliseconds for next one, or next event loop sub RunTimers { - return $LoopTimeout unless @Timers; + next_tick(); + + return ((@$nextq || @ToClose) ? 0 : $LoopTimeout) unless @Timers; my $now = now(); @@ -185,6 +207,9 @@ sub RunTimers { $to_run->[1]->($now) if $to_run->[1]; } + # timers may enqueue into nextq: + return 0 if (@$nextq || @ToClose); + return $LoopTimeout unless @Timers; # convert time to an even number of milliseconds, adding 1 @@ -202,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 = AddTimer(undef, 1, \&reap_pids); + } +} + +# reentrant SIGCHLD handler (since reap_pids is not reentrant) +sub enqueue_reap ($) { push @$nextq, \&reap_pids }; + +sub running () { ($SIG{CHLD} // '') eq \&enqueue_reap } + sub EpollEventLoop { - while (1) { + local $in_loop = 1; + do { my @events; my $i; my $timeout = RunTimers(); @@ -217,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 ) >> @@ -246,17 +299,8 @@ sub PostEventLoop { # now we can close sockets that wanted to close during our event processing. # (we didn't want to close them during the loop, as we didn't want fd numbers # being reused and confused during the event loop) - while (my $sock = shift @ToClose) { - my $fd = fileno($sock); - - # close the socket. (not a PublicInbox::DS close) - CORE::close($sock); - - # and now we can finally remove the fd from the map. see - # comment above in ->close. - delete $DescriptorMap{$fd}; - } - + delete($DescriptorMap{fileno($_)}) for @ToClose; + @ToClose = (); # let refcounting drop everything all at once # by default we keep running, unless a postloop callback (either per-object # or global) cancels it @@ -317,6 +361,8 @@ sub new { ### I N S T A N C E M E T H O D S ##################################################################### +sub requeue ($) { push @$nextq, $_[0] } + =head2 C<< $obj->close >> Close the socket. @@ -373,6 +419,10 @@ sub psendfile ($$$) { $written; } +sub epbit ($$) { # (sock, default) + ref($_[0]) eq 'IO::Socket::SSL' ? PublicInbox::TLS::epollbit() : $_[1]; +} + # returns 1 if done, 0 if incomplete sub flush_write ($) { my ($self) = @_; @@ -391,8 +441,8 @@ next_buf: goto next_buf; } } elsif ($! == EAGAIN) { + epwait($sock, epbit($sock, EPOLLOUT) | EPOLLONESHOT); $self->{wbuf_off} = $off; - watch($self, EPOLLOUT|EPOLLONESHOT); return 0; } else { return $self->close; @@ -412,17 +462,25 @@ next_buf: 1; # all done } -sub do_read ($$$$) { +sub rbuf_idle ($$) { + my ($self, $rbuf) = @_; + if ($$rbuf eq '') { # who knows how long till we can read again + delete $self->{rbuf}; + } else { + $self->{rbuf} = $rbuf; + } +} + +sub do_read ($$$;$) { my ($self, $rbuf, $len, $off) = @_; - my $r = sysread($self->{sock}, $$rbuf, $len, $off); + my $r = sysread(my $sock = $self->{sock}, $$rbuf, $len, $off // 0); return ($r == 0 ? $self->close : $r) if defined $r; # common for clients to break connections without warning, # would be too noisy to log here: - if (ref($self) eq 'IO::Socket::SSL') { - my $ev = PublicInbox::TLS::epollbit() or return $self->close; - watch($self, $ev | EPOLLONESHOT); - } elsif ($! == EAGAIN) { - watch($self, EPOLLIN | EPOLLONESHOT); + if ($! == EAGAIN) { + epwait($sock, epbit($sock, EPOLLIN) | EPOLLONESHOT); + rbuf_idle($self, $rbuf); + 0; } else { $self->close; } @@ -440,15 +498,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): $!"); @@ -499,14 +550,20 @@ sub write { if (defined $written) { return 1 if $written == $to_write; + requeue($self); # runs: event_step -> flush_write } elsif ($! == EAGAIN) { + epwait($sock, epbit($sock, EPOLLOUT) | EPOLLONESHOT); $written = 0; } else { return $self->close; } + + # deal with EAGAIN or partial write: my $tmpio = tmpio($self, $bref, $written) or return 0; - $self->{wbuf} = [ $tmpio ]; - watch($self, EPOLLOUT|EPOLLONESHOT); + + # wbuf may be an empty array if we're being called inside + # ->flush_write via CODE bref: + push @{$self->{wbuf} ||= []}, $tmpio; return 0; } } @@ -525,46 +582,41 @@ sub msg_more ($$) { # queue up the unwritten substring: my $tmpio = tmpio($self, \($_[1]), $n) or return 0; $self->{wbuf} = [ $tmpio ]; - watch($self, EPOLLOUT|EPOLLONESHOT); + epwait($sock, EPOLLOUT|EPOLLONESHOT); return 0; } } - $self->write(\($_[1])); + + # don't redispatch into NNTPdeflate::write + PublicInbox::DS::write($self, \($_[1])); } -sub watch ($$) { - my ($self, $ev) = @_; - my $sock = $self->{sock} or return; +sub epwait ($$) { + my ($sock, $ev) = @_; epoll_ctl($Epoll, EPOLL_CTL_MOD, fileno($sock), $ev) and confess("EPOLL_CTL_MOD $!"); - 0; } -sub watch_in1 ($) { watch($_[0], EPOLLIN | EPOLLONESHOT) } - # return true if complete, false if incomplete (or failure) sub accept_tls_step ($) { my ($self) = @_; my $sock = $self->{sock} or return; return 1 if $sock->accept_SSL; return $self->close if $! != EAGAIN; - if (my $ev = PublicInbox::TLS::epollbit()) { - unshift @{$self->{wbuf} ||= []}, \&accept_tls_step; - return watch($self, $ev | EPOLLONESHOT); - } - drop($self, 'BUG? EAGAIN but '.PublicInbox::TLS::err()); + epwait($sock, PublicInbox::TLS::epollbit() | EPOLLONESHOT); + unshift @{$self->{wbuf} ||= []}, \&accept_tls_step; + 0; } +# return true if complete, false if incomplete (or failure) sub shutdn_tls_step ($) { my ($self) = @_; my $sock = $self->{sock} or return; return $self->close if $sock->stop_SSL(SSL_fast_shutdown => 1); return $self->close if $! != EAGAIN; - if (my $ev = PublicInbox::TLS::epollbit()) { - unshift @{$self->{wbuf} ||= []}, \&shutdn_tls_step; - return watch($self, $ev | EPOLLONESHOT); - } - drop($self, 'BUG? EAGAIN but '.PublicInbox::TLS::err()); + epwait($sock, PublicInbox::TLS::epollbit() | EPOLLONESHOT); + unshift @{$self->{wbuf} ||= []}, \&shutdn_tls_step; + 0; } # don't bother with shutdown($sock, 2), we don't fork+exec w/o CLOEXEC @@ -579,6 +631,32 @@ sub shutdn ($) { } } +# 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 //= AddTimer(undef, 60, \&_run_later); +} + package PublicInbox::DS::Timer; # [$abs_float_firetime, $coderef]; sub cancel {