File Coverage

blib/lib/Mojo/Server/DaemonControl.pm
Criterion Covered Total %
statement 50 204 24.5
branch 6 64 9.3
condition 9 41 21.9
subroutine 13 31 41.9
pod 5 5 100.0
total 83 345 24.0


line stmt bran cond sub pod time code
1             package Mojo::Server::DaemonControl;
2 7     7   3620673 use Mojo::Base 'Mojo::EventEmitter', -signatures;
  7         73  
  7         51  
3              
4 7     7   33839 use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
  7         21  
  7         382  
5 7     7   42 use File::Spec;
  7         15  
  7         163  
6 7     7   3561 use IO::Select;
  7         11977  
  7         367  
7 7     7   56 use Mojo::File qw(curfile path);
  7         27  
  7         357  
8 7     7   3595 use Mojo::Server::Daemon;
  7         820999  
  7         82  
9 7     7   354 use Mojo::Util qw(steady_time);
  7         19  
  7         371  
10 7     7   3940 use Mojolicious;
  7         622343  
  7         90  
11 7     7   386 use POSIX qw(WNOHANG);
  7         18  
  7         71  
12              
13             our $VERSION = '0.05';
14              
15             # This should be considered internal for now
16             our $MOJODCTL = do {
17             my $x = $0 =~ m!\bmojodctl$! && -x $0 ? $0 : $ENV{MOJODCTL_BINARY};
18             $x ||= curfile->dirname->dirname->dirname->dirname->child(qw(script mojodctl));
19             -x $x ? $x : 'mojodctl';
20             };
21              
22             has cleanup => 0;
23             has graceful_timeout => 120;
24             has heartbeat_interval => 5;
25             has heartbeat_timeout => 50;
26             has listen => sub ($self) { [split /,/, $ENV{MOJO_LISTEN} || 'http://*:8080'] };
27             has log => sub ($self) { $self->_build_log };
28             has pid_file => sub ($self) { path(File::Spec->tmpdir, path($0)->basename . '.pid') };
29             has workers => 4;
30              
31             has _listen_handles => sub ($self) {
32             my $daemon = Mojo::Server::Daemon->new(listen => $self->listen, silent => 1);
33             my $loop = $daemon->start->ioloop;
34             my @fh = map { $loop->acceptor($_)->handle } @{$daemon->acceptors};
35             die "Did you forget to specify listen addresses?" unless @fh;
36             return \@fh;
37             };
38              
39 2     2 1 3494 sub check_pid ($self) {
  2         5  
  2         3  
40 2 100 66     8 return 0 unless my $pid = -r $self->pid_file && $self->pid_file->slurp;
41 1         146 chomp $pid;
42 1 50 33     39 return $pid if $pid && kill 0, $pid;
43 0         0 $self->pid_file->remove;
44 0         0 return 0;
45             }
46              
47 1     1 1 3 sub ensure_pid_file ($self, $pid) {
  1         2  
  1         3  
  1         2  
48 1 50       4 return $self if -s (my $file = $self->pid_file);
49 1         42 $self->log->info("Writing pid $pid to $file");
50 1   33     72 return $file->spurt("$pid\n")->chmod(0644) && $self;
51             }
52              
53 0     0 1 0 sub reload ($self, $app) {
  0         0  
  0         0  
  0         0  
54 0 0       0 return _errno(3) unless my $pid = $self->check_pid;
55 0         0 $self->log->info("Starting hot deployment of $pid");
56 0 0       0 return kill(USR2 => $pid) ? _errno(0) : _errno(1);
57             }
58              
59 0     0 1 0 sub run ($self, $app) {
  0         0  
  0         0  
  0         0  
60              
61             # Cannot run two daemons at the same time
62 0 0       0 if (my $pid = $self->check_pid) {
63 0         0 $self->log->info("Manager for $app is already running ($pid)");
64 0         0 return _errno(16);
65             }
66              
67             # Listen for signals and start the daemon
68 0     0   0 local $SIG{CHLD} = sub { $self->_waitpid };
  0         0  
69 0     0   0 local $SIG{INT} = sub { $self->stop('INT') };
  0         0  
70 0     0   0 local $SIG{QUIT} = sub { $self->stop('QUIT') };
  0         0  
71 0     0   0 local $SIG{TERM} = sub { $self->stop('TERM') };
  0         0  
72 0     0   0 local $SIG{TTIN} = sub { $self->workers($self->workers + 1) };
  0         0  
73 0 0   0   0 local $SIG{TTOU} = sub { $self->workers($self->workers - 1) if $self->workers > 0 };
  0         0  
74 0     0   0 local $SIG{USR2} = sub { $self->_start_hot_deployment };
  0         0  
75              
76 0         0 $self->cleanup(1)->ensure_pid_file($self->{pid} = $$);
77 0         0 $self->_create_worker_read_write_pipe;
78 0         0 $self->_listen_handles;
79              
80 0         0 @$self{qw(pid pool running)} = ($$, {}, 1);
81 0         0 $self->emit('start');
82 0         0 $self->log->info("Manager for $app started");
83 0         0 $self->_manage($app) while $self->{running};
84 0         0 $self->log->info("Manager for $app stopped");
85 0         0 $self->cleanup(0)->pid_file->remove;
86 0         0 return _errno(0);
87             }
88              
89 0     0 1 0 sub stop ($self, $signal = 'TERM') {
  0         0  
  0         0  
  0         0  
90 0         0 $self->{stop_signal} = $signal;
91 0         0 $self->log->info("Manager will stop workers with signal $signal");
92 0         0 return $self->emit(stop => $signal);
93             }
94              
95 1     1   3 sub _build_log ($self) {
  1         2  
  1         2  
96             my $level = $ENV{MOJO_LOG_LEVEL}
97 1   33     11 || ($ENV{HARNESS_IS_VERBOSE} ? 'debug' : $ENV{HARNESS_ACTIVE} ? 'error' : 'info');
98 1         11 return Mojo::Log->new(level => $level);
99             }
100              
101 0     0   0 sub _create_worker_read_write_pipe ($self) {
  0         0  
  0         0  
102 0 0       0 return if $self->{worker_read};
103 0 0       0 pipe $self->{worker_read}, $self->{worker_write} or die "pipe: $!";
104             }
105              
106 0     0   0 sub _errno ($n) { $! = $n }
  0         0  
  0         0  
  0         0  
107              
108 0     0   0 sub _kill ($self, $signal, $w, $level, $reason) {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
109 0 0       0 return if $w->{$signal};
110 0   0     0 $w->{$signal} = kill($signal, $w->{pid}) || 0;
111 0         0 $self->log->$level("$reason (signalled=$w->{$signal})");
112             }
113              
114 0     0   0 sub _manage ($self, $app) {
  0         0  
  0         0  
  0         0  
115              
116             # Make sure we have a PID file and get status from workers
117 0         0 $self->ensure_pid_file($self->{pid});
118 0         0 $self->_read_heartbeat;
119              
120             # Stop workers and eventually manager
121 0         0 my $pool = $self->{pool};
122 0 0       0 if (my $signal = $self->{stop_signal}) {
123 0 0       0 return delete @$self{qw(running stop_signal)} unless keys %$pool; # Fully stopped
124 0   0     0 return map { $_->{$signal} ||= kill $signal, $_->{pid} } values %$pool;
  0         0  
125             }
126              
127             # Decrease workers on SIGTTOU
128 0         0 my $time = steady_time;
129 0         0 my @maybe_stop = grep { !$_->{graceful} } values %$pool;
  0         0  
130 0         0 $_->{graceful} = $time for splice @maybe_stop, $self->workers;
131              
132             # Figure out worker health
133 0         0 my $ht = $self->heartbeat_timeout;
134 0         0 my (@graceful, @healthy, @starting);
135 0         0 for my $pid (sort keys %$pool) {
136 0 0       0 my $w = $pool->{$pid} or next;
137 0 0       0 if ($w->{graceful}) { push @graceful, $pid }
  0 0       0  
    0          
138 0         0 elsif (!$w->{time}) { push @starting, $pid }
139 0   0     0 elsif ($w->{time} + $ht <= $time) { $w->{graceful} //= $time; push @graceful, $pid }
  0         0  
140 0         0 else { push @healthy, $pid }
141             }
142              
143             # Start or stop workers based on worker health
144 0         0 my $n_missing = $self->workers - (@healthy + @starting);
145 0 0       0 if ($n_missing > 0) {
    0          
146 0         0 $self->log->info("Manager starting $n_missing workers");
147 0   0     0 $self->_spawn($app) while !$self->{stop_signal} && $n_missing-- > 0;
148             }
149             elsif (!@starting) {
150 0         0 my $gt = $self->graceful_timeout;
151 0         0 for my $pid (@graceful) {
152 0 0       0 next unless my $w = $pool->{$pid};
153 0 0 0     0 if ($gt && $w->{graceful} + $gt < $time) {
154 0         0 $self->_kill(KILL => $w, warn => "Stopping worker $pid immediately");
155             }
156             else {
157 0         0 $self->_kill(QUIT => $w, info => "Stopping worker $pid gracefully ($gt seconds)");
158             }
159             }
160             }
161             }
162              
163 0     0   0 sub _read_heartbeat ($self) {
  0         0  
  0         0  
164 0   0     0 my $select = $self->{select} ||= IO::Select->new($self->{worker_read});
165 0 0       0 return unless $select->can_read(0.1);
166 0 0       0 return unless $self->{worker_read}->sysread(my $chunk, 4194304);
167              
168 0         0 my $time = steady_time;
169 0         0 while ($chunk =~ s/mojodctl:\d+:(\d+):(\w)\n//mg) {
170 0 0       0 next unless my $w = $self->{pool}{$1};
171 0 0       0 ($w->{killed} = $time), $self->log->fatal("Worker $w->{pid} force killed") if $2 eq 'k';
172 0 0 0     0 $w->{graceful} ||= $time if $2 eq 'g';
173 0         0 $w->{time} = $time;
174 0         0 $self->emit(heartbeat => $w);
175             }
176             }
177              
178 0     0   0 sub _spawn ($self, $app) {
  0         0  
  0         0  
  0         0  
179              
180             # Parent
181 0 0       0 die "Can't fork: $!" unless defined(my $pid = fork);
182 0 0       0 if ($pid) {
183 0         0 my $w = $self->{pool}{$pid} = {pid => $pid, time => steady_time};
184 0         0 $self->log->info("Worker $pid started");
185 0         0 return $self->emit(spawn => $w);
186             }
187              
188             # Child
189 0   0     0 $ENV{MOJO_LOG_LEVEL} ||= $self->log->level;
190 0         0 $ENV{MOJODCTL_CONTROL_CLASS} = 'Mojo::Server::DaemonControl::Worker';
191 0         0 $ENV{MOJODCTL_HEARTBEAT_FD} = fileno $self->{worker_write};
192 0         0 $ENV{MOJODCTL_HEARTBEAT_INTERVAL} = $self->heartbeat_interval;
193              
194 0         0 my (@urls, @args) = @{$self->listen};
  0         0  
195 0         0 for my $fh ($self->{worker_write}, @{$self->_listen_handles}) {
  0         0  
196              
197             # Remove close-on-exec flag
198             # https://stackoverflow.com/questions/14351147/perl-passing-an-open-socket-across-fork-exec
199 0 0       0 my $flags = fcntl $fh, F_GETFD, 0 or die "fcntl F_GETFD: $!";
200 0 0       0 fcntl $fh, F_SETFD, $flags & ~FD_CLOEXEC or die "fcntl F_SETFD: $!";
201              
202 0 0       0 next if $fh eq $self->{worker_write};
203 0         0 my $url = Mojo::URL->new(shift @urls);
204 0         0 $url->query->param(fd => fileno $fh);
205 0         0 push @args, -l => $url->to_string;
206             }
207              
208 0         0 $self->log->debug("Starting $^X $MOJODCTL $app daemon @args");
209 0         0 exec $^X, $MOJODCTL => $app => daemon => @args;
210 0         0 die "Could not exec $app: $!";
211             }
212              
213 0     0   0 sub _start_hot_deployment ($self) {
  0         0  
  0         0  
214 0         0 $self->log->info('Starting hot deployment.');
215 0         0 my $time = steady_time;
216 0         0 $_->{graceful} = $time for values %{$self->{pool}};
  0         0  
217             }
218              
219 0     0   0 sub _waitpid ($self) {
  0         0  
  0         0  
220 0         0 while ((my $pid = waitpid -1, WNOHANG) > 0) {
221 0 0       0 next unless my $w = delete $self->{pool}{$pid};
222 0         0 $self->log->info("Worker $pid stopped");
223 0         0 $self->emit(reap => $w);
224             }
225             }
226              
227 2     2   389 sub DESTROY ($self) {
  2         3  
  2         4  
228 2   66     7 my $path = $self->cleanup && $self->pid_file;
229 2 100 66     29 $path->remove if $path and -e $path;
230             }
231              
232             1;
233              
234             =encoding utf8
235              
236             =head1 NAME
237              
238             Mojo::Server::DaemonControl - A Mojolicious daemon manager
239              
240             =head1 SYNOPSIS
241              
242             =head2 Commmand line
243              
244             # Start the manager
245             $ mojodctl -l 'http://*:8080' -P /tmp/myapp.pid -w 4 /path/to/myapp.pl;
246              
247             # Reload the manager
248             $ mojodctl -R -P /tmp/myapp.pid /path/to/myapp.pl;
249              
250             # For more options
251             $ mojodctl --help
252              
253             =head2 Perl API
254              
255             use Mojo::Server::DaemonControl;
256             my $dctl = Mojo::Server::DaemonControl->new(listen => ['http://*:8080'], workers => 4);
257             $dctl->run('/path/to/my-mojo-app.pl');
258              
259             =head2 Mojolicious application
260              
261             It is possible to use the L hook to change
262             server settings. The C<$app> is also available, meaning the values can be read
263             from a config file. See L and
264             L for more information about what to tweak.
265              
266             use Mojolicious::Lite -signatures;
267              
268             app->hook(before_server_start => sub ($server, $app) {
269             if ($sever->isa('Mojo::Server::DaemonControl::Worker')) {
270             $server->inactivity_timeout(60);
271             $server->max_clients(100);
272             $server->max_requests(10);
273             }
274             });
275              
276             =head1 DESCRIPTION
277              
278             L is not a web server. Instead it manages one or
279             more L processes that can handle web requests.
280              
281             This server is an alternative to L where each of the
282             workers handle long running (WebSocket) requests. The main difference is that a
283             hot deploy will simply start new workers, instead of restarting the manager.
284             This is useful if you need/want to deploy a new version of your server during
285             the L. Normally this is not something you would need, but in
286             some cases where the graceful timeout and long running requests last for
287             several hours or even days, then it might come in handy to let the old
288             code run, while new processes are deployed.
289              
290             Note that L is currently EXPERIMENTAL and it has
291             not been tested in production yet. Feedback is more than welcome.
292              
293             =head1 SIGNALS
294              
295             =head2 INT, TERM
296              
297             Shut down server immediately.
298              
299             =head2 QUIT
300              
301             Shut down server gracefully.
302              
303             =head2 TTIN
304              
305             Increase worker pool by one.
306              
307             =head2 TTOU
308              
309             Decrease worker pool by one.
310              
311             =head2 USR2
312              
313             Will prevent existing workers from accepting new connections and eventually
314             stop them, and start new workers in a fresh environment that handles the new
315             connections. The manager process will remain the same.
316              
317             $ mojodctl
318             |- myapp.pl-1647405707
319             |- myapp.pl-1647405707
320             |- myapp.pl-1647405707
321             |- myapp.pl
322             |- myapp.pl
323             '- myapp.pl
324              
325             EXPERIMENTAL: The workers that waits to be stopped will have a timestamp
326             appended to C<$0> to illustrate which is new and which is old.
327              
328             =head1 ATTRIBUTES
329              
330             L inherits all attributes from
331             L and implements the following ones.
332              
333             =head2 cleanup
334              
335             $bool = $dctl->cleanup;
336             $dctl = $dctl->cleanup(1);
337              
338             Set this to true and L will remove L
339             when this object goes out of scope.
340              
341             =head2 graceful_timeout
342              
343             $timeout = $dctl->graceful_timeout;
344             $dctl = $dctl->graceful_timeout(120);
345              
346             A worker will be forced stopped if it could not be gracefully stopped after
347             this amount of time.
348              
349             =head2 heartbeat_interval
350              
351             $num = $dctl->heartbeat_interval;
352             $dctl = $dctl->heartbeat_interval(5);
353              
354             Heartbeat interval in seconds. This value is passed on to
355             L.
356              
357             =head2 heartbeat_timeout
358              
359             $num = $dctl->heartbeat_timeout;
360             $dctl = $dctl->heartbeat_timeout(120);
361              
362             A worker will be stopped gracefully if a heartbeat has not been seen within
363             this amount of time.
364              
365             =head2 listen
366              
367             $array_ref = $dctl->listen;
368             $dctl = $dctl->listen(['http://127.0.0.1:3000']);
369              
370             Array reference with one or more locations to listen on.
371             See L for more details.
372              
373             =head2 log
374              
375             $log = $dctl->log;
376             $dctl = $dctl->log(Mojo::Log->new);
377              
378             A L object used for logging.
379              
380             =head2 pid_file
381              
382             $file = $dctl->pid_file;
383             $dctl = $dctl->pid_file(Mojo::File->new);
384              
385             A L object with the path to the pid file.
386              
387             Note that the PID file must end with ".pid"! Default path is "mojodctl.pid" in
388             L.
389              
390             =head2 workers
391              
392             $int = $dctl->workers;
393             $dctl = $dctl->workers(4);
394              
395             Number of worker processes, defaults to 4. See L
396             for more details.
397              
398             =head1 METHODS
399              
400             L inherits all methods from
401             L and implements the following ones.
402              
403             =head2 check_pid
404              
405             $int = $dctl->check_pid;
406              
407             Returns the PID of the running process documented in L or zero (0)
408             if it is not running.
409              
410             =head2 ensure_pid_file
411              
412             $dctl->ensure_pid_file($pid);
413              
414             Makes sure L exists and contains the current PID.
415              
416             =head2 reload
417              
418             $int = $dctl->reload($app);
419              
420             Tries to reload a running instance by sending L to L.
421              
422             =head2 run
423              
424             $int = $dctl->run($app);
425              
426             Run the menager and wait for L. Note that C<$app> is not loaded in
427             the manager process, which means that each worker does not share any code or
428             memory.
429              
430             =head2 stop
431              
432             $dctl->stop($signal);
433              
434             Used to stop the running manager and any L with the C<$signal> INT,
435             QUIT or TERM (default).
436              
437             =head1 AUTHOR
438              
439             Jan Henning Thorsen
440              
441             =head1 COPYRIGHT AND LICENSE
442              
443             Copyright (C) Jan Henning Thorsen
444              
445             This program is free software, you can redistribute it and/or modify it under
446             the terms of the Artistic License version 2.0.
447              
448             =head1 SEE ALSO
449              
450             L, L,
451             L.
452              
453             =cut