package Feersum::Runner; use warnings; use strict; use EV; use Feersum; use Socket qw/SOMAXCONN/; use POSIX (); use Scalar::Util qw/weaken/; use Carp qw/carp croak/; use constant DEATH_TIMER => 5.0; # seconds use constant DEATH_TIMER_INCR => 2.0; # seconds use constant DEFAULT_HOST => 'localhost'; use constant DEFAULT_PORT => 5000; our $INSTANCE; sub new { ## no critic (RequireArgUnpacking) my $c = shift; croak "Only one Feersum::Runner instance can be active at a time" if $INSTANCE && $INSTANCE->{running}; $INSTANCE = bless {quiet=>1, @_, running=>0}, $c; return $INSTANCE; } sub DESTROY { local $@; my $self = shift; if (my $f = $self->{endjinn}) { $f->request_handler(sub{}); $f->unlisten(); } $self->{_quit} = undef; return; } sub _prepare { my $self = shift; $self->{listen} ||= [ ($self->{host}||DEFAULT_HOST).':'.($self->{port}||DEFAULT_PORT) ]; croak "Feersum doesn't support multiple 'listen' directives yet" if @{$self->{listen}} > 1; my $listen = shift @{$self->{listen}}; my $sock; if ($listen =~ m#^unix/#) { croak "listening on a unix socket isn't supported yet"; } else { require IO::Socket::INET; $sock = IO::Socket::INET->new( LocalAddr => $listen, ReuseAddr => 1, Proto => 'tcp', Listen => SOMAXCONN, Blocking => 0, ); croak "couldn't bind to socket: $!" unless $sock; } $self->{sock} = $sock; my $f = Feersum->endjinn; $f->use_socket($sock); if ($self->{options}) { # Plack::Runner puts these here $self->{pre_fork} = delete $self->{options}{pre_fork}; } $self->{endjinn} = $f; return; } # for overriding: sub assign_request_handler { ## no critic (RequireArgUnpacking) return $_[0]->{endjinn}->request_handler($_[1]); } sub run { my $self = shift; weaken $self; $self->{quiet} or warn "Feersum [$$]: starting...\n"; $self->_prepare(); my $app = shift || delete $self->{app}; if (!$app && $self->{app_file}) { local ($@, $!); $app = do $self->{app_file}; warn "couldn't parse $self->{app_file}: $@" if $@; warn "couldn't do $self->{app_file}: $!" if ($! && !defined $app); warn "couldn't run $self->{app_file}: didn't return anything" unless $app; } die "app not defined or failed to compile" unless $app; $self->assign_request_handler($app); undef $app; $self->{_quit} = EV::signal 'QUIT', sub { $self->quit }; $self->_start_pre_fork if $self->{pre_fork}; EV::run; $self->{quiet} or warn "Feersum [$$]: done\n"; $self->DESTROY(); return; } sub _fork_another { my ($self, $slot) = @_; weaken $self; my $pid = fork; croak "failed to fork: $!" unless defined $pid; unless ($pid) { EV::default_loop()->loop_fork; $self->{quiet} or warn "Feersum [$$]: starting\n"; delete $self->{_kids}; delete $self->{pre_fork}; eval { EV::run; }; ## no critic (RequireCheckingReturnValueOfEval) carp $@ if $@; POSIX::exit($@ ? -1 : 0); ## no critic (ProhibitMagicNumbers) } $self->{_n_kids}++; $self->{_kids}[$slot] = EV::child $pid, 0, sub { my $w = shift; $self->{quiet} or warn "Feersum [$$]: child $pid exited ". "with rstatus ".$w->rstatus."\n"; $self->{_n_kids}--; if ($self->{_shutdown}) { EV::break(EV::BREAK_ALL()) unless $self->{_n_kids}; return; } $self->_fork_another(); }; return; } sub _start_pre_fork { my $self = shift; POSIX::setsid(); $self->{_kids} = []; $self->{_n_kids} = 0; $self->_fork_another($_) for (1 .. $self->{pre_fork}); $self->{endjinn}->unlisten(); return; } sub quit { my $self = shift; return if $self->{_shutdown}; $self->{_shutdown} = 1; $self->{quiet} or warn "Feersum [$$]: shutting down...\n"; my $death = DEATH_TIMER; if ($self->{_n_kids}) { # in parent, broadcast SIGQUIT to the group (not self) kill 3, -$$; ## no critic (ProhibitMagicNumbers) $death += DEATH_TIMER_INCR; } else { # in child or solo process $self->{endjinn}->graceful_shutdown(sub { POSIX::exit(0) }); } $self->{_death} = EV::timer $death, 0, sub { POSIX::exit(1) }; return; } 1; __END__ =head1 NAME Feersum::Runner =head1 SYNOPSIS use Feersum::Runner; my $runner = Feersum::Runner->new( listen => 'localhost:5000', pre_fork => 0, quiet => 1, app_file => 'app.feersum', ); $runner->run($feersum_app); =head1 DESCRIPTION Much like L, but with far fewer options. =head1 METHODS =over 4 =item C<< Feersum::Runner->new(%params) >> Returns a Feersum::Runner singleton. Params are only applied for the first invocation. =over 8 =item listen Listen on this TCP socket (C format). =item pre_fork Fork this many worker processes. The fork is run immediately at startup and after the app is loaded (i.e. in the C method). =item quiet Don't be so noisy. (default: on) =item app_file Load this filename as a native feersum app. =back =item C<< $runner->run($feersum_app) >> Run Feersum with the specified app code reference. Note that this is not a PSGI app, but a native Feersum app. =item C<< $runner->assign_request_handler($subref) >> For sub-classes to override, assigns an app handler. (e.g. L). By default, this assigns a Feersum-native (and not PSGI) handler. =item C<< $runner->quit() >> Initiate a graceful shutdown. A signal handler for SIGQUIT will call this method. =back =head1 AUTHOR Jeremy Stashewsky, C<< stash@cpan.org >> =head1 COPYRIGHT AND LICENSE Copyright (C) 2010 by Jeremy Stashewsky & Socialtext Inc. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.7 or, at your option, any later version of Perl 5 you may have available. =cut