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);
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_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 $nextt; # timer for next_tick
-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 ($later_timer, $reap_timer);
our (
%DescriptorMap, # fd (num) -> PublicInbox::DS object
$Epoll, # Global epoll fd (or DSKQXS ref)
$LoopTimeout, # timeout of event loop in milliseconds
$DoneInit, # if we've done the one-time module init yet
@Timers, # timers
+ $in_loop,
);
Reset();
=cut
sub Reset {
%DescriptorMap = ();
+ $nextq = [];
+ $WaitPids = [];
+ $later_queue = [];
+ $reap_timer = $later_timer = undef;
@ToClose = ();
$LoopTimeout = -1; # no timeout by default
@Timers = ();
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";
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();
$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
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();
# in that event.
$DescriptorMap{$events[$i]->[0]}->event_step;
}
- return unless PostEventLoop();
- }
+ } while (PostEventLoop());
+ _run_later();
}
=head2 C<< CLASS->SetPostLoopCallback( CODEREF ) >>
# 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
### 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.
$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) = @_;
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;
sub do_read ($$$;$) {
my ($self, $rbuf, $len, $off) = @_;
- my $r = sysread($self->{sock}, $$rbuf, $len, $off // 0);
+ 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;
- rbuf_idle($self, $rbuf);
- watch($self, $ev | EPOLLONESHOT);
- } elsif ($! == EAGAIN) {
+ if ($! == EAGAIN) {
+ epwait($sock, epbit($sock, EPOLLIN) | EPOLLONESHOT);
rbuf_idle($self, $rbuf);
- watch($self, EPOLLIN | EPOLLONESHOT);
+ 0;
} else {
$self->close;
}
# 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): $!");
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;
# wbuf may be an empty array if we're being called inside
# ->flush_write via CODE bref:
push @{$self->{wbuf} ||= []}, $tmpio;
- watch($self, EPOLLOUT|EPOLLONESHOT);
return 0;
}
}
# 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
}
}
-sub next_tick () {
- $nextt = undef;
- my $q = $nextq;
- $nextq = [];
- $_->event_step for @$q;
+# 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 requeue ($) {
- push @$nextq, $_[0];
- $nextt ||= PublicInbox::EvCleanup::asap(*next_tick);
+sub later ($) {
+ my ($cb) = @_;
+ push @$later_queue, $cb;
+ $later_timer //= AddTimer(undef, 60, \&_run_later);
}
package PublicInbox::DS::Timer;