From 26f4bcb1ce2dad98c457c3b6b755cca134485b14 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Wed, 28 Feb 2018 12:13:27 -0500 Subject: Bug 1437646 - Add Bugzilla::DaemonControl --- Bugzilla/DaemonControl.pm | 327 ++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 4 + 2 files changed, 331 insertions(+) create mode 100644 Bugzilla/DaemonControl.pm diff --git a/Bugzilla/DaemonControl.pm b/Bugzilla/DaemonControl.pm new file mode 100644 index 000000000..a1c9fd8a5 --- /dev/null +++ b/Bugzilla/DaemonControl.pm @@ -0,0 +1,327 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::DaemonControl; +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::Constants qw(bz_locations); +use Cwd qw(realpath); +use English qw(-no_match_vars $PROGRAM_NAME); +use File::Spec::Functions qw(catfile); +use Future::Utils qw(repeat try_repeat); +use Future; +use IO::Async::Loop; +use IO::Async::Process; +use IO::Async::Protocol::LineStream; +use IO::Async::Signal; +use IO::Socket; +use LWP::Simple qw(get); +use POSIX qw(setsid WEXITSTATUS); + +use base qw(Exporter); + +our @EXPORT_OK = qw( + run_httpd run_cereal run_cereal_and_httpd + catch_signal on_finish on_exception + assert_httpd assert_database assert_selenium +); + +our %EXPORT_TAGS = ( + all => \@EXPORT_OK, + run => [grep { /^run_/ } @EXPORT_OK], + utils => [qw(catch_signal on_exception on_finish)], +); + +use constant HTTPD_BIN => '/usr/sbin/httpd'; +use constant HTTPD_CONFIG => realpath(catfile( bz_locations->{confdir}, 'httpd.conf' )); + +sub catch_signal { + my ($name, @done) = @_; + my $loop = IO::Async::Loop->new; + my $signal_f = $loop->new_future; + my $signal = IO::Async::Signal->new( + name => $name, + on_receipt => sub { + $signal_f->done(@done); + } + ); + $signal_f->on_cancel( + sub { + my $l = IO::Async::Loop->new; + $l->remove($signal); + }, + ); + + $loop->add($signal); + + return $signal_f; +} + +sub cereal { + local $PROGRAM_NAME = "cereal"; + $ENV{LOGGING_PORT} //= 5880; + + my $loop = IO::Async::Loop->new; + my $on_stream = sub { + my ($stream) = @_; + my $protocol = IO::Async::Protocol::LineStream->new( + transport => $stream, + on_read_line => sub { + my ( $self, $line ) = @_; + say $line; + }, + ); + $loop->add($protocol); + }; + $loop->listen( + host => '127.0.0.1', + service => $ENV{LOGGING_PORT}, + socktype => 'stream', + on_stream => $on_stream, + )->get; + kill 'USR1', getppid(); + + exit catch_signal('TERM', 0)->get; +} + +sub run_cereal { + my $loop = IO::Async::Loop->new; + my $exit_f = $loop->new_future; + my $cereal = IO::Async::Process->new( + code => \&cereal, + on_finish => on_finish($exit_f), + on_exception => on_exception( "cereal", $exit_f ), + ); + $exit_f->on_cancel( sub { $cereal->kill('TERM') } ); + my $signal_f = catch_signal('USR1'); + $loop->add($cereal); + $signal_f->get; + + return $exit_f; +} + +sub run_httpd { + my (@args) = @_; + my $loop = IO::Async::Loop->new; + + my $exit_f = $loop->new_future; + my $httpd = IO::Async::Process->new( + code => sub { + # we have to setsid() to make a new process group + # or else apache will kill its parent. + setsid(); + exec HTTPD_BIN, '-DFOREGROUND', '-f' => HTTPD_CONFIG, @args; + }, + on_finish => on_finish($exit_f), + on_exception => on_exception( 'httpd', $exit_f ), + ); + $exit_f->on_cancel( sub { $httpd->kill('TERM') } ); + $loop->add($httpd); + + return $exit_f; +} + +sub run_cereal_and_httpd { + my @httpd_args = @_; + + my $lc = Bugzilla::Install::Localconfig::read_localconfig(); + if ( ($lc->{inbound_proxies} // '') eq '*' && $lc->{urlbase} =~ /^https/) { + push @httpd_args, '-DHTTPS'; + } + push @httpd_args, '-DNETCAT_LOGS'; + my $cereal_exit_f = run_cereal(); + my $signal_f = catch_signal("TERM", 0); + my $httpd_exit_f = run_httpd(@httpd_args); + + return Future->wait_any($cereal_exit_f, $httpd_exit_f, $signal_f); +} + +sub assert_httpd { + my $loop = IO::Async::Loop->new; + my $port = $ENV{PORT} // 8000; + my $repeat = repeat { + $loop->delay_future(after => 0.25)->then( + sub { + Future->wrap(get("http://localhost:$port/__lbheartbeat__") // ''); + }, + ); + } until => sub { + my $f = shift; + ( $f->get =~ /^httpd OK/ ); + }; + my $timeout = $loop->timeout_future(after => 20)->else_fail("assert_httpd timeout"); + return Future->wait_any($repeat, $timeout); +} + +sub assert_selenium { + my ($host, $port) = @_; + $host //= 'localhost'; + $port //= 4444; + my $loop = IO::Async::Loop->new; + my $repeat = repeat { + $loop->delay_future(after => 1)->then( + sub { + my $sock = IO::Socket::INET->new( PeerAddr => $host, PeerPort => $port ); + Future->wrap($sock ? 1 : 0); + }, + ); + } until => sub { shift->get }; + my $timeout = $loop->timeout_future(after => 60)->else_fail("assert_selenium timeout"); + return Future->wait_any($repeat, $timeout); +} + +sub assert_database { + my $loop = IO::Async::Loop->new; + my $lc = Bugzilla::Install::Localconfig::read_localconfig(); + + for my $var (qw(db_name db_host db_user db_pass)) { + return $loop->new_future->die("$var is not set!") unless $lc->{$var}; + } + + my $dsn = "dbi:mysql:database=$lc->{db_name};host=$lc->{db_host}"; + my $repeat = repeat { + $loop->delay_future( after => 0.25 )->then( + sub { + my $dbh = DBI->connect( + $dsn, + $lc->{db_user}, + $lc->{db_pass}, + { RaiseError => 0, PrintError => 0 }, + ); + Future->wrap($dbh); + } + ); + } until => sub { defined shift->get }; + + my $timeout = $loop->timeout_future( after => 20 )->else_fail("assert_database timeout"); + my $any_f = Future->needs_any( $repeat, $timeout ); + return $any_f->transform( + done => sub { return }, + fail => sub { "unable to connect to $dsn as $lc->{db_user}" }, + ); +} + +sub on_finish { + my ($f) = @_; + + return sub { + my ($self, $exitcode) = @_; + $f->done(WEXITSTATUS($exitcode)); + }; +} + +sub on_exception { + my ( $name, $f ) = @_; + + return sub { + my ( $self, $exception, $errno, $exitcode ) = @_; + + if ( length $exception ) { + $f->fail( "$name died with the exception $exception (errno was $errno)\n" ); + } + elsif ( ( my $status = WEXITSTATUS($exitcode) ) == 255 ) { + $f->fail("$name failed to exec() - $errno\n"); + } + else { + $f->fail("$name exited with exit status $status\n"); + } + }; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::DaemonControl - Utility functions for controlling daemons + +=head1 SYNOPSIS + + my $httpd_exit_code_f = run_httpd(@httpd_args); + my $signal_f = catch_signal("TERM"); + +=head1 DESCRIPTION + +This module exports functions that either start daemons (L, L), +check for running services (L, L, L), +or help build more functions like the above (L, L). + +The C and C functions return Futures, see L for details +on that. But if you've used Promises in the javascript, Futures are the same concept. + +=head1 FUNCTIONS + +Nothing is exported by default, but you can request C<:all> for that. +You can also just get the run_* functions with C<:run>. + +=head2 run_httpd() + +This function starts an httpd and returns a future that is B when the httpd exits. +The return value will be the exit code of the process. + +Thus the following program would exit with whatever value httpd exits with: + + exit run_httpd()->get; + +It may also B in unlikely situations, such as a L failing, httpd not being found, etc. + +Canceling the future will send C to httpd. + +=head2 run_cereal() + +This runs a builtin process that listens on C for TCP +connections. Each connection may send lines of text, and those lines of text +will be written to B. Once you start this, you should limit or stop +entirely printing to B to ensure that output is well-ordered. + +If you need to listen on a different port, set the environmental variable +C. + +This returns a future similar to L. +Canceling the future will terminate the cereal daemon. + +=head2 run_cereal_and_httpd() + +This will start up cereal and the httpd. It will return a future +that is B when either httpd or cereal exits. +The future will also be B if C is sent to the process that calls +this function. + +Because of how futures work, when one of these processes is done (or when we get the signal) +the other futures are canceled. + +This means that if cereal exits, httpd will exit. +And if httpd exits, cereal will exit. + +=head2 assert_database() + +This provides a simple way to wait on the database being up. +It will either be B with no usable return value, or fail with a timeout error. + + # wait until we have a database + assert_database()->get; + +=head2 assert_seleniuim() + +This returns a future that is complete when we can reach selenium, +or it fails with a timeout. + +=head2 assert_httpd() + +This returns a future that is complete when we can reach the __lbheartbeat__ +endpoint, or it fails with a timeout. + +=head2 on_finish($f) + +This returns a callback that will complete a future. It is to be used with L. + +=head2 on_exception($f) + +This returns a callback that will fail a future. It is to be used with L. diff --git a/Dockerfile b/Dockerfile index 6ebdc1946..c1525f217 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,12 @@ ENV HTTPD_MaxSpareServers=20 ENV HTTPD_ServerLimit=256 ENV HTTPD_MaxClients=256 ENV HTTPD_MaxRequestsPerChild=4000 + ENV PORT=8000 +# we run a loopback logging server on this TCP port. +ENV LOGGING_PORT=5880 + WORKDIR /app COPY . . -- cgit v1.2.3-24-g4f1b