File Coverage

blib/lib/Mojo/Server/DaemonControl.pm
Criterion Covered Total %
statement 50 203 24.6
branch 6 64 9.3
condition 9 41 21.9
subroutine 13 31 41.9
pod 5 5 100.0
total 83 344 24.1


line stmt bran cond sub pod time code
1             package Mojo::Server::DaemonControl;
2 7     7   3645127 use Mojo::Base 'Mojo::EventEmitter', -signatures;
  7         74  
  7         48  
3              
4 7     7   33768 use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
  7         21  
  7         421  
5 7     7   56 use File::Spec;
  7         16  
  7         173  
6 7     7   3512 use IO::Select;
  7         11867  
  7         367  
7 7     7   55 use Mojo::File qw(curfile path);
  7         16  
  7         356  
8 7     7   3590 use Mojo::Server::Daemon;
  7         814223  
  7         71  
9 7     7   388 use Mojo::Util qw(steady_time);
  7         17  
  7         372  
10 7     7   4140 use Mojolicious;
  7         621686  
  7         83  
11 7     7   383 use POSIX qw(WNOHANG);
  7         17  
  7         66  
12              
13             our $VERSION = '0.04';
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 3743 sub check_pid ($self) {
  2         5  
  2         2  
40 2 100 66     7 return 0 unless my $pid = -r $self->pid_file && $self->pid_file->slurp;
41 1         151 chomp $pid;
42 1 50 33     33 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         2  
  1         4  
48 1 50       4 return $self if -s (my $file = $self->pid_file);
49 1         44 $self->log->info("Writing pid $pid to $file");
50 1   33     103 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   2 sub _build_log ($self) {
  1         2  
  1         2  
96             my $level = $ENV{MOJO_LOG_LEVEL}
97 1   33     10 || ($ENV{HARNESS_IS_VERBOSE} ? 'debug' : $ENV{HARNESS_ACTIVE} ? 'error' : 'info');
98 1         12 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 @args;
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('http://127.0.0.1');
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   380 sub DESTROY ($self) {
  2         4  
  2         3  
228 2   66     8 my $path = $self->cleanup && $self->pid_file;
229 2 100 66     32 $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 ENVIRONMENT VARIABLES
294              
295             Some environment variables can be set in C service files, while other
296             can be useful to be read when initializing your web server.
297              
298             =head2 MOJODCTL_CONTROL_CLASS
299              
300             This environment variable will be set to L
301             inside the worker process.
302              
303             =head1 SIGNALS
304              
305             =head2 INT, TERM
306              
307             Shut down server immediately.
308              
309             =head2 QUIT
310              
311             Shut down server gracefully.
312              
313             =head2 TTIN
314              
315             Increase worker pool by one.
316              
317             =head2 TTOU
318              
319             Decrease worker pool by one.
320              
321             =head2 USR2
322              
323             Will prevent existing workers from accepting new connections and eventually
324             stop them, and start new workers in a fresh environment that handles the new
325             connections. The manager process will remain the same.
326              
327             $ mojodctl
328             |- myapp.pl-1647405707
329             |- myapp.pl-1647405707
330             |- myapp.pl-1647405707
331             |- myapp.pl
332             |- myapp.pl
333             '- myapp.pl
334              
335             EXPERIMENTAL: The workers that waits to be stopped will have a timestamp
336             appended to C<$0> to illustrate which is new and which is old.
337              
338             =head1 ATTRIBUTES
339              
340             L inherits all attributes from
341             L and implements the following ones.
342              
343             =head2 graceful_timeout
344              
345             $timeout = $dctl->graceful_timeout;
346             $dctl = $dctl->graceful_timeout(120);
347              
348             A worker will be forced stopped if it could not be gracefully stopped after
349             this amount of time.
350              
351             =head2 heartbeat_interval
352              
353             $num = $dctl->heartbeat_interval;
354             $dctl = $dctl->heartbeat_interval(5);
355              
356             Heartbeat interval in seconds. This value is passed on to
357             L.
358              
359             =head2 heartbeat_timeout
360              
361             $num = $dctl->heartbeat_timeout;
362             $dctl = $dctl->heartbeat_timeout(120);
363              
364             A worker will be stopped gracefully if a heartbeat has not been seen within
365             this amount of time.
366              
367             =head2 listen
368              
369             $array_ref = $dctl->listen;
370             $dctl = $dctl->listen(['http://127.0.0.1:3000']);
371              
372             Array reference with one or more locations to listen on.
373             See L for more details.
374              
375             =head2 log
376              
377             $log = $dctl->log;
378             $dctl = $dctl->log(Mojo::Log->new);
379              
380             A L object used for logging.
381              
382             =head2 pid_file
383              
384             $file = $dctl->pid_file;
385             $dctl = $dctl->pid_file(Mojo::File->new);
386              
387             A L object with the path to the pid file.
388              
389             Note that the PID file must end with ".pid"! Default path is "mojodctl.pid" in
390             L.
391              
392             =head2 workers
393              
394             $int = $dctl->workers;
395             $dctl = $dctl->workers(4);
396              
397             Number of worker processes, defaults to 4. See L
398             for more details.
399              
400             =head1 METHODS
401              
402             L inherits all methods from
403             L and implements the following ones.
404              
405             =head2 check_pid
406              
407             $int = $dctl->check_pid;
408              
409             Returns the PID of the running process documented in L or zero (0)
410             if it is not running.
411              
412             =head2 ensure_pid_file
413              
414             $dctl->ensure_pid_file($pid);
415              
416             Makes sure L exists and contains the current PID.
417              
418             =head2 reload
419              
420             $int = $dctl->reload($app);
421              
422             Tries to reload a running instance by sending L to L.
423              
424             =head2 run
425              
426             $int = $dctl->run($app);
427              
428             Run the menager and wait for L. Note that C<$app> is not loaded in
429             the manager process, which means that each worker does not share any code or
430             memory.
431              
432             =head2 stop
433              
434             $dctl->stop($signal);
435              
436             Used to stop the running manager and any L with the C<$signal> INT,
437             QUIT or TERM (default).
438              
439             =head1 AUTHOR
440              
441             Jan Henning Thorsen
442              
443             =head1 COPYRIGHT AND LICENSE
444              
445             Copyright (C) Jan Henning Thorsen
446              
447             This program is free software, you can redistribute it and/or modify it under
448             the terms of the Artistic License version 2.0.
449              
450             =head1 SEE ALSO
451              
452             L, L,
453             L.
454              
455             =cut