X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FDS.pm;h=7f7cb85d8583cd69c509f7a7ca2280dcd01e156e;hb=3c39f9c942a6975245fda878e9b957d8d3367662;hp=8fc49eee67e9c0b564f1d0800284a8eec05f52d0;hpb=b4aae3e011e24a9aacbd6d84c0e0aa610144bb76;p=public-inbox.git diff --git a/lib/PublicInbox/DS.pm b/lib/PublicInbox/DS.pm index 8fc49eee..7f7cb85d 100644 --- a/lib/PublicInbox/DS.pm +++ b/lib/PublicInbox/DS.pm @@ -16,39 +16,35 @@ package PublicInbox::DS; use strict; use bytes; -use POSIX (); +use POSIX qw(WNOHANG); use IO::Handle qw(); -use Fcntl qw(FD_CLOEXEC F_SETFD F_GETFD); +use Fcntl qw(SEEK_SET :DEFAULT); use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC); use parent qw(Exporter); -our @EXPORT_OK = qw(now); +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 - 'wbuf', # arrayref of scalars, scalarrefs, or coderefs to write + 'rbuf', # scalarref, usually undef + 'wbuf', # arrayref of coderefs or GLOB refs 'wbuf_off', # offset into first element of wbuf to start writing at - 'event_watch', # bitmask of events the client is interested in (POLLIN,OUT,etc.) ); use Errno qw(EAGAIN EINVAL); -use Carp qw(croak confess); - -use constant POLLIN => 1; -use constant POLLOUT => 4; -use constant POLLERR => 8; -use constant POLLHUP => 16; -use constant POLLNVAL => 32; - -our $HAVE_KQUEUE = eval { require IO::KQueue; 1 }; +use Carp qw(croak confess carp); +require File::Spec; +my $nextq = []; # queue for next_tick +my $WaitPids = []; # list of [ pid, callback, callback_arg ] +my $reap_timer; our ( - $HaveEpoll, # Flag -- is epoll available? initially undefined. - $HaveKQueue, %DescriptorMap, # fd (num) -> PublicInbox::DS object - $Epoll, # Global epoll fd (for epoll mode only) - $KQueue, # Global kqueue fd ref (for kqueue mode only) + $Epoll, # Global epoll fd (or DSKQXS ref) $_io, # IO::Handle for Epoll @ToClose, # sockets to close when event loop is done @@ -59,8 +55,6 @@ our ( @Timers, # timers ); -# this may be set to zero with old kernels -our $EPOLLEXCLUSIVE = EPOLLEXCLUSIVE; Reset(); ##################################################################### @@ -74,6 +68,8 @@ Reset all state =cut sub Reset { %DescriptorMap = (); + $WaitPids = []; + $reap_timer = undef; @ToClose = (); $LoopTimeout = -1; # no timeout by default @Timers = (); @@ -81,13 +77,8 @@ sub Reset { $PostLoopCallback = undef; $DoneInit = 0; - # NOTE kqueue is close-on-fork, and we don't account for it, yet - # OTOH, we (public-inbox) don't need this sub outside of tests... - POSIX::close($$KQueue) if !$_io && $KQueue && $$KQueue >= 0; - $KQueue = undef; - - $_io = undef; # close $Epoll - $Epoll = undef; + $_io = undef; # closes real $Epoll FD + $Epoll = undef; # may call DSKQXS::DESTROY *EventLoop = *FirstTimeEventLoop; } @@ -115,12 +106,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"; @@ -159,26 +144,19 @@ sub _InitPoller return if $DoneInit; $DoneInit = 1; - if ($HAVE_KQUEUE) { - $KQueue = IO::KQueue->new(); - $HaveKQueue = defined $KQueue; - if ($HaveKQueue) { - *EventLoop = *KQueueEventLoop; - } - } - elsif (PublicInbox::Syscall::epoll_defined()) { - $Epoll = eval { epoll_create(1024); }; - $HaveEpoll = defined $Epoll && $Epoll >= 0; - if ($HaveEpoll) { - set_cloexec($Epoll); - *EventLoop = *EpollEventLoop; + if (PublicInbox::Syscall::epoll_defined()) { + $Epoll = epoll_create(); + set_cloexec($Epoll) if (defined($Epoll) && $Epoll >= 0); + } else { + my $cls; + for (qw(DSKQXS DSPoll)) { + $cls = "PublicInbox::$_"; + last if eval "require $cls"; } + $cls->import(qw(epoll_ctl epoll_wait)); + $Epoll = $cls->new; } - - if (!$HaveEpoll && !$HaveKQueue) { - require IO::Poll; - *EventLoop = *PollEventLoop; - } + *EventLoop = *EpollEventLoop; } =head2 C<< CLASS->EventLoop() >> @@ -192,20 +170,30 @@ sub FirstTimeEventLoop { _InitPoller(); - if ($HaveEpoll) { - EpollEventLoop($class); - } elsif ($HaveKQueue) { - KQueueEventLoop($class); - } else { - PollEventLoop($class); - } + EventLoop($class); } 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(); @@ -215,6 +203,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 @@ -232,11 +223,33 @@ sub RunTimers { return $timeout; } -### The epoll-based event loop. Gets installed as EventLoop if IO::Epoll loads -### okay. -sub EpollEventLoop { - my $class = shift; +# 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 EpollEventLoop { + local $SIG{CHLD} = \&enqueue_reap; while (1) { my @events; my $i; @@ -253,78 +266,6 @@ sub EpollEventLoop { } return unless PostEventLoop(); } - exit 0; -} - -### The fallback IO::Poll-based event loop. Gets installed as EventLoop if -### IO::Epoll fails to load. -sub PollEventLoop { - my $class = shift; - - my PublicInbox::DS $pob; - - while (1) { - my $timeout = RunTimers(); - - # the following sets up @poll as a series of ($poll,$event_mask) - # items, then uses IO::Poll::_poll, implemented in XS, which - # modifies the array in place with the even elements being - # replaced with the event masks that occured. - my @poll; - while ( my ($fd, $sock) = each %DescriptorMap ) { - push @poll, $fd, $sock->{event_watch}; - } - - # if nothing to poll, either end immediately (if no timeout) - # or just keep calling the callback - unless (@poll) { - select undef, undef, undef, ($timeout / 1000); - return unless PostEventLoop(); - next; - } - - my $count = IO::Poll::_poll($timeout, @poll); - unless ($count >= 0) { - return unless PostEventLoop(); - next; - } - - # Fetch handles with read events - while (@poll) { - my ($fd, $state) = splice(@poll, 0, 2); - $DescriptorMap{$fd}->event_step if $state; - } - - return unless PostEventLoop(); - } - - exit 0; -} - -### The kqueue-based event loop. Gets installed as EventLoop if IO::KQueue works -### okay. -sub KQueueEventLoop { - my $class = shift; - - while (1) { - my $timeout = RunTimers(); - my @ret = eval { $KQueue->kevent($timeout) }; - if (my $err = $@) { - # workaround https://rt.cpan.org/Ticket/Display.html?id=116615 - if ($err =~ /Interrupted system call/) { - @ret = (); - } else { - die $err; - } - } - - foreach my $kev (@ret) { - $DescriptorMap{$kev->[0]}->event_step; - } - return unless PostEventLoop(); - } - - exit(0); } =head2 C<< CLASS->SetPostLoopCallback( CODEREF ) >> @@ -352,17 +293,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) - $sock->close; - - # 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 @@ -393,7 +325,7 @@ This is normally (always?) called from your subclass via: =cut sub new { - my ($self, $sock, $exclusive) = @_; + my ($self, $sock, $ev) = @_; $self = fields::new($self) unless ref $self; $self->{sock} = $sock; @@ -402,32 +334,15 @@ sub new { Carp::cluck("undef sock and/or fd in PublicInbox::DS->new. sock=" . ($sock || "") . ", fd=" . ($fd || "")) unless $sock && $fd; - my $ev = $self->{event_watch} = POLLERR|POLLHUP|POLLNVAL; - _InitPoller(); - if ($HaveEpoll) { - if ($exclusive) { - $ev = $self->{event_watch} = EPOLLIN|EPOLLERR|EPOLLHUP|$EPOLLEXCLUSIVE; - } -retry: - if (epoll_ctl($Epoll, EPOLL_CTL_ADD, $fd, $ev)) { - if ($! == EINVAL && ($ev & $EPOLLEXCLUSIVE)) { - $EPOLLEXCLUSIVE = 0; # old kernel - $ev = $self->{event_watch} = EPOLLIN|EPOLLERR|EPOLLHUP; - goto retry; - } - die "couldn't add epoll watch for $fd: $!\n"; + if (epoll_ctl($Epoll, EPOLL_CTL_ADD, $fd, $ev)) { + if ($! == EINVAL && ($ev & EPOLLEXCLUSIVE)) { + $ev &= ~EPOLLEXCLUSIVE; + goto retry; } + die "couldn't add epoll watch for $fd: $!\n"; } - elsif ($HaveKQueue) { - # Add them to the queue but disabled for now - $KQueue->EV_SET($fd, IO::KQueue::EVFILT_READ(), - IO::KQueue::EV_ADD() | IO::KQueue::EV_DISABLE()); - $KQueue->EV_SET($fd, IO::KQueue::EVFILT_WRITE(), - IO::KQueue::EV_ADD() | IO::KQueue::EV_DISABLE()); - } - Carp::cluck("PublicInbox::DS::new blowing away existing descriptor map for fd=$fd ($DescriptorMap{$fd})") if $DescriptorMap{$fd}; @@ -440,6 +355,8 @@ retry: ### 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. @@ -456,11 +373,9 @@ sub close { # if we're using epoll, we have to remove this from our epoll fd so we stop getting # notifications about it - if ($HaveEpoll) { - my $fd = fileno($sock); - epoll_ctl($Epoll, EPOLL_CTL_DEL, $fd, 0) and - confess("EPOLL_CTL_DEL: $!"); - } + my $fd = fileno($sock); + epoll_ctl($Epoll, EPOLL_CTL_DEL, $fd, 0) and + confess("EPOLL_CTL_DEL: $!"); # we explicitly don't delete from DescriptorMap here until we # actually close the socket, as we might be in the middle of @@ -478,54 +393,123 @@ sub close { return 0; } +# portable, non-thread-safe sendfile emulation (no pread, yet) +sub psendfile ($$$) { + my ($sock, $fh, $off) = @_; + + seek($fh, $$off, SEEK_SET) or return; + defined(my $to_write = read($fh, my $buf, 16384)) or return; + my $written = 0; + while ($to_write > 0) { + if (defined(my $w = syswrite($sock, $buf, $to_write, $written))) { + $written += $w; + $to_write -= $w; + } else { + return if $written == 0; + last; + } + } + $$off += $written; + $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) = @_; my $wbuf = $self->{wbuf} or return 1; - my $sock = $self->{sock} or return 1; + my $sock = $self->{sock}; +next_buf: while (my $bref = $wbuf->[0]) { - my $ref = ref($bref); - if ($ref eq 'SCALAR') { - my $len = bytes::length($$bref); - my $off = $self->{wbuf_off} || 0; - my $to_write = $len - $off; - my $written = syswrite($sock, $$bref, $to_write, $off); - if (defined $written) { - if ($written == $to_write) { - shift @$wbuf; + if (ref($bref) ne 'CODE') { + my $off = delete($self->{wbuf_off}) // 0; + while ($sock) { + my $w = psendfile($sock, $bref, \$off); + if (defined $w) { + if ($w == 0) { + shift @$wbuf; + goto next_buf; + } + } elsif ($! == EAGAIN) { + epwait($sock, epbit($sock, EPOLLOUT) | EPOLLONESHOT); + $self->{wbuf_off} = $off; + return 0; } else { - $self->{wbuf_off} = $off + $written; + return $self->close; } - next; # keep going until EAGAIN - } elsif ($! == EAGAIN) { - $self->watch_write(1); - } else { - $self->close; } - return 0; } else { #($ref eq 'CODE') { shift @$wbuf; - $bref->(); + my $before = scalar(@$wbuf); + $bref->($self); + + # bref may be enqueueing more CODE to call (see accept_tls_step) + return 0 if (scalar(@$wbuf) > $before); } } # while @$wbuf delete $self->{wbuf}; - $self->watch_write(0); 1; # all done } +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(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 ($! == EAGAIN) { + epwait($sock, epbit($sock, EPOLLIN) | EPOLLONESHOT); + rbuf_idle($self, $rbuf); + 0; + } else { + $self->close; + } +} + +# drop the socket if we hit unrecoverable errors on our system which +# require BOFH attention: ENOSPC, EFBIG, EIO, EMFILE, ENFILE... +sub drop { + my $self = shift; + carp(@_); + $self->close; +} + +# n.b.: use ->write/->read for this buffer to allow compatibility with +# PerlIO::mmap or PerlIO::scalar if needed +sub tmpio ($$$) { + my ($self, $bref, $off) = @_; + 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): $!"); + $fh +} + =head2 C<< $obj->write( $data ) >> Write the specified data to the underlying handle. I may be scalar, -scalar ref, code ref (to run when there), or undef just to kick-start. +scalar ref, code ref (to run when there). Returns 1 if writes all went through, or 0 if there are writes in queue. If it returns 1, caller should stop waiting for 'writable' events) =cut sub write { my ($self, $data) = @_; - return flush_write($self) unless defined $data; # nobody should be writing to closed sockets, but caller code can # do two writes within an event, have the first fail and @@ -537,11 +521,22 @@ sub write { my $sock = $self->{sock} or return 1; my $ref = ref $data; my $bref = $ref ? $data : \$data; - if (my $wbuf = $self->{wbuf}) { # already buffering, can't write more... - push @$wbuf, $bref; + my $wbuf = $self->{wbuf}; + if ($wbuf && scalar(@$wbuf)) { # already buffering, can't write more... + if ($ref eq 'CODE') { + push @$wbuf, $bref; + } else { + my $last = $wbuf->[-1]; + if (ref($last) eq 'GLOB') { # append to tmp file buffer + $last->print($$bref) or return drop($self, "print: $!"); + } else { + my $tmpio = tmpio($self, $bref, 0) or return 0; + push @$wbuf, $tmpio; + } + } return 0; } elsif ($ref eq 'CODE') { - $bref->(); + $bref->($self); return 1; } else { my $to_write = bytes::length($$bref); @@ -549,76 +544,98 @@ sub write { if (defined $written) { return 1 if $written == $to_write; - $self->{wbuf_off} = $written; - $self->{wbuf} = [ $bref ]; - return flush_write($self); # try until EAGAIN + requeue($self); # runs: event_step -> flush_write } elsif ($! == EAGAIN) { - $self->{wbuf} = [ $bref ]; - $self->watch_write(1); + epwait($sock, epbit($sock, EPOLLOUT) | EPOLLONESHOT); + $written = 0; } else { - $self->close; + return $self->close; } + + # deal with EAGAIN or partial write: + my $tmpio = tmpio($self, $bref, $written) or return 0; + + # wbuf may be an empty array if we're being called inside + # ->flush_write via CODE bref: + push @{$self->{wbuf} ||= []}, $tmpio; return 0; } } -=head2 C<< $obj->watch_read( $boolean ) >> - -Turn 'readable' event notification on or off. - -=cut -sub watch_read { - my PublicInbox::DS $self = shift; - my $sock = $self->{sock} or return; +use constant MSG_MORE => ($^O eq 'linux') ? 0x8000 : 0; - my $val = shift; - my $event = $self->{event_watch}; - - $event &= ~POLLIN if ! $val; - $event |= POLLIN if $val; +sub msg_more ($$) { + my $self = $_[0]; + my $sock = $self->{sock} or return 1; - my $fd = fileno($sock); - # If it changed, set it - if ($event != $self->{event_watch}) { - if ($HaveKQueue) { - $KQueue->EV_SET($fd, IO::KQueue::EVFILT_READ(), - $val ? IO::KQueue::EV_ENABLE() : IO::KQueue::EV_DISABLE()); - } - elsif ($HaveEpoll) { - epoll_ctl($Epoll, EPOLL_CTL_MOD, $fd, $event) and - confess("EPOLL_CTL_MOD: $!"); + if (MSG_MORE && !$self->{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 ]; + epwait($sock, EPOLLOUT|EPOLLONESHOT); + return 0; } - $self->{event_watch} = $event; } + + # don't redispatch into NNTPdeflate::write + PublicInbox::DS::write($self, \($_[1])); } -=head2 C<< $obj->watch_write( $boolean ) >> +sub epwait ($$) { + my ($sock, $ev) = @_; + epoll_ctl($Epoll, EPOLL_CTL_MOD, fileno($sock), $ev) and + confess("EPOLL_CTL_MOD $!"); +} -Turn 'writable' event notification on or off. +# 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; + epwait($sock, PublicInbox::TLS::epollbit() | EPOLLONESHOT); + unshift @{$self->{wbuf} ||= []}, \&accept_tls_step; + 0; +} -=cut -sub watch_write { - my PublicInbox::DS $self = shift; +# 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; + epwait($sock, PublicInbox::TLS::epollbit() | EPOLLONESHOT); + unshift @{$self->{wbuf} ||= []}, \&shutdn_tls_step; + 0; +} - my $val = shift; - my $event = $self->{event_watch}; +# don't bother with shutdown($sock, 2), we don't fork+exec w/o CLOEXEC +# or fork w/o exec, so no inadvertant socket sharing +sub shutdn ($) { + my ($self) = @_; + my $sock = $self->{sock} or return; + if (ref($sock) eq 'IO::Socket::SSL') { + shutdn_tls_step($self); + } else { + $self->close; + } +} - $event &= ~POLLOUT if ! $val; - $event |= POLLOUT if $val; - my $fd = fileno($sock); +# must be called with eval, PublicInbox::DS may not be loaded (see t/qspawn.t) +sub dwaitpid ($$$) { + my ($pid, $cb, $arg) = @_; + my $chld = $SIG{CHLD}; + if (defined($chld) && $chld eq \&enqueue_reap) { + push @$WaitPids, [ $pid, $cb, $arg ]; - # If it changed, set it - if ($event != $self->{event_watch}) { - if ($HaveKQueue) { - $KQueue->EV_SET($fd, IO::KQueue::EVFILT_WRITE(), - $val ? IO::KQueue::EV_ENABLE() : IO::KQueue::EV_DISABLE()); - } - elsif ($HaveEpoll) { - epoll_ctl($Epoll, EPOLL_CTL_MOD, $fd, $event) and - confess "EPOLL_CTL_MOD: $!"; - } - $self->{event_watch} = $event; + # We could've just missed our SIGCHLD, cover it, here: + requeue(\&reap_pids); + } else { + die "Not in EventLoop\n"; } }