]> git.ipfire.org Git - thirdparty/public-inbox.git/commitdiff
www: use a dedicated limiter for blob solver
authorEric Wong <e@80x24.org>
Mon, 11 Mar 2024 19:40:09 +0000 (19:40 +0000)
committerEric Wong <e@80x24.org>
Tue, 12 Mar 2024 06:18:15 +0000 (06:18 +0000)
Wrap the entire solver command chain with a dedicated limiter.
The normal limiter is designed for longer-lived commands or ones
which serve a single HTTP request (e.g. git-http-backend or
cgit) and not effective for short memory + CPU intensive commands
used for solver.

Each overall solver request is both memory + CPU intensive: it
spawns several short-lived git processes(*) in addition to a
longer-lived `git cat-file --batch' process.

Thus running parallel solvers from a single -netd/-httpd worker
(which have their own parallelization) results in excessive
parallelism that is both memory and CPU-bound (not network-bound)
and cascade into slowdowns for handling simpler memory/CPU-bound
requests.  Parallel solvers were also responsible for the
increased lifetime and frequency of zombies since the event loop
was too saturated to reap them.

We'll also return 503 on excessive solver queueing, since these
require an FD for the client HTTP(S) socket to be held onto.

(*) git (update-index|apply|ls-files) are all run by solver and
    short-lived

lib/PublicInbox/SolverGit.pm
lib/PublicInbox/ViewVCS.pm

index 4e79f7505c686bf9e99d5e6943611e44e914c5f2..296e7d17841564279b8993b73655ce9354d4e551 100644 (file)
@@ -256,6 +256,12 @@ sub update_index_result ($$) {
        next_step($self); # onto do_git_apply
 }
 
+sub qsp_qx ($$$) {
+       my ($self, $qsp, $cb) = @_;
+       $qsp->{qsp_err} = \($self->{-qsp_err} = '');
+       $qsp->psgi_qx($self->{psgi_env}, $self->{limiter}, $cb, $self);
+}
+
 sub prepare_index ($) {
        my ($self) = @_;
        my $patches = $self->{patches};
@@ -284,9 +290,8 @@ sub prepare_index ($) {
        my $cmd = [ qw(git update-index -z --index-info) ];
        my $qsp = PublicInbox::Qspawn->new($cmd, $self->{git_env}, $rdr);
        $path_a = git_quote($path_a);
-       $qsp->{qsp_err} = \($self->{-qsp_err} = '');
        $self->{-msg} = "index prepared:\n$mode_a $oid_full\t$path_a";
-       $qsp->psgi_qx($self->{psgi_env}, undef, \&update_index_result, $self);
+       qsp_qx $self, $qsp, \&update_index_result;
 }
 
 # pure Perl "git init"
@@ -465,8 +470,7 @@ sub apply_result ($$) { # qx_cb
        my @cmd = qw(git ls-files -s -z);
        my $qsp = PublicInbox::Qspawn->new(\@cmd, $self->{git_env});
        $self->{-cur_di} = $di;
-       $qsp->{qsp_err} = \($self->{-qsp_err} = '');
-       $qsp->psgi_qx($self->{psgi_env}, undef, \&ls_files_result, $self);
+       qsp_qx $self, $qsp, \&ls_files_result;
 }
 
 sub do_git_apply ($) {
@@ -495,8 +499,7 @@ sub do_git_apply ($) {
        my $opt = { 2 => 1, -C => _tmp($self)->dirname, quiet => 1 };
        my $qsp = PublicInbox::Qspawn->new(\@cmd, $self->{git_env}, $opt);
        $self->{-cur_di} = $di;
-       $qsp->{qsp_err} = \($self->{-qsp_err} = '');
-       $qsp->psgi_qx($self->{psgi_env}, undef, \&apply_result, $self);
+       qsp_qx $self, $qsp, \&apply_result;
 }
 
 sub di_url ($$) {
index 61329db6c21dc18850666657eaaff2f48d7864d4..790b9a2c59089c2fd2e8b7d72ed7cc21b77c559c 100644 (file)
@@ -49,6 +49,10 @@ my %GIT_MODE = (
        '160000' => 'g', # commit (gitlink)
 );
 
+# TODO: not fork safe, but we don't fork w/o exec in PublicInbox::WWW
+my (@solver_q, $solver_lim);
+my $solver_nr = 0;
+
 sub html_page ($$;@) {
        my ($ctx, $code) = @_[0, 1];
        my $wcb = delete $ctx->{-wcb};
@@ -614,26 +618,52 @@ sub show_blob { # git->cat_async callback
                '</code></pre></td></tr></table>'.dbg_log($ctx), @def);
 }
 
-# GET /$INBOX/$GIT_OBJECT_ID/s/
-# GET /$INBOX/$GIT_OBJECT_ID/s/$FILENAME
-sub show ($$;$) {
-       my ($ctx, $oid_b, $fn) = @_;
-       my $hints = $ctx->{hints} = {};
+sub start_solver ($) {
+       my ($ctx) = @_;
        while (my ($from, $to) = each %QP_MAP) {
                my $v = $ctx->{qp}->{$from} // next;
-               $hints->{$to} = $v if $v ne '';
+               $ctx->{hints}->{$to} = $v if $v ne '';
        }
-       $ctx->{fn} = $fn;
-       $ctx->{-tmp} = File::Temp->newdir("solver.$oid_b-XXXX", TMPDIR => 1);
+       $ctx->{-next_solver} = PublicInbox::OnDestroy->new($$, \&next_solver);
+       ++$solver_nr;
+       $ctx->{-tmp} = File::Temp->newdir("solver.$ctx->{oid_b}-XXXX",
+                                               TMPDIR => 1);
        $ctx->{lh} or open $ctx->{lh}, '+>>', "$ctx->{-tmp}/solve.log";
        my $solver = PublicInbox::SolverGit->new($ctx->{ibx},
                                                \&solve_result, $ctx);
+       $solver->{limiter} = $solver_lim;
        $solver->{gits} //= [ $ctx->{git} ];
        $solver->{tmp} = $ctx->{-tmp}; # share tmpdir
        # PSGI server will call this immediately and give us a callback (-wcb)
+       $solver->solve(@$ctx{qw(env lh oid_b hints)});
+}
+
+# run the next solver job when done and DESTROY-ed
+sub next_solver {
+       --$solver_nr;
+       # XXX FIXME: client may've disconnected if it waited a long while
+       start_solver(shift(@solver_q) // return);
+}
+
+sub may_start_solver ($) {
+       my ($ctx) = @_;
+       $solver_lim //= $ctx->{www}->{pi_cfg}->limiter('codeblob');
+       if ($solver_nr >= $solver_lim->{max}) {
+               @solver_q > 128 ? html_page($ctx, 503, 'too busy')
+                               : push(@solver_q, $ctx);
+       } else {
+               start_solver($ctx);
+       }
+}
+
+# GET /$INBOX/$GIT_OBJECT_ID/s/
+# GET /$INBOX/$GIT_OBJECT_ID/s/$FILENAME
+sub show ($$;$) {
+       my ($ctx, $oid_b, $fn) = @_;
+       @$ctx{qw(oid_b fn)} = ($oid_b, $fn);
        sub {
                $ctx->{-wcb} = $_[0]; # HTTP write callback
-               $solver->solve($ctx->{env}, $ctx->{lh}, $oid_b, $hints);
+               may_start_solver $ctx;
        };
 }