File Coverage

blib/lib/Mojo/Server/DaemonControl.pm
Criterion Covered Total %
statement 138 227 60.7
branch 22 66 33.3
condition 22 53 41.5
subroutine 29 36 80.5
pod 5 5 100.0
total 216 387 55.8


line stmt bran cond sub pod time code
1             package Mojo::Server::DaemonControl;
2 9     9   3610442 use Mojo::Base 'Mojo::EventEmitter', -signatures;
  9         97  
  9         51  
3              
4 9     9   32464 use File::Basename qw(basename);
  9         21  
  9         370  
5 9     9   42 use File::Spec::Functions qw(tmpdir);
  9         14  
  9         308  
6 9     9   3548 use IO::Select;
  9         11029  
  9         385  
7 9     9   1045 use IO::Socket::UNIX;
  9         20213  
  9         74  
8 9     9   4073 use Mojo::File qw(curfile path);
  9         20  
  9         431  
9 9     9   3553 use Mojo::Log;
  9         88998  
  9         91  
10 9     9   4111 use Mojo::URL;
  9         54750  
  9         134  
11 9     9   328 use Mojo::Util qw(steady_time);
  9         15  
  9         366  
12 9     9   43 use POSIX qw(WNOHANG);
  9         16  
  9         72  
13 9     9   4263 use Scalar::Util qw(weaken);
  9         16  
  9         23013  
14              
15             our $VERSION = '0.03';
16              
17             # This should be considered internal for now
18             our $MOJODCTL = do {
19             my $x = $0 =~ m!\bmojodctl$! && -x $0 ? $0 : $ENV{MOJODCTL_BINARY};
20             $x ||= curfile->dirname->dirname->dirname->dirname->child(qw(script mojodctl));
21             -x $x ? $x : 'mojodctl';
22             };
23              
24             has graceful_timeout => sub ($self) { $ENV{MOJODCTL_GRACEFUL_TIMEOUT} || 120 };
25             has heartbeat_interval => sub ($self) { $ENV{MOJODCTL_HEARTBEAT_INTERVAL} || 5 };
26             has heartbeat_timeout => sub ($self) { $ENV{MOJODCTL_HEARTBEAT_TIMEOUT} || 50 };
27             has listen => sub ($self) { $self->_build_listen };
28             has log => sub ($self) { $self->_build_log };
29             has pid_file => sub ($self) { $self->_build_pid_file };
30             has workers => sub ($self) { $ENV{MOJODCTL_WORKERS} || 4 };
31             has worker_pipe => sub ($self) { $self->_build_worker_pipe };
32              
33 16     16 1 6937 sub check_pid ($self) {
  16         19  
  16         19  
34 16 100 100     40 return 0 unless my $pid = -r $self->pid_file && $self->pid_file->slurp;
35 1         141 chomp $pid;
36 1 50 33     28 return $pid if $pid && kill 0, $pid;
37 0         0 $self->pid_file->remove;
38 0         0 return 0;
39             }
40              
41 1     1 1 2 sub ensure_pid_file ($self) {
  1         2  
  1         2  
42 1   33     5 my $pid = $self->{pid} ||= $$;
43 1 50       3 return $self if -s (my $file = $self->pid_file);
44 1         31 $self->log->debug("Writing pid $pid to @{[$self->pid_file]}");
  1         3  
45 1   33     27 return $file->spurt("$pid\n")->chmod(0644) && $self;
46             }
47              
48 0     0 1 0 sub reload ($self, $app) {
  0         0  
  0         0  
  0         0  
49 0 0       0 return _errno(3) unless my $pid = $self->check_pid;
50 0         0 $self->log->info("Starting hot deployment of $pid.");
51 0 0       0 return kill(USR2 => $pid) ? _errno(0) : _errno(1);
52             }
53              
54 14     14 1 22560 sub run ($self, $app) {
  14         20  
  14         14  
  14         16  
55 14 50       26 if (my $pid = $self->check_pid) {
56 0         0 $self->log->info("Manager for $app is already running ($pid).");
57 0         0 return _errno(16);
58             }
59              
60 14         770 weaken $self;
61 14     1   200 local $SIG{CHLD} = sub { $self->_waitpid };
  1         345494  
62 14     2   156 local $SIG{INT} = sub { $self->stop('INT') };
  2         152  
63 14     2   150 local $SIG{QUIT} = sub { $self->stop('QUIT') };
  2         92  
64 14     8   172 local $SIG{TERM} = sub { $self->stop('TERM') };
  8         178  
65 14     4   166 local $SIG{TTIN} = sub { $self->_inc_workers(1) };
  4         132  
66 14     24   150 local $SIG{TTOU} = sub { $self->_inc_workers(-1) };
  24         468  
67 14     0   154 local $SIG{USR2} = sub { $self->_hot_deploy };
  0         0  
68              
69 14   100     64 $self->{pool} ||= {};
70 14         42 @$self{qw(pid running)} = ($$, 1);
71 14         48 $self->worker_pipe; # Make sure we have a working pipe
72 14         1352 $self->emit('start');
73 13         471 $self->log->info("Manager for $app started");
74 13         131 $self->_manage($app) while $self->{running};
75 13         32 $self->log->info("Manager for $app stopped");
76 13         96 return _errno(0);
77             }
78              
79 12     12 1 14 sub stop ($self, $signal = 'TERM') {
  12         14  
  12         14  
  12         12  
80 12         22 $self->{stop_signal} = $signal;
81 12         28 $self->log->info("Manager will stop workers with signal $signal");
82 12         124 return $self->emit(stop => $signal);
83             }
84              
85 2     2   3 sub _build_listen ($self) {
  2         2  
  2         9  
86 2   100     21 return [map { Mojo::URL->new($_) } split ',', $ENV{MOJODCTL_LISTEN} || 'http://*:8080'];
  3         225  
87             }
88              
89 7     7   13 sub _build_log ($self) {
  7         12  
  7         9  
90             $ENV{MOJODCTL_LOG_LEVEL}
91 7 50 66     71 ||= $ENV{HARNESS_IS_VERBOSE} ? 'debug' : $ENV{HARNESS_ACTIVE} ? 'error' : 'info';
    50          
92 7   66     49 $ENV{MOJO_LOG_LEVEL} ||= $ENV{MOJODCTL_LOG_LEVEL};
93 7         75 my $log = Mojo::Log->new(level => $ENV{MOJODCTL_LOG_LEVEL});
94 7 100       206 $log->path($ENV{MOJODCTL_LOG_FILE}) if $ENV{MOJODCTL_LOG_FILE};
95 7         62 return $log;
96             }
97              
98 8     8   13 sub _build_pid_file ($self) {
  8         11  
  8         9  
99 8 100       26 return path($ENV{MOJODCTL_PID_FILE}) if $ENV{MOJODCTL_PID_FILE};
100 7         23 return path(tmpdir, basename($0) . '.pid');
101             }
102              
103 6     6   8 sub _build_worker_pipe ($self) {
  6         6  
  6         6  
104 6         14 my $path = $self->pid_file->to_string =~ s!\.pid$!.sock!r;
105 6 50       84 die qq(PID file "@{[$self->pid_file]}" must end with ".pid") unless $path =~ m!\.sock$!;
  0         0  
106 6 50       104 path($path)->remove if -S $path;
107 6   50     54 return IO::Socket::UNIX->new(Listen => 1, Local => $path, Type => SOCK_DGRAM)
108             || die "Can't create a worker pipe: $@";
109             }
110              
111 13     13   16 sub _errno ($n) { $! = $n }
  13         28  
  13         20  
  13         671  
112              
113 0     0   0 sub _hot_deploy ($self) {
  0         0  
  0         0  
114 0         0 $self->log->info('Starting hot deployment.');
115 0         0 my $time = steady_time;
116 0         0 $_->{graceful} = $time for values %{$self->{pool}};
  0         0  
117             }
118              
119 28     28   44 sub _inc_workers ($self, $by) {
  28         36  
  28         30  
  28         28  
120 28         42 my $workers = $self->workers + $by;
121 28 100       108 $workers = 1 if $workers < 1;
122 28         54 $self->workers($workers);
123              
124 28         120 my $time = steady_time;
125 28         126 my @stop = grep { !$_->{graceful} } values %{$self->{pool}};
  144         208  
  28         58  
126 28         46 splice @stop, 0, $workers;
127 28         180 $_->{graceful} = $time for @stop;
128             }
129              
130 0     0   0 sub _kill ($self, $signal, $w, $reason = "with $signal") {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
131 0 0       0 return if $w->{$signal};
132 0   0     0 $w->{$signal} = kill($signal => $w->{pid}) // 0;
133 0         0 $self->log->info("Stopping worker $w->{pid} $reason == $w->{$signal}");
134             }
135              
136 0     0   0 sub _manage ($self, $app) {
  0         0  
  0         0  
  0         0  
137 0         0 $self->_read_heartbeat;
138              
139             # Stop workers and eventually manager
140 0         0 my $pool = $self->{pool};
141 0 0       0 if (my $signal = $self->{stop_signal}) {
142 0 0       0 return delete @$self{qw(running stop_signal)} unless keys %$pool; # Fully stopped
143 0 0       0 return map { $_->{$signal} || $self->_kill($signal => $_) } values %{$self->{pool}};
  0         0  
  0         0  
144             }
145              
146             # Make sure we have enough workers and a pid file
147 0         0 my $need = $self->workers - int grep { !$_->{graceful} } values %$pool;
  0         0  
148 0 0       0 $self->log->debug("Manager starting $need workers") if $need > 0;
149 0   0     0 $self->_spawn($app) while !$self->{stop_signal} && $need-- > 0;
150 0         0 $self->ensure_pid_file;
151              
152             # Keep track of worker health
153 0         0 my $gt = $self->graceful_timeout;
154 0         0 my $ht = $self->heartbeat_timeout;
155 0         0 my $time = steady_time;
156 0         0 for my $pid (keys %$pool) {
157 0 0       0 next unless my $w = $pool->{$pid};
158              
159 0 0 0     0 if (!$w->{graceful} and $w->{time} + $ht <= $time) {
160 0         0 $w->{graceful} = $time;
161 0         0 $self->log->error("Worker $pid has no heartbeat");
162             }
163              
164 0 0 0     0 if ($gt and $w->{graceful} and $w->{graceful} + $gt < $time) {
    0 0        
165 0         0 $self->_kill(KILL => $w, 'with no heartbeat');
166             }
167             elsif ($w->{graceful}) {
168 0         0 $self->_kill(QUIT => $w, 'gracefully');
169             }
170             }
171             }
172              
173 0     0   0 sub _read_heartbeat ($self) {
  0         0  
  0         0  
174 0   0     0 my $select = $self->{select} ||= IO::Select->new($self->worker_pipe);
175 0 0       0 return unless $select->can_read(0.1);
176 0 0       0 return unless $self->worker_pipe->sysread(my $chunk, 4194304);
177              
178 0         0 my $time = steady_time;
179 0         0 while ($chunk =~ /(\d+):(\w)\n/g) {
180 0 0       0 next unless my $w = $self->{pool}{$1};
181 0 0       0 ($w->{killed} = $time), $self->log->fatal("Worker $w->{pid} forced killed") if $2 eq 'k';
182 0 0 0     0 $w->{graceful} ||= $time if $2 eq 'g';
183 0         0 $w->{time} = $time;
184 0         0 $self->emit(heartbeat => $w);
185             }
186             }
187              
188 0     0   0 sub _spawn ($self, $app) {
  0         0  
  0         0  
  0         0  
189 0         0 my @args;
190             push @args, map {
191 0         0 my $url = $_->clone;
192 0         0 $url->query->param(reuse => 1);
193 0         0 (-l => $url->to_string);
194 0         0 } @{$self->listen};
  0         0  
195              
196             # Parent
197 0         0 my $ppid = $$;
198 0 0       0 die "Can't fork: $!" unless defined(my $pid = fork);
199 0 0       0 return $self->emit(spawn => $self->{pool}{$pid} = {pid => $pid, time => steady_time}) if $pid;
200              
201             # Child
202 0         0 $ENV{MOJODCTL_HEARTBEAT_INTERVAL} = $self->heartbeat_interval;
203 0         0 $ENV{MOJODCTL_CONTROL_CLASS} = 'Mojo::Server::DaemonControl::Worker';
204 0         0 $ENV{MOJODCTL_CONTROL_SOCK} = $self->worker_pipe->hostpath;
205 0         0 $ENV{MOJODCTL_PID} = $ppid;
206 0         0 $self->log->debug("Exec $^X $MOJODCTL $app daemon @args");
207 0         0 exec $^X, $MOJODCTL => $app => daemon => @args;
208 0         0 die "Could not exec $app: $!";
209             }
210              
211 1     1   14 sub _waitpid ($self) {
  1         6  
  1         11  
212 1         74 while ((my $pid = waitpid -1, WNOHANG) > 0) {
213 1 50       20 next unless my $w = delete $self->{pool}{$pid};
214 1         19 $self->log->debug("Worker $pid stopped");
215 1         34 $self->emit(reap => $w);
216             }
217             }
218              
219 9     9   6774 sub DESTROY ($self) {
  9         16  
  9         11  
220 9 100 100     348 return if $self->{pid} and $self->{pid} != $$; # Fork safety
221 8         24 my $path = $self->pid_file;
222 8 100 66     67 $path->remove if $path and -e $path;
223              
224 8         288 my $worker_pipe = $self->{worker_pipe};
225 8 100 66     102 path($worker_pipe->hostpath)->remove if $worker_pipe and -S $worker_pipe->hostpath;
226             }
227              
228             1;
229              
230             =encoding utf8
231              
232             =head1 NAME
233              
234             Mojo::Server::DaemonControl - A Mojolicious daemon manager
235              
236             =head1 SYNOPSIS
237              
238             =head2 Commmand line
239              
240             # Start the manager
241             $ mojodctl -l 'http://*:8080' -P /tmp/myapp.pid -w 4 /path/to/myapp.pl;
242              
243             # Reload the manager
244             $ mojodctl -R -P /tmp/myapp.pid /path/to/myapp.pl;
245              
246             # For more options
247             $ mojodctl --help
248              
249             =head2 Perl API
250              
251             use Mojo::Server::DaemonControl;
252             my $listen = Mojo::URL->new('http://*:8080');
253             my $dctl = Mojo::Server::DaemonControl->new(listen => [$listen], workers => 4);
254              
255             $dctl->run('/path/to/my-mojo-app.pl');
256              
257             =head2 Mojolicious application
258              
259             It is possible to use the L hook to change
260             server settings. The C<$app> is also available, meaning the values can be read
261             from a config file. See L and
262             L for more information about what to tweak.
263              
264             use Mojolicious::Lite -signatures;
265              
266             app->hook(before_server_start => sub ($server, $app) {
267             if ($sever->isa('Mojo::Server::DaemonControl::Worker')) {
268             $server->inactivity_timeout(60);
269             $server->max_clients(100);
270             $server->max_requests(10);
271             }
272             });
273              
274             =head1 DESCRIPTION
275              
276             L is not a web server. Instead it manages one or
277             more L processes that can handle web requests. Each of
278             these servers are started with L
279             enabled.
280              
281             This means it is only supported on systems that support
282             L. It also does not support fork
283             emulation. It should work on most modern Linux based systems though.
284              
285             This server is an alternative to L where each of the
286             workers handle long running (WebSocket) requests. The main difference is that a
287             hot deploy will simply start new workers, instead of restarting the manager.
288             This is useful if you need/want to deploy a new version of your server during
289             the L. Normally this is not something you would need, but in
290             some cases where the graceful timeout and long running requests last for
291             several hours or even days, then it might come in handy to let the old
292             code run, while new processes are deployed.
293              
294             Note that L is currently EXPERIMENTAL and it has
295             not been tested in production yet. Feedback is more than welcome.
296              
297             =head1 ENVIRONMENT VARIABLES
298              
299             Some environment variables can be set in C service files, while other
300             can be useful to be read when initializing your web server.
301              
302             =head2 MOJODCTL_CONTROL_CLASS
303              
304             This environment variable will be set to L
305             inside the worker process.
306              
307             =head2 MOJODCTL_GRACEFUL_TIMEOUT
308              
309             Can be used to set the default value for L.
310              
311             =head2 MOJODCTL_HEARTBEAT_INTERVAL
312              
313             Can be used to set the default value for L and will be set
314             to ensure a default value for L.
315              
316             =head2 MOJODCTL_HEARTBEAT_TIMEOUT
317              
318             Can be used to set the default value for L.
319              
320             =head2 MOJODCTL_LISTEN
321              
322             Can be used to set the default value for L. The environment variable
323             will be split on comma for multiple listen addresses.
324              
325             =head2 MOJODCTL_LOG_FILE
326              
327             By default the log will be written to STDERR. It is possible to set this
328             environment variable to log to a file instead.
329              
330             =head2 MOJODCTL_LOG_LEVEL
331              
332             Can be set to debug, info, warn, error, fatal. Default log level is "info".
333              
334             =head2 MOJODCTL_PID_FILE
335              
336             Can be used to set a default value for L.
337              
338             =head2 MOJODCTL_WORKERS
339              
340             Can be used to set a default value for L.
341              
342             =head1 SIGNALS
343              
344             =head2 INT, TERM
345              
346             Shut down server immediately.
347              
348             =head2 QUIT
349              
350             Shut down server gracefully.
351              
352             =head2 TTIN
353              
354             Increase worker pool by one.
355              
356             =head2 TTOU
357              
358             Decrease worker pool by one.
359              
360             =head2 USR2
361              
362             Will prevent existing workers from accepting new connections and eventually
363             stop them, and start new workers in a fresh environment that handles the new
364             connections. The manager process will remain the same.
365              
366             $ mojodctl
367             |- myapp.pl-1647405707
368             |- myapp.pl-1647405707
369             |- myapp.pl-1647405707
370             |- myapp.pl
371             |- myapp.pl
372             '- myapp.pl
373              
374             EXPERIMENTAL: The workers that waits to be stopped will have a timestamp
375             appended to C<$0> to illustrate which is new and which is old.
376              
377             =head1 ATTRIBUTES
378              
379             L inherits all attributes from
380             L and implements the following ones.
381              
382             =head2 graceful_timeout
383              
384             $timeout = $dctl->graceful_timeout;
385             $dctl = $dctl->graceful_timeout(120);
386              
387             A worker will be forced stopped if it could not be gracefully stopped after
388             this amount of time.
389              
390             =head2 heartbeat_interval
391              
392             $num = $dctl->heartbeat_interval;
393             $dctl = $dctl->heartbeat_interval(5);
394              
395             Heartbeat interval in seconds. This value is passed on to
396             L.
397              
398             =head2 heartbeat_timeout
399              
400             $num = $dctl->heartbeat_timeout;
401             $dctl = $dctl->heartbeat_timeout(120);
402              
403             A worker will be stopped gracefully if a heartbeat has not been seen within
404             this amount of time.
405              
406             =head2 listen
407              
408             $array_ref = $dctl->listen;
409             $dctl = $dctl->listen([Mojo::URL->new]);
410              
411             An array-ref of L objects for what to listen to. See
412             L for supported values.
413              
414             The C query parameter will be added automatically before starting the
415             L sub process.
416              
417             =head2 log
418              
419             $log = $dctl->log;
420             $dctl = $dctl->log(Mojo::Log->new);
421              
422             A L object used for logging.
423              
424             =head2 pid_file
425              
426             $file = $dctl->pid_file;
427             $dctl = $dctl->pid_file(Mojo::File->new);
428              
429             A L object with the path to the pid file.
430              
431             Note that the PID file must end with ".pid"! Default path is "mojodctl.pid" in
432             L.
433              
434             =head2 workers
435              
436             $int = $dctl->workers;
437             $dctl = $dctl->workers(4);
438              
439             Number of worker processes, defaults to 4. See L
440             for more details.
441              
442             =head2 worker_pipe
443              
444             $socket = $dctl->worker_pipe;
445              
446             Holds a L object used to communicate with workers.
447              
448             =head1 METHODS
449              
450             L inherits all methods from
451             L and implements the following ones.
452              
453             =head2 check_pid
454              
455             $int = $dctl->check_pid;
456              
457             Returns the PID of the running process documented in L or zero (0)
458             if it is not running.
459              
460             =head2 ensure_pid_file
461              
462             $dctl->ensure_pid_file;
463              
464             Makes sure L exists and contains the current PID.
465              
466             =head2 reload
467              
468             $int = $dctl->reload($app);
469              
470             Tries to reload a running instance by sending L to L.
471              
472             =head2 run
473              
474             $int = $dctl->run($app);
475              
476             Run the menager and wait for L. Note that C<$app> is not loaded in
477             the manager process, which means that each worker does not share any code or
478             memory.
479              
480             =head2 stop
481              
482             $dctl->stop($signal);
483              
484             Used to stop the running manager and any L with the C<$signal> INT,
485             QUIT or TERM (default).
486              
487             =head1 AUTHOR
488              
489             Jan Henning Thorsen
490              
491             =head1 COPYRIGHT AND LICENSE
492              
493             Copyright (C) Jan Henning Thorsen
494              
495             This program is free software, you can redistribute it and/or modify it under
496             the terms of the Artistic License version 2.0.
497              
498             =head1 SEE ALSO
499              
500             L, L,
501             L.
502              
503             =cut