X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FDS.pm;h=970061fd55f1c44c77d51084d7d9891aeb3f48f3;hb=8775167475d8bfc25d532b777147a8b1ef1cd99b;hp=9c801214b150290a907733d3b9bf3e255c4a7300;hpb=dbcdabe601cfb29c8b7d5f169be9bf560d656a42;p=public-inbox.git diff --git a/lib/PublicInbox/DS.pm b/lib/PublicInbox/DS.pm index 9c801214..970061fd 100644 --- a/lib/PublicInbox/DS.pm +++ b/lib/PublicInbox/DS.pm @@ -3,56 +3,59 @@ # # 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(FD_CLOEXEC F_SETFD F_GETFD SEEK_SET); +use Fcntl qw(SEEK_SET :DEFAULT); use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC); use parent qw(Exporter); -our @EXPORT_OK = qw(now msg_more write_in_full); +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 coderefs or GLOB refs + 'rbuf', # scalarref, usually undef + 'wbuf', # arrayref of coderefs or GLOB refs (autovivified) 'wbuf_off', # offset into first element of wbuf to start writing at - 'event_watch', # bitmask of events the client is interested in - # (EPOLLIN,OUT,etc.) ); -use Errno qw(EAGAIN EINVAL); -use Carp qw(croak confess); -use File::Temp qw(tempfile); - -our $HAVE_KQUEUE = eval { require IO::KQueue; IO::KQueue->import; 1 }; +use Errno qw(EAGAIN EINVAL); +use Carp qw(confess carp); +my $nextq; # queue for next_tick +my $wait_pids; # list of [ pid, callback, callback_arg ] +my $later_queue; # list of callbacks to run at some later interval +my $EXPMAP; # fd -> idle_time +our $EXPTIME = 180; # 3 minutes +my ($later_timer, $reap_timer, $exp_timer); +my $ToClose; # sockets to close when event loop is done 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 $PostLoopCallback, # subref to call at the end of each loop, if defined (global) $LoopTimeout, # timeout of event loop in milliseconds $DoneInit, # if we've done the one-time module init yet @Timers, # timers + $in_loop, ); Reset(); @@ -68,20 +71,17 @@ Reset all state =cut sub Reset { %DescriptorMap = (); - @ToClose = (); + $wait_pids = $later_queue = undef; + $EXPMAP = {}; + $nextq = $ToClose = $reap_timer = $later_timer = $exp_timer = undef; $LoopTimeout = -1; # no timeout by default @Timers = (); $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; } @@ -98,26 +98,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) = @_; - - if (!$secs) { - my $timer = bless([0, $coderef], 'PublicInbox::DS::Timer'); - unshift(@Timers, $timer); - return $timer; - } +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; @@ -153,21 +145,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; } + *EventLoop = *EpollEventLoop; } =head2 C<< CLASS->EventLoop() >> @@ -181,18 +171,30 @@ sub FirstTimeEventLoop { _InitPoller(); - if ($HaveEpoll) { - EpollEventLoop($class); - } elsif ($HaveKQueue) { - KQueueEventLoop($class); - } + EventLoop($class); } sub now () { clock_gettime(CLOCK_MONOTONIC) } +sub next_tick () { + my $q = $nextq or return; + $nextq = undef; + 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(); @@ -202,6 +204,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 @@ -219,12 +224,57 @@ 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 $wait_pids list, which is hopefully +# not too big. We keep $wait_pids small by not calling dwaitpid() +# until we've hit EOF when reading the stdout of the child. +sub reap_pids { + my $tmp = $wait_pids or return; + $wait_pids = $reap_timer = undef; + foreach my $ary (@$tmp) { + my ($pid, $cb, $arg) = @$ary; + my $ret = waitpid($pid, WNOHANG); + if ($ret == 0) { + push @$wait_pids, $ary; # autovivifies @$wait_pids + } elsif ($cb) { + eval { $cb->($arg, $pid) }; + } + } + # we may not be done, yet, and could've missed/masked a SIGCHLD: + $reap_timer = add_timer(1, \&reap_pids) if $wait_pids; +} + +# reentrant SIGCHLD handler (since reap_pids is not reentrant) +sub enqueue_reap ($) { push @$nextq, \&reap_pids }; # autovivifies + +sub in_loop () { $in_loop } - while (1) { +# Internal function: run the post-event callback, send read events +# for pushed-back data, and close pending connections. returns 1 +# if event loop should continue, or 0 to shut it all down. +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) + if (my $close_now = $ToClose) { + $ToClose = undef; # will be autovivified on push + @$close_now = map { fileno($_) } @$close_now; + + # order matters, destroy expiry times, first: + delete @$EXPMAP{@$close_now}; + + # ->DESTROY methods may populate ToClose + delete @DescriptorMap{@$close_now}; + } + + # by default we keep running, unless a postloop callback cancels it + $PostLoopCallback ? $PostLoopCallback->(\%DescriptorMap) : 1; +} + +sub EpollEventLoop { + local $in_loop = 1; + do { my @events; my $i; my $timeout = RunTimers(); @@ -238,35 +288,8 @@ sub EpollEventLoop { # in that event. $DescriptorMap{$events[$i]->[0]}->event_step; } - 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); + } while (PostEventLoop()); + _run_later(); } =head2 C<< CLASS->SetPostLoopCallback( CODEREF ) >> @@ -287,37 +310,6 @@ sub SetPostLoopCallback { $PostLoopCallback = (defined $ref && ref $ref eq 'CODE') ? $ref : undef; } -# Internal function: run the post-event callback, send read events -# for pushed-back data, and close pending connections. returns 1 -# if event loop should continue, or 0 to shut it all down. -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}; - } - - - # by default we keep running, unless a postloop callback (either per-object - # or global) cancels it - my $keep_running = 1; - - # now we're at the very end, call callback if defined - if (defined $PostLoopCallback) { - $keep_running &&= $PostLoopCallback->(\%DescriptorMap); - } - - return $keep_running; -} - ##################################################################### ### PublicInbox::DS-the-object code ##################################################################### @@ -341,35 +333,19 @@ 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; - - $self->{event_watch} = $ev; - _InitPoller(); - if ($HaveEpoll) { -retry: - if (epoll_ctl($Epoll, EPOLL_CTL_ADD, $fd, $ev)) { - if ($! == EINVAL && ($ev & EPOLLEXCLUSIVE)) { - $self->{event_watch} = ($ev &= ~EPOLLEXCLUSIVE); - 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) { - my $f = $ev & EPOLLIN ? EV_ENABLE() : EV_DISABLE(); - $KQueue->EV_SET($fd, EVFILT_READ(), EV_ADD() | $f); - $f = $ev & EPOLLOUT ? EV_ENABLE() : EV_DISABLE(); - $KQueue->EV_SET($fd, EVFILT_WRITE(), EV_ADD() | $f); - } - - 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; } @@ -377,6 +353,8 @@ retry: ### I N S T A N C E M E T H O D S ##################################################################### +sub requeue ($) { push @$nextq, $_[0] } # autovivifies + =head2 C<< $obj->close >> Close the socket. @@ -393,11 +371,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 @@ -410,7 +386,7 @@ sub close { # defer closing the actual socket until the event loop is done # processing this round of events. (otherwise we might reuse fds) - push @ToClose, $sock; + push @$ToClose, $sock; # autovivifies $ToClose return 0; } @@ -419,8 +395,8 @@ sub close { sub psendfile ($$$) { my ($sock, $fh, $off) = @_; - sysseek($fh, $$off, SEEK_SET) or return; - defined(my $to_write = sysread($fh, my $buf, 16384)) or return; + 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))) { @@ -435,17 +411,21 @@ 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) = @_; 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]) { if (ref($bref) ne 'CODE') { my $off = delete($self->{wbuf_off}) // 0; - while (1) { + while ($sock) { my $w = psendfile($sock, $bref, \$off); if (defined $w) { if ($w == 0) { @@ -453,8 +433,8 @@ next_buf: goto next_buf; } } elsif ($! == EAGAIN) { + epwait($sock, epbit($sock, EPOLLOUT) | EPOLLONESHOT); $self->{wbuf_off} = $off; - watch_write($self, 1); return 0; } else { return $self->close; @@ -462,38 +442,60 @@ next_buf: } } 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 write_in_full ($$$$) { - my ($fh, $bref, $len, $off) = @_; - my $rv = 0; - while ($len > 0) { - my $w = syswrite($fh, $$bref, $len, $off); - return ($rv ? $rv : $w) unless $w; # undef or 0 - $rv += $w; - $len -= $w; - $off += $w; +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; } - $rv } -sub tmpbuf ($$) { - my ($bref, $off) = @_; - # open(my $fh, '+>>', undef) doesn't set O_APPEND - my ($fh, $path) = tempfile('wbuf-XXXXXXX', TMPDIR => 1); - open $fh, '+>>', $path or die "open: $!"; - unlink $path; - my $to_write = bytes::length($$bref) - $off; - my $w = write_in_full($fh, $bref, $to_write, $off); - die "write_in_full ($to_write): $!" unless defined $w; - $w == $to_write ? $fh : die("short write $w < $to_write"); +# 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 ) >> @@ -517,20 +519,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... + 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 - write_in_full($last, $bref, bytes::length($$bref), 0); + $last->print($$bref) or return drop($self, "print: $!"); } else { - push @$wbuf, tmpbuf($bref, 0); + 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); @@ -538,13 +542,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; } - $self->{wbuf} = [ tmpbuf($bref, $written) ]; - watch_write($self, 1); + + # 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; # autovivifies return 0; } } @@ -554,63 +565,115 @@ 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}) { + 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: - $self->{wbuf} = [ tmpbuf(\($_[1]), $n) ]; - watch_write($self, 1); + my $tmpio = tmpio($self, \($_[1]), $n) or return 0; + push @{$self->{wbuf}}, $tmpio; # autovivifies + epwait($sock, EPOLLOUT|EPOLLONESHOT); return 0; } } - $self->write(\($_[1])); + + # don't redispatch into NNTPdeflate::write + PublicInbox::DS::write($self, \($_[1])); +} + +sub epwait ($$) { + my ($sock, $ev) = @_; + epoll_ctl($Epoll, EPOLL_CTL_MOD, fileno($sock), $ev) and + confess("EPOLL_CTL_MOD $!"); +} + +# 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); # autovivifies + 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; + epwait($sock, PublicInbox::TLS::epollbit() | EPOLLONESHOT); + unshift(@{$self->{wbuf}}, \&shutdn_tls_step); # autovivifies + 0; } -sub watch_chg ($$$) { - my ($self, $bits, $set) = @_; +# 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; - my $cur = $self->{event_watch}; - my $changes = $cur; - if ($set) { - $changes |= $bits; + if (ref($sock) eq 'IO::Socket::SSL') { + shutdn_tls_step($self); } else { - $changes &= ~$bits; + $self->close; } - return if $changes == $cur; - my $fd = fileno($sock); - if ($HaveEpoll) { - epoll_ctl($Epoll, EPOLL_CTL_MOD, $fd, $changes) and - confess("EPOLL_CTL_MOD $!"); - } elsif ($HaveKQueue) { - my $flag = $set ? EV_ENABLE() : EV_DISABLE(); - $KQueue->EV_SET($fd, EVFILT_READ(), $flag) if $bits & EPOLLIN; - $KQueue->EV_SET($fd, EVFILT_WRITE(), $flag) if $bits & EPOLLOUT; - } - $self->{event_watch} = $changes; } -=head2 C<< $obj->watch_read( $boolean ) >> +# must be called with eval, PublicInbox::DS may not be loaded (see t/qspawn.t) +sub dwaitpid ($$$) { + die "Not in EventLoop\n" unless $in_loop; + push @$wait_pids, [ @_ ]; # [ $pid, $cb, $arg ] -Turn 'readable' event notification on or off. + # We could've just missed our SIGCHLD, cover it, here: + requeue(\&reap_pids); +} -=cut -sub watch_read ($$) { watch_chg($_[0], EPOLLIN, $_[1]) }; +sub _run_later () { + my $run = $later_queue or return; + $later_timer = $later_queue = undef; + $_->() for @$run; +} -=head2 C<< $obj->watch_write( $boolean ) >> +sub later ($) { + push @$later_queue, $_[0]; # autovivifies @$later_queue + $later_timer //= add_timer(60, \&_run_later); +} -Turn 'writable' event notification on or off. +sub expire_old () { + my $now = now(); + my $exp = $EXPTIME; + my $old = $now - $exp; + my %new; + while (my ($fd, $idle_at) = each %$EXPMAP) { + if ($idle_at < $old) { + my $ds_obj = $DescriptorMap{$fd}; + $new{$fd} = $idle_at if !$ds_obj->shutdn; + } else { + $new{$fd} = $idle_at; + } + } + $EXPMAP = \%new; + $exp_timer = scalar(keys %new) ? later(\&expire_old) : undef; +} -=cut -sub watch_write ($$) { watch_chg($_[0], EPOLLOUT, $_[1]) }; +sub update_idle_time { + my ($self) = @_; + my $sock = $self->{sock} or return; + $EXPMAP->{fileno($sock)} = now(); + $exp_timer //= later(\&expire_old); +} -package PublicInbox::DS::Timer; -# [$abs_float_firetime, $coderef]; -sub cancel { - $_[0][1] = undef; +sub not_idle_long { + my ($self, $now) = @_; + my $sock = $self->{sock} or return; + my $idle_at = $EXPMAP->{fileno($sock)} or return; + ($idle_at + $EXPTIME) > $now; } 1;