+my $EXPMAP; # fd -> [ idle_time, $self ]
+my $expt;
+our $EXPTIME = 180; # 3 minutes
+my $WEAKEN = {}; # string(nntpd) -> nntpd
+my $weakt;
+my $nextt;
+
+my $nextq = [];
+sub next_tick () {
+ $nextt = undef;
+ my $q = $nextq;
+ $nextq = [];
+ foreach my $nntp (@$q) {
+ # for request && response protocols, always finish writing
+ # before finishing reading:
+ if (my $long_cb = $nntp->{long_res}) {
+ $nntp->write($long_cb);
+ } elsif (&Danga::Socket::POLLIN & $nntp->{event_watch}) {
+ event_read($nntp);
+ }
+ }
+}
+
+sub update_idle_time ($) {
+ my ($self) = @_;
+ my $tmp = $self->{sock} or return;
+ $tmp = fileno($tmp);
+ defined $tmp and $EXPMAP->{$tmp} = [ now(), $self ];
+}
+
+# reduce FD pressure by closing some "git cat-file --batch" processes
+# and unused FDs for msgmap and Xapian indices
+sub weaken_groups () {
+ $weakt = undef;
+ foreach my $nntpd (values %$WEAKEN) {
+ $_->weaken_all foreach (@{$nntpd->{grouplist}});
+ }
+ $WEAKEN = {};
+}
+
+sub expire_old () {
+ my $now = now();
+ my $exp = $EXPTIME;
+ my $old = $now - $exp;
+ my $nr = 0;
+ my %new;
+ while (my ($fd, $v) = each %$EXPMAP) {
+ my ($idle_time, $nntp) = @$v;
+ if ($idle_time < $old) {
+ $nntp->close; # idempotent
+ } else {
+ ++$nr;
+ $new{$fd} = $v;
+ }
+ }
+ $EXPMAP = \%new;
+ if ($nr) {
+ $expt = PublicInbox::EvCleanup::later(*expire_old);
+ weaken_groups();
+ } else {
+ $expt = undef;
+ # noop to kick outselves out of the loop ASAP so descriptors
+ # really get closed
+ PublicInbox::EvCleanup::asap(sub {});
+
+ # grace period for reaping resources
+ $weakt ||= PublicInbox::EvCleanup::later(*weaken_groups);
+ }
+}
+