]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/NNTP.pm
nntp: speed up mid_lookup() using ->ALL extindex
[public-inbox.git] / lib / PublicInbox / NNTP.pm
index eb2c0b38c445025dec375e4dd8f443a35ced59e5..7b3b1ffe98eb99fe8a00a4186c207ca70d203a9b 100644 (file)
@@ -31,9 +31,9 @@ use Errno qw(EAGAIN);
 my $ONE_MSGID = qr/\A$MID_EXTRACT\z/;
 my @OVERVIEW = qw(Subject From Date Message-ID References);
 my $OVERVIEW_FMT = join(":\r\n", @OVERVIEW, qw(Bytes Lines), '') .
-               "Xref:full\r\n";
+               "Xref:full\r\n.";
 my $LIST_HEADERS = join("\r\n", @OVERVIEW,
-                       qw(:bytes :lines Xref To Cc)) . "\r\n";
+                       qw(:bytes :lines Xref To Cc)) . "\r\n.";
 my $CAPABILITIES = <<"";
 101 Capability list:\r
 VERSION 2\r
@@ -92,8 +92,7 @@ sub process_line ($$) {
                err($self, 'error from: %s (%s)', $l, $err);
                $res = '503 program fault - command not performed';
        }
-       return 0 unless defined $res;
-       res($self, $res);
+       defined($res) ? res($self, $res) : 0;
 }
 
 # The keyword argument is not used (rfc3977 5.2.2)
@@ -109,9 +108,7 @@ sub cmd_capabilities ($;$) {
 
 sub cmd_mode ($$) {
        my ($self, $arg) = @_;
-       $arg = uc $arg;
-       return r501 unless $arg eq 'READER';
-       '201 Posting prohibited';
+       uc($arg) eq 'READER' ? '201 Posting prohibited' : r501;
 }
 
 sub cmd_slave ($) { '202 slave status noted' }
@@ -120,46 +117,66 @@ sub cmd_xgtitle ($;$) {
        my ($self, $wildmat) = @_;
        more($self, '282 list of groups and descriptions follows');
        list_newsgroups($self, $wildmat);
-       '.'
 }
 
-sub list_overview_fmt ($) {
-       my ($self) = @_;
-       $self->msg_more($OVERVIEW_FMT);
-}
+sub list_overview_fmt ($) { $OVERVIEW_FMT }
 
-sub list_headers ($;$) {
-       my ($self) = @_;
-       $self->msg_more($LIST_HEADERS);
-}
+sub list_headers ($;$) { $LIST_HEADERS }
 
-sub list_active ($;$) {
-       my ($self, $wildmat) = @_;
-       wildmat2re($wildmat);
-       my $groups = $self->{nntpd}->{groups};
-       for my $ngname (grep(/$wildmat/, @{$self->{nntpd}->{groupnames}})) {
-               group_line($self, $groups->{$ngname});
+sub list_active_i { # "LIST ACTIVE" and also just "LIST" (no args)
+       my ($self, $groupnames) = @_;
+       my @window = splice(@$groupnames, 0, 100) or return 0;
+       my $ibx;
+       my $groups = $self->{nntpd}->{pi_config}->{-by_newsgroup};
+       for my $ngname (@window) {
+               $ibx = $groups->{$ngname} and group_line($self, $ibx);
        }
+       scalar(@$groupnames); # continue if there's more
 }
 
-sub list_active_times ($;$) {
+sub list_active ($;$) { # called by cmd_list
        my ($self, $wildmat) = @_;
        wildmat2re($wildmat);
-       my $groups = $self->{nntpd}->{groups};
-       for my $ngname (grep(/$wildmat/, @{$self->{nntpd}->{groupnames}})) {
-               my $ibx = $groups->{$ngname};
+       long_response($self, \&list_active_i, [
+               grep(/$wildmat/, @{$self->{nntpd}->{groupnames}}) ]);
+}
+
+sub list_active_times_i {
+       my ($self, $groupnames) = @_;
+       my @window = splice(@$groupnames, 0, 100) or return 0;
+       my $groups = $self->{nntpd}->{pi_config}->{-by_newsgroup};
+       for my $ngname (@window) {
+               my $ibx = $groups->{$ngname} or next;
                my $c = eval { $ibx->uidvalidity } // time;
                more($self, "$ngname $c <$ibx->{-primary_address}>");
        }
+       scalar(@$groupnames); # continue if there's more
 }
 
-sub list_newsgroups ($;$) {
+sub list_active_times ($;$) { # called by cmd_list
        my ($self, $wildmat) = @_;
        wildmat2re($wildmat);
-       my $groups = $self->{nntpd}->{groups};
-       for my $ngname (grep(/$wildmat/, @{$self->{nntpd}->{groupnames}})) {
-               more($self, "$ngname ".$groups->{$ngname}->description);
+       long_response($self, \&list_active_times_i, [
+               grep(/$wildmat/, @{$self->{nntpd}->{groupnames}}) ]);
+}
+
+sub list_newsgroups_i {
+       my ($self, $groupnames) = @_;
+       my @window = splice(@$groupnames, 0, 100) or return 0;
+       my $groups = $self->{nntpd}->{pi_config}->{-by_newsgroup};
+       my $ibx;
+       for my $ngname (@window) {
+               $ibx = $groups->{$ngname} and
+                       more($self, "$ngname ".$ibx->description);
        }
+       scalar(@$groupnames); # continue if there's more
+}
+
+sub list_newsgroups ($;$) { # called by cmd_list
+       my ($self, $wildmat) = @_;
+       wildmat2re($wildmat);
+       long_response($self, \&list_newsgroups_i, [
+               grep(/$wildmat/, @{$self->{nntpd}->{groupnames}}) ]);
 }
 
 # LIST SUBSCRIPTIONS, DISTRIB.PATS are not supported
@@ -168,6 +185,7 @@ sub cmd_list ($;$$) {
        if (scalar @args) {
                my $arg = shift @args;
                $arg =~ tr/A-Z./a-z_/;
+               my $ret = $arg eq 'active';
                $arg = "list_$arg";
                $arg = $self->can($arg);
                return r501 unless $arg && args_ok($arg, scalar @args);
@@ -175,11 +193,9 @@ sub cmd_list ($;$$) {
                $arg->($self, @args);
        } else {
                more($self, '215 list of newsgroups follows');
-               foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
-                       group_line($self, $ng);
-               }
+               long_response($self, \&list_active_i, [ # copy array
+                       @{$self->{nntpd}->{groupnames}} ]);
        }
-       '.'
 }
 
 sub listgroup_range_i {
@@ -247,6 +263,19 @@ sub group_line ($$) {
        more($self, "$ng->{newsgroup} $max $min n");
 }
 
+sub newgroups_i {
+       my ($self, $ts, $i, $groupnames) = @_;
+       my $end = $$i + 100;
+       my $groups = $self->{nntpd}->{pi_config}->{-by_newsgroup};
+       while ($$i < $end) {
+               my $ngname = $groupnames->[$$i++] // return;
+               my $ibx = $groups->{$ngname} or next; # expired on reload
+               next unless (eval { $ibx->uidvalidity } // 0) > $ts;
+               group_line($self, $ibx);
+       }
+       1;
+}
+
 sub cmd_newgroups ($$$;$$) {
        my ($self, $date, $time, $gmt, $dists) = @_;
        my $ts = eval { parse_time($date, $time, $gmt) };
@@ -254,12 +283,8 @@ sub cmd_newgroups ($$$;$$) {
 
        # TODO dists
        more($self, '231 list of new newsgroups follows');
-       foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
-               my $c = eval { $ng->uidvalidity } // 0;
-               next unless $c > $ts;
-               group_line($self, $ng);
-       }
-       '.'
+       long_response($self, \&newgroups_i, $ts, \(my $i = 0),
+                               $self->{nntpd}->{groupnames});
 }
 
 sub wildmat2re (;$) {
@@ -392,18 +417,26 @@ sub header_append ($$$) {
        $hdr->header_set($k, @v, $v);
 }
 
-sub xref ($$$$) {
-       my ($self, $ng, $n, $mid) = @_;
-       my $ret = $self->{nntpd}->{servername} . " $ng->{newsgroup}:$n";
-
-       # num_for is pretty cheap and sometimes we'll lookup the existence
-       # of an article without getting even the OVER info.  In other words,
-       # I'm not sure if its worth optimizing by scanning To:/Cc: and
-       # PublicInbox::ExtMsg on the PSGI end is just as expensive
-       foreach my $other (@{$self->{nntpd}->{grouplist}}) {
-               next if $ng eq $other;
-               my $num = eval { $other->mm->num_for($mid) } or next;
-               $ret .= " $other->{newsgroup}:$num";
+sub xref ($$$) {
+       my ($self, $cur_ibx, $smsg) = @_;
+       my $nntpd = $self->{nntpd};
+       my $cur_ngname = $cur_ibx->{newsgroup};
+       my $ret = "$nntpd->{servername} $cur_ngname:$smsg->{num}";
+       if (my $ALL = $nntpd->{pi_config}->ALL) {
+               if (my $ary = $ALL->nntp_xref_for($cur_ibx, $smsg)) {
+                       $ret .= join(' ', '', @$ary) if scalar(@$ary);
+               }
+               # better off wrong than slow if there's thousands of groups,
+               # so no fallback to the slow path below:
+       } else { # slow path
+               my $mid = $smsg->{mid};
+               my $groups = $nntpd->{pi_config}->{-by_newsgroup};
+               for my $xngname (@{$nntpd->{groupnames}}) {
+                       next if $cur_ngname eq $xngname;
+                       my $xibx = $groups->{$xngname} or next;
+                       my $num = eval { $xibx->mm->num_for($mid) } or next;
+                       $ret .= " $xngname:$num";
+               }
        }
        $ret;
 }
@@ -427,7 +460,7 @@ sub set_nntp_headers ($$) {
 
        # clobber some existing headers
        my $ibx = $smsg->{-ibx};
-       my $xref = xref($smsg->{nntp}, $ibx, $smsg->{num}, $mid);
+       my $xref = xref($smsg->{nntp}, $ibx, $smsg);
        $hdr->header_set('Xref', $xref);
 
        # RFC 5536 3.1.4
@@ -697,10 +730,36 @@ sub mid_lookup ($$) {
                my $n = $self_ng->mm->num_for($mid);
                return ($self_ng, $n) if defined $n;
        }
-       foreach my $ng (values %{$self->{nntpd}->{groups}}) {
-               next if defined $self_ng && $ng eq $self_ng;
-               my $n = $ng->mm->num_for($mid);
-               return ($ng, $n) if defined $n;
+       my $pi_cfg = $self->{nntpd}->{pi_config};
+       if (my $ALL = $pi_cfg->ALL) {
+               my ($id, $prev);
+               while (my $smsg = $ALL->over->next_by_mid($mid, \$id, \$prev)) {
+                       my $xr3 = $ALL->over->get_xref3($smsg->{num});
+                       if (my @x = grep(/:$smsg->{blob}\z/, @$xr3)) {
+                               my ($ngname, $xnum) = split(/:/, $x[0]);
+                               my $ibx = $pi_cfg->{-by_newsgroup}->{$ngname};
+                               return ($ibx, $xnum) if $ibx;
+                               # fall through to trying all xref3s
+                       } else {
+                               warn <<EOF;
+W: xref3 missing for <$mid> ($smsg->{blob}) in $ALL->{topdir}, -extindex bug?
+EOF
+                       }
+                       # try all xref3s
+                       for my $x (@$xr3) {
+                               my ($ngname, $xnum) = split(/:/, $x);
+                               my $ibx = $pi_cfg->{-by_newsgroup}->{$ngname};
+                               return ($ibx, $xnum) if $ibx;
+                               warn "W: `$ngname' does not exist for #$xnum\n";
+                       }
+               }
+               # no warning here, $mid is just invalid
+       } else { # slow path for non-ALL users
+               foreach my $ibx (values %{$self->{nntpd}->{groups}}) {
+                       next if defined $self_ng && $ibx eq $self_ng;
+                       my $n = $ibx->mm->num_for($mid);
+                       return ($ibx, $n) if defined $n;
+               }
        }
        (undef, undef);
 }
@@ -708,12 +767,12 @@ sub mid_lookup ($$) {
 sub xref_range_i {
        my ($self, $beg, $end) = @_;
        my $ng = $self->{ng};
-       my $r = $ng->mm->msg_range($beg, $end);
-       @$r or return;
+       my $msgs = $ng->over->query_xover($$beg, $end);
+       scalar(@$msgs) or return;
+       $$beg = $msgs->[-1]->{num} + 1;
        more($self, join("\r\n", map {
-               my $num = $_->[0];
-               "$num ".xref($self, $ng, $num, $_->[1]);
-       } @$r));
+               "$_->{num} ".xref($self, $ng, $_);
+       } @$msgs));
        1;
 }
 
@@ -724,8 +783,9 @@ sub hdr_xref ($$$) { # optimize XHDR Xref [range] for rtin
                my $mid = $1;
                my ($ng, $n) = mid_lookup($self, $mid);
                return r430 unless $n;
+               my $smsg = $ng->over->get_art($n) or return;
                hdr_mid_response($self, $xhdr, $ng, $n, $range,
-                               xref($self, $ng, $n, $mid));
+                               xref($self, $ng, $smsg));
        } else { # numeric range
                $range = $self->{article} unless defined $range;
                my $r = get_range($self, $range);
@@ -856,11 +916,11 @@ sub cmd_xrover ($;$) {
        long_response($self, \&xrover_i, @$r);
 }
 
-sub over_line ($$$$) {
-       my ($self, $ng, $num, $smsg) = @_;
+sub over_line ($$$) {
+       my ($self, $ng, $smsg) = @_;
        # n.b. field access and procedural calls can be
        # 10%-15% faster than OO method calls:
-       my $s = join("\t", $num,
+       my $s = join("\t", $smsg->{num},
                $smsg->{subject},
                $smsg->{from},
                PublicInbox::Smsg::date($smsg),
@@ -868,7 +928,7 @@ sub over_line ($$$$) {
                $smsg->{references},
                $smsg->{bytes},
                $smsg->{lines},
-               "Xref: " . xref($self, $ng, $num, $smsg->{mid}));
+               "Xref: " . xref($self, $ng, $smsg));
        utf8::encode($s);
        $s
 }
@@ -883,8 +943,8 @@ sub cmd_over ($;$) {
 
                # Only set article number column if it's the current group
                my $self_ng = $self->{ng};
-               $n = 0 if (!$self_ng || $self_ng ne $ng);
-               more($self, over_line($self, $ng, $n, $smsg));
+               $smsg->{num} = 0 if (!$self_ng || $self_ng ne $ng);
+               more($self, over_line($self, $ng, $smsg));
                '.';
        } else {
                cmd_xover($self, $range);
@@ -899,7 +959,7 @@ sub xover_i {
 
        # OVERVIEW.FMT
        more($self, join("\r\n", map {
-               over_line($self, $ng, $_->{num}, $_);
+               over_line($self, $ng, $_);
                } @$msgs));
        $$beg = $msgs->[-1]->{num} + 1;
 }