X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=t%2Fhttpd-unix.t;h=fe4a21616373a682d71020a6be26e7b10b6f76fe;hb=4eee5af6011cc8cdefb66c9729952c7eff5c0b0b;hp=bd4ee12eeb6869101763f9bef05eaa54d49facab;hpb=3c313f9034aac96182e2efdc2f92c40803626f32;p=public-inbox.git diff --git a/t/httpd-unix.t b/t/httpd-unix.t index bd4ee12e..fe4a2161 100644 --- a/t/httpd-unix.t +++ b/t/httpd-unix.t @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2019 all contributors +# Copyright (C) 2016-2021 all contributors # License: AGPL-3.0+ # Tests for binding Unix domain sockets use strict; @@ -6,12 +6,9 @@ use warnings; use Test::More; use PublicInbox::TestCommon; use Errno qw(EADDRINUSE); - -foreach my $mod (qw(Plack::Util Plack::Builder HTTP::Date HTTP::Status)) { - eval "require $mod"; - plan skip_all => "$mod missing for httpd-unix.t" if $@; -} - +use Cwd qw(abs_path); +use Carp qw(croak); +require_mods(qw(Plack::Util Plack::Builder HTTP::Date HTTP::Status)); use IO::Socket::UNIX; my ($tmpdir, $for_destroy) = tmpdir(); my $unix = "$tmpdir/unix.sock"; @@ -45,13 +42,12 @@ for (1..1000) { ok(-S $unix, 'UNIX socket was bound by -httpd'); sub check_sock ($) { my ($unix) = @_; - my $sock = IO::Socket::UNIX->new(Peer => $unix, Type => SOCK_STREAM); - warn "E: $! connecting to $unix\n" unless defined $sock; - ok($sock, 'client UNIX socket connected'); + my $sock = IO::Socket::UNIX->new(Peer => $unix, Type => SOCK_STREAM) + // BAIL_OUT "E: $! connecting to $unix"; ok($sock->write("GET /host-port HTTP/1.0\r\n\r\n"), 'wrote req to server'); ok($sock->read(my $buf, 4096), 'read response'); - like($buf, qr!\r\n\r\n127\.0\.0\.1:0\z!, + like($buf, qr!\r\n\r\n127\.0\.0\.1 0\z!, 'set REMOTE_ADDR and REMOTE_PORT for Unix socket'); } @@ -84,30 +80,122 @@ check_sock($unix); ok(-S $unix, 'unix socket still exists'); } +# portable Perl can delay or miss signal dispatches due to races, +# so disable some tests on systems lacking signalfd(2) or EVFILT_SIGNAL +my $has_sigfd = PublicInbox::Sigfd->new({}, 0) ? 1 : $ENV{TEST_UNRELIABLE}; + +sub delay_until { + my $cond = shift; + my $end = time + 30; + do { + return if $cond->(); + select undef, undef, undef, 0.012; + } until (time > $end); + Carp::confess('condition failed'); +} + SKIP: { - eval 'require Net::Server::Daemonize'; - skip('Net::Server missing for pid-file/daemonization test', 20) if $@; + require_mods('Net::Server::Daemonize', 52); + $has_sigfd or skip('signalfd / EVFILT_SIGNAL not available', 52); my $pid_file = "$tmpdir/pid"; + my $read_pid = sub { + my $f = shift; + open my $fh, '<', $f or die "open $f failed: $!"; + my $pid = do { local $/; <$fh> }; + chomp($pid) or die("pid file not ready $!"); + $pid; + }; + for my $w (qw(-W0 -W1)) { # wait for daemonization $spawn_httpd->("-l$unix", '-D', '-P', $pid_file, $w); $td->join; is($?, 0, "daemonized $w process"); check_sock($unix); - - ok(-f $pid_file, "$w pid file written"); - open my $fh, '<', "$tmpdir/pid" or die "open failed: $!"; - my $rpid = do { local $/; <$fh> }; - chomp $rpid; - like($rpid, qr/\A\d+\z/s, "$w pid file looks like a pid"); - is(kill('TERM', $rpid), 1, "signaled daemonized $w process"); - for (1..100) { - kill(0, $rpid) or last; - select undef, undef, undef, 0.02; - } - is(kill(0, $rpid), 0, "daemonized $w process exited"); + ok(-s $pid_file, "$w pid file written"); + my $pid = $read_pid->($pid_file); + is(kill('TERM', $pid), 1, "signaled daemonized $w process"); + delay_until(sub { !kill(0, $pid) }); + is(kill(0, $pid), 0, "daemonized $w process exited"); ok(!-e $pid_file, "$w pid file unlinked at exit"); } + + # try a USR2 upgrade with workers: + my $httpd = abs_path('blib/script/public-inbox-httpd'); + $psgi = abs_path($psgi); + my $opt = { run_mode => 0 }; + + my @args = ("-l$unix", '-D', '-P', $pid_file, -1, $out, -2, $err); + $td = start_script([$httpd, @args, $psgi], undef, $opt); + $td->join; + is($?, 0, "daemonized process again"); + check_sock($unix); + ok(-s $pid_file, 'pid file written'); + my $pid = $read_pid->($pid_file); + + # stop worker to ensure check_sock below hits $new_pid + kill('TTOU', $pid) or die "TTOU failed: $!"; + + kill('USR2', $pid) or die "USR2 failed: $!"; + delay_until(sub { + $pid != (eval { $read_pid->($pid_file) } // $pid) + }); + my $new_pid = $read_pid->($pid_file); + isnt($new_pid, $pid, 'new child started'); + ok($new_pid > 0, '$new_pid valid'); + delay_until(sub { -s "$pid_file.oldbin" }); + my $old_pid = $read_pid->("$pid_file.oldbin"); + is($old_pid, $pid, '.oldbin pid file written'); + ok($old_pid > 0, '$old_pid valid'); + + check_sock($unix); # ensures $new_pid is ready to receive signals + + # first, back out of the upgrade + kill('QUIT', $new_pid) or die "kill new PID failed: $!"; + delay_until(sub { + $pid == (eval { $read_pid->($pid_file) } // 0) + }); + is($read_pid->($pid_file), $pid, 'old PID file restored'); + ok(!-f "$pid_file.oldbin", '.oldbin PID file gone'); + + # retry USR2 upgrade + kill('USR2', $pid) or die "USR2 failed: $!"; + delay_until(sub { + $pid != (eval { $read_pid->($pid_file) } // $pid) + }); + $new_pid = $read_pid->($pid_file); + isnt($new_pid, $pid, 'new child started again'); + $old_pid = $read_pid->("$pid_file.oldbin"); + is($old_pid, $pid, '.oldbin pid file written'); + + # drop the old parent + kill('QUIT', $old_pid) or die "QUIT failed: $!"; + delay_until(sub { !kill(0, $old_pid) }); + ok(!-f "$pid_file.oldbin", '.oldbin PID file gone'); + + # drop the new child + check_sock($unix); + kill('QUIT', $new_pid) or die "QUIT failed: $!"; + delay_until(sub { !kill(0, $new_pid) }); + ok(!-f $pid_file, 'PID file is gone'); + + + # try USR2 without workers (-W0) + $td = start_script([$httpd, @args, '-W0', $psgi], undef, $opt); + $td->join; + is($?, 0, 'daemonized w/o workers'); + check_sock($unix); + $pid = $read_pid->($pid_file); + + # replace running process + kill('USR2', $pid) or die "USR2 failed: $!"; + delay_until(sub { !kill(0, $pid) }); + + check_sock($unix); + $pid = $read_pid->($pid_file); + kill('QUIT', $pid) or die "USR2 failed: $!"; + delay_until(sub { !kill(0, $pid) }); + ok(!-f $pid_file, 'PID file is gone'); } done_testing();