diff --git a/Changes b/Changes index ef5fc39..56b2602 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,10 @@ * Module name POD format fixed (RT 93280) * Add "forground" to --help (by marcusramberg) +0.00100X 2014-02-19 Kieren Diment + * Infrastructure for daemon plugins + * HotStandby plugin for zero downtime plack/fastcgi stuff + 0.001005 2014-02-19 SymKat * Constructor now accepts a list as well as a hashref * New method added: run_command, allows multiple instances of D::C diff --git a/Makefile.PL b/Makefile.PL index 4642e6a..7d1ffd5 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -6,6 +6,7 @@ all_from 'lib/Daemon/Control.pm'; license 'perl'; # uses Module::Install::Repository +build_requires 'Module::Install::Repository' => 0; auto_set_repository; # Specific dependencies @@ -13,7 +14,7 @@ requires 'File::Spec' => '0'; requires 'POSIX' => '0'; requires 'Cwd' => '0'; requires 'File::Path' => '2.08'; +recommends 'Role::Tiny' => 0; test_requires 'Test::More' => '0.88'; - WriteAll; diff --git a/README.md b/README.md index 966b61f..fbd8b99 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Write a program that describes the daemon: use strict; use Daemon::Control; - Daemon::Control->new( + exit Daemon::Control->new( name => "My Daemon", lsb_start => '$syslog $remote_fs', lsb_stop => '$syslog', @@ -44,7 +44,7 @@ Write a program that describes the daemon: By default `run` will use @ARGV for the action, and exit with an LSB compatible exit code. For finer control, you can use `run_command`, which will return the exit code, and accepts the action as an argument. This enables more programatic -control, as well as running multiple instances of M from one script. +control, as well as running multiple instances of [Daemon::Control](https://metacpan.org/pod/Daemon::Control) from one script. my $daemon = Daemon::Control->new( ... @@ -59,10 +59,6 @@ You can also make an LSB compatible init script: /home/symkat/etc/init.d/program get_init_file > /etc/init.d/program - - - - # CONSTRUCTOR The constructor takes the following arguments as a list or a hash ref. @@ -185,7 +181,7 @@ and STDERR will be redirected to `stderr_file`. Setting this to 0 will disable redirecting before a double fork. This is useful when you are using a code reference and would like to leave the filehandles alone until you're in control. -Call `-`redirect\_filehandles> on the Daemon::Control instance your coderef is +Call `->redirect_filehandles` on the Daemon::Control instance your coderef is passed to redirect the filehandles. ## stdout\_file @@ -253,7 +249,6 @@ as that the daemon started. A shortcut to turn status off and go into foregroun mode is `foreground` being set to 1, or `DC_FOREGROUND` being set as an environment variable. Additionally, calling `foreground` instead of `start` will override the forking mode at run-time. - $daemon->fork( 0 ); @@ -317,13 +312,25 @@ If this boolean flag is set to a true value all output from the init script $daemon->quiet( 1 ); +## reload\_signal + +The signal to send to the daemon when reloading it. +Default signal is `HUP`. + +## stop\_signals + +An array ref of signals that should be tried (in order) when +stopping the daemon. +Default signals are `TERM`, `TERM`, `INT` and `KILL` (yes, `TERM` +is tried twice). + # METHODS ## run\_command This function will process an action on the Daemon::Control instance. Valid arguments are those which a `do_` method exists for, such as -__start__, __stop__, __restart__. Returns the LSB exit code for the +**start**, **stop**, **restart**. Returns the LSB exit code for the action processed. ## run @@ -344,10 +351,10 @@ exits. Called by: ## do\_foreground -Is called when __foreground__ is given as an argument. Starts the +Is called when **foreground** is given as an argument. Starts the program or code reference and stays in the foreground -- no forking is done, regardless of the compile-time arguments. Additionally, -turns `quiet` on to avoid showing M output. +turns `quiet` on to avoid showing [Daemon::Control](https://metacpan.org/pod/Daemon::Control) output. /usr/bin/my_program_launcher.pl foreground @@ -367,8 +374,8 @@ Called by: ## do\_reload -Is called when reload is given as an argument. Sends a HUP signal to the -daemon. +Is called when reload is given as an argument. Sends the signal +`reload_signal` to the daemon. /usr/bin/my_program_launcher.pl reload @@ -410,9 +417,62 @@ An accessor for the PID. Set by read\_pid, or when the program is started. A function to dump the LSB compatible init script. Used by do\_get\_init\_file. +# FAQ + +## LOGGING TO SYSLOG + +Logging a daemon::control script to syslog can be a little involved. +If you're using Log4perl or similar, consider using +[Log::Dispatch::Syslog](https://metacpan.org/pod/Log::Dispatch::Syslog) and/or [Sys::Syslog](https://metacpan.org/pod/Sys::Syslog). An alternative +approach using a fifo is as follows: + +First, set up the stderr\_file and stdout\_file to a fifo. + + Daemon::Control->new({ + ..., # normal setup + stderr_file => "/var/log/myuser/myservice.fifo", + stdout_file => "/var/log/myuser/myservice.fifo", + ..., })->run; + +However you need a service running that reads from the fifo, in this +case logger(1). When your main service (that writes to the fifos) exits +this close is read by ` logger ` and causes it to exit. In order to avoid +that we created a service that respawns logger when it dies. This +example is for a redhat system running upstart: + +The following script can be dropped into /etc/init as fifo-logger.conf +And then started with ` initctl start fifo-logger `. + + # fifo-logger - logger process for fcgi + # + # Will respawn the logger process as it exits on file close + + start on stopped rc RUNLEVEL=[345] + + stop on starting shutdown + + console output + respawn + + script + echo $$ > /var/run/fifo-logger.pid + exec logger -f /var/log/myuser/myservice.fifo -t myservice -p local0.notice + end script + + pre-start script + if [ ! -e /var/log/myuser/myservice.fifo ]; then + mkfifo /var/log/myuser/myservice.fifo + fi + chown hiive.hiive /var/log/myuser/myservice.fifo + chmod 660 /var/log/myuser/myservice.fifo + end script + +From this point, all the output will be sent to syslog as local0.notice and can +then be routed/cycled a needed without requiring a restart of the application. + # AUTHOR - Kaitlyn Parkhurst (SymKat) __ ( Blog: [http://symkat.com/](http://symkat.com/) ) +> Kaitlyn Parkhurst (SymKat) __ ( Blog: [http://symkat.com/](http://symkat.com/) ) ## CONTRIBUTORS @@ -420,6 +480,8 @@ A function to dump the LSB compatible init script. Used by do\_get\_init\_file. - Mike Doherty (doherty) __ - Karen Etheridge (ether) __ - Ævar Arnfjörð Bjarmason (avar) __ +- Kieren Diment _ +- Mark Curtis _ ## SPONSORS diff --git a/lib/Daemon/Control.pm b/lib/Daemon/Control.pm index fe1c7ad..bebb720 100644 --- a/lib/Daemon/Control.pm +++ b/lib/Daemon/Control.pm @@ -8,7 +8,7 @@ use File::Path qw( make_path ); use Cwd 'abs_path'; require 5.008001; # Supporting 5.8.1+ -our $VERSION = '0.001005'; # 0.1.5 +our $VERSION = '0.00100X'; # 0.1.X $VERSION = eval $VERSION; my @accessors = qw( @@ -116,6 +116,23 @@ sub new { return $self; } +sub with_plugins { + my ($class, $plugins) = @_; + $plugins ||= (); + my @plugins = ref $plugins ? @$plugins : ($plugins); + return $class if ! $plugins; + + @plugins = map { + my ($fqns, $name) = $_ =~ /^(\+)?(.*?)$/; + $_ = $fqns ? $name : "Daemon::Control::Plugin::$name"; + } @plugins; + require Role::Tiny if @plugins; + if (@plugins) { + $class = Role::Tiny->create_class_with_roles($class, @plugins); + } + + return $class; +} # Set the uid, triggered from getting the uid if the user has changed. sub _set_uid_from_name { @@ -418,6 +435,21 @@ sub do_start { my ( $self ) = @_; # Optionally check if a process is already running with the same name + my $prereq_check = $self->_check_prereq_no_processes; + return $prereq_check if $prereq_check; + + # Make sure the PID file exists. + $self->_ensure_pid_file_exists(); + + # Duplicate Check + my $is_duplicate = $self->_check_for_duplicate(); + return $is_duplicate if $is_duplicate; + + return $self->_finish_start(); +} + +sub _check_prereq_no_processes { + my ($self) = @_; if ($self->prereq_no_process) { my $program = $self->program; @@ -431,20 +463,27 @@ sub do_start { return 1; } } +} - # Make sure the PID file exists. +sub _ensure_pid_file_exists { + my ($self) = @_; if ( ! -f $self->pid_file ) { $self->pid( 0 ); # Make PID invalid. $self->write_pid(); } +} - # Duplicate Check +sub _check_for_duplicate { + my ($self) = @_; $self->read_pid; if ( $self->pid && $self->pid_running ) { $self->pretty_print( "Duplicate Running", "red" ); return 1; } +} +sub _finish_start { + my ($self) = @_; $self->_create_resource_dir; $self->fork( 2 ) unless defined $self->fork; @@ -453,6 +492,7 @@ sub do_start { $self->_foreground if $self->fork == 0; $self->pretty_print( "Started" ); return 0; + } sub do_show_warnings { @@ -472,34 +512,15 @@ sub do_show_warnings { } sub do_stop { - my ( $self ) = @_; + my ( $self, $start_pid ) = @_; - $self->read_pid; - my $start_pid = $self->pid; + $start_pid ||= $self->_get_start_pid; # Probably don't want to send anything to init(1). return 1 unless $start_pid > 1; - if ( $self->pid_running($start_pid) ) { - SIGNAL: - foreach my $signal (@{ $self->stop_signals }) { - $self->trace( "Sending $signal signal to pid $start_pid..." ); - kill $signal => $start_pid; - - for (1..$self->kill_timeout) - { - # abort early if the process is now stopped - $self->trace("checking if pid $start_pid is still running..."); - last if not $self->pid_running($start_pid); - sleep 1; - } - last unless $self->pid_running($start_pid); - } - if ( $self->pid_running($start_pid) ) { - $self->pretty_print( "Failed to Stop", "red" ); - return 1; - } - $self->pretty_print( "Stopped" ); + my $failed = $self->_send_stop_signals($start_pid); + return 1 if $failed; } else { $self->pretty_print( "Not Running", "red" ); } @@ -515,6 +536,35 @@ sub do_stop { return 0; } +sub _get_start_pid { + my ($self) = @_; + $self->read_pid; + return $self->pid; +} + +sub _send_stop_signals { + my ($self, $start_pid) = @_; + SIGNAL: + foreach my $signal (@{ $self->stop_signals }) { + $self->trace( "Sending $signal signal to pid $start_pid..." ); + kill $signal => $start_pid; + + for (1..$self->kill_timeout) + { + # abort early if the process is now stopped + $self->trace("checking if pid $start_pid is still running..."); + last if not $self->pid_running($start_pid); + sleep 1; + } + last unless $self->pid_running($start_pid); + } + if ( $ARGV[0] ne 'restart' && $self->pid_running($start_pid) ) { + $self->pretty_print( "Failed to Stop", "red" ); + return 1; + } + $self->pretty_print( "Stopped" ); +} + sub do_restart { my ( $self ) = @_; $self->read_pid; @@ -1019,6 +1069,25 @@ stopping the daemon. Default signals are C, C, C and C (yes, C is tried twice). +=head2 plugins + +A string or an arrayref of Daemon::Control plugins to use. Each +entry can be in the form of a fully qualified namespace prepended by a +C<+>, or a string assumed to be in the C +namespace. For example: + + Daemon::Control->with_plugins('HotStandby')->new(...); + +will load the L plugin + + Daemon::Control->with_plugins( '+My::Plugin')->new(...); + +will load the C< My::Plugin > plugin + + Daemon::Control->with_plugins(plugins => [ qw/+My::Plugin HotStandby/ ]->new(...); + +will load both. + =head1 METHODS =head2 run_command @@ -1112,6 +1181,60 @@ An accessor for the PID. Set by read_pid, or when the program is started. A function to dump the LSB compatible init script. Used by do_get_init_file. +=head1 FAQ + +=head2 LOGGING TO SYSLOG + +Logging a daemon::control script to syslog can be a little involved. +If you're using Log4perl or similar, consider using +L and/or L. An alternative +approach using a fifo is as follows: + +First, set up the stderr_file and stdout_file to a fifo. + + Daemon::Control->new({ + ..., # normal setup + stderr_file => "/var/log/myuser/myservice.fifo", + stdout_file => "/var/log/myuser/myservice.fifo", + ..., })->run; + +However you need a service running that reads from the fifo, in this +case logger(1). When your main service (that writes to the fifos) exits +this close is read by C< logger > and causes it to exit. In order to avoid +that we created a service that respawns logger when it dies. This +example is for a redhat system running upstart: + +The following script can be dropped into /etc/init as fifo-logger.conf +And then started with C< initctl start fifo-logger >. + + # fifo-logger - logger process for fcgi + # + # Will respawn the logger process as it exits on file close + + start on stopped rc RUNLEVEL=[345] + + stop on starting shutdown + + console output + respawn + + script + echo $$ > /var/run/fifo-logger.pid + exec logger -f /var/log/myuser/myservice.fifo -t myservice -p local0.notice + end script + + pre-start script + if [ ! -e /var/log/myuser/myservice.fifo ]; then + mkfifo /var/log/myuser/myservice.fifo + fi + chown hiive.hiive /var/log/myuser/myservice.fifo + chmod 660 /var/log/myuser/myservice.fifo + end script + +From this point, all the output will be sent to syslog as local0.notice and can +then be routed/cycled a needed without requiring a restart of the application. + + =head1 AUTHOR =over 4 @@ -1132,6 +1255,10 @@ Kaitlyn Parkhurst (SymKat) Isymkat@symkat.comE> ( Blog: Lavar@cpan.orgE> +=item * Kieren Diment Izarquon@cpan.org> + +=item * Mark Curtis Imark.curtis@affinitylive.com> + =back =head2 SPONSORS diff --git a/lib/Daemon/Control/Plugin/HotStandby.pm b/lib/Daemon/Control/Plugin/HotStandby.pm new file mode 100644 index 0000000..27bbba2 --- /dev/null +++ b/lib/Daemon/Control/Plugin/HotStandby.pm @@ -0,0 +1,56 @@ +package Daemon::Control::Plugin::HotStandby; +use Role::Tiny; + +=head2 NAME + +Daemon::Control::Plugin::HotStandby + +=head2 DESCRIPTION + +This is a plugin basically for PSGI workers so that a standby worker +can be spun up prior to terminating the original worker. + +=head2 AUTHOR + +Kieren Diment + +=cut + + +around do_restart => sub { + my $orig = shift; + my ($self) = @_; + + # check old running + $self->read_pid; + my $old_pid = $self->pid; + if ($self->pid && $self->pid_running) { + $self->pretty_print("Found existing process"); + } + else { # warn if not + $self->pretty_print("No process running for hot standby zero downtime", "red"); + } + + + $self->_finish_start; + # Start new get pid. + $self->read_pid; + my $new_pid = $self->pid; + # check new came up. Die if failed. + sleep (($self->kill_timeout * 2) + 1); + + + return 1 unless $old_pid > 1; + if ( $self->pid_running($old_pid) ) { + my $failed = $self->_send_stop_signals($old_pid); + return 1 if $failed; + } else { + $self->pretty_print( "Not Running", "red" ); + } + + $self->_check_stop_outcome($old_pid); + $self->_ensure_pid_file_exists; + return 0; +}; + +1; diff --git a/t/08-null_plugin.t b/t/08-null_plugin.t new file mode 100644 index 0000000..337a218 --- /dev/null +++ b/t/08-null_plugin.t @@ -0,0 +1,22 @@ +#!/usr/bin/env perl +use warnings; +use strict; +BEGIN { + use Test::More; + eval 'use Role::Tiny'; + plan skip_all => 'Role::Tiny not installed' if $@; +} + +package Daemon::Control::Plugin::Null; +use Role::Tiny; +1; + +use Daemon::Control; + +for ('Null', '+Daemon::Control::Plugin::Null') { + my $dc = Daemon::Control->with_plugins($_)->new(); + Test::More::ok(Role::Tiny::does_role($dc, 'Daemon::Control::Plugin::Null'), + "Plugin role is appplied"); +} +Test::More::done_testing; + diff --git a/t/10-hot_standby.t b/t/10-hot_standby.t new file mode 100644 index 0000000..59a3431 --- /dev/null +++ b/t/10-hot_standby.t @@ -0,0 +1,75 @@ +#!/usr/bin/perl +use warnings; +use strict; +BEGIN { + use Test::More; + eval 'use Role::Tiny'; + plan skip_all => 'Role::Tiny not installed' if $@; +} + +use File::Temp qw/tempfile/; +my ($pid_fh, $fn) = tempfile(); +$ENV{DC_TEST_TEMP_FILE} = $fn; + +my ( $file, $ilib ); + +# Let's make it so people can test in t/ or in the dist directory. +if ( -f 't/bin/10-hot_standby.pl' ) { # Dist Directory. + $file = "t/bin/10-hot_standby.pl"; + $ilib = "lib"; +} elsif ( -f 'bin/10-hot_standby.pl' ) { + $file = "bin/10-hot_standby.pl"; + $ilib = "../lib"; +} else { + die "Tests should be run in the dist directory or t/"; +} + + +sub current_pid { + seek $pid_fh, 0, 0; + my $pid = <$pid_fh>; + chomp $pid; + return $pid; +} + +sub get_command_output { + my ( @command ) = @_; + open my $lf, "-|", @command + or die "Couldn't get pipe to '@command': $!"; + my $content = do { local $/; <$lf> }; + close $lf; + return $content; +} + +my $out; + +ok $out = get_command_output( "$^X -I$ilib $file start" ), "Started system daemon"; +like $out, qr/\[Started\]/, "Daemon started."; +ok $out = get_command_output( "$^X -I$ilib $file status" ), "Get status of system daemon."; +like $out, qr/\[Running\]/, "Daemon running."; +ok $? >> 8 == 0, "Exit Status = 0"; +sleep 10; +ok $out = get_command_output( "$^X -I$ilib $file stop" ), "Stop daemon and get status."; +ok $out = get_command_output( "$^X -I$ilib $file status" ), "Get status of system daemon."; +like $out, qr/\[Not Running\]/, "Daemon not running."; +ok $? >> 8 == 3, "Exit Status = 3"; + +# Testing restart. +ok $out = get_command_output( "$^X -I$ilib $file start" ), "Started system daemon"; +like $out, qr/\[Started\]/, "Daemon started for restarting"; +ok $out = get_command_output( "$^X -I$ilib $file status" ), "Get status of system daemon."; +like $out, qr/\[Running\]/, "Daemon running for restarting."; +my $start_pid = current_pid(); +ok $out = get_command_output( "$^X -I$ilib $file restart" ), "Get status of system daemon."; +like $out, qr/\[Found existing.*\[Started\]/ms, "Daemon restarted."; +my $next_pid = current_pid(); +ok $out = get_command_output( "$^X -I$ilib $file status" ), "Get status of system daemon."; + +# not sure how to check that $start_pid is still alive for a period of time before being killed at this stage. +isnt $start_pid, $next_pid, "pid file contents swapped"; + +like $out, qr/\[Running\]/, "Daemon running after restart."; +ok $out = get_command_output( "$^X -I$ilib $file stop" ), "Get status of system daemon."; +like $out, qr/\[Stopped\]/, "Daemon stopped after restart."; + +done_testing; diff --git a/t/bin/10-hot_standby.pl b/t/bin/10-hot_standby.pl new file mode 100644 index 0000000..4e70162 --- /dev/null +++ b/t/bin/10-hot_standby.pl @@ -0,0 +1,22 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Daemon::Control; + +my ($path) = $0 =~ m{(.*/)}; +my $script = $path . '10-hot_standby_daemon.sh'; +Daemon::Control->with_plugins('HotStandby')->new({ + name => "My Daemon", + lsb_start => '$syslog $remote_fs', + lsb_stop => '$syslog', + lsb_sdesc => 'My Daemon Short', + lsb_desc => 'My Daemon controls the My Daemon daemon.', + path => '/usr/sbin/mydaemon/init.pl', + program => $script, + pid_file => $ENV{DC_TEST_TEMP_FILE} || '/tmp/daemon_control_manual_test_pid', + stderr_file => '/tmp/test_hot_standby_err', + stdout_file => '/tmp/test_hot_standby_out', + + fork => 2, + +})->run; diff --git a/t/bin/10-hot_standby_daemon.sh b/t/bin/10-hot_standby_daemon.sh new file mode 100755 index 0000000..44a712b --- /dev/null +++ b/t/bin/10-hot_standby_daemon.sh @@ -0,0 +1,2 @@ +#!/bin/sh +while true; do echo $$; sleep 1; done