I am working on a simple Mojolicious::Lite
based server that includes a websocket end point.
I would like to handle some termination signals to terminate gracefully the websocket connections and avoid exceptions in the clients (a java application).
I have tried to define my signal handlers like I am used to with my previous servers using HTTP::Daemon
.
The problem is that they seem to be ignored. Perhaps redefined in the Mojolicious layer, I did not found any reference on it yet.
I am expecting to see my termination message, but it does not happen
[Mon Mar 23 14:01:28 2020] [info] Listening at "http://*:3000"
Server available at http://127.0.0.1:3000
^C # <-- i want to see my signal received message here if type Ctrl-c
I am sending SIGINT
directly by entering Ctrl-C
when the server is in foreground in the terminal, and I can terminate gracefully the server (e.g. when started by a cron or other displayless mean) with a kill <pid>
.
In some previous servers I tried to be quite exaustive by handling:
HUP
hijacked signal used nowadays to reload configSIGINT
Ctrl-CSIGQUIT
Ctrl-\SIGABRT
e.g abnormal library terminationSIGTERM
external termination request - "friendly" kill
(by opposition of brutal kill -9
TSTP
suspend with Ctrl-ZCONT
when resuming from Ctrl-Z with fg
or bg
All these handlers allow to exit gracefully with cleaning resources, ensuring data consistency or reload configuration or data models after external change, depending on the program and the needs.
I have found the package Mojo::IOLoop::Signal
, « a Non-blocking signal handler » but it seems to be a different thing. Wrong?
Here is my simplified code (runs with a simple perl ws_store_test.pl daemon
):
File ws_store_test.pl
# Automatically enables "strict", "warnings", "utf8" and Perl 5.10 features
use Mojolicious::Lite;
my $store = {};
my $ws_clients = {};
sub terminate_clients {
for my $peer (keys %$ws_clients){
$ws_clients->{$peer}->finish;
}
}
$SIG{INT} = sub {
say "SIGINT"; # to be sure to display something
app->log->info("SIGINT / CTRL-C received. Leaving...");
terminate_clients;
};
$SIG{TERM} = sub {
say "SIGTERM"; # to be sure to display something
app->log->info("SIGTERM - External termination request. Leaving...");
terminate_clients;
};
# this simulates a change on datamodel and notifies the clients
sub update_store {
my $t = localtime time;
$store->{last_time} = $t;
for my $peer (keys %$ws_clients){
app->log->debug(sprintf 'notify %s', $peer);
$ws_clients->{$peer}->send({ json => $store
});
}
}
# Route with placeholder - to test datamodel contents
get '/:foo' => sub {
my $c = shift;
my $foo = $c->param('foo');
$store->{$foo}++;
$c->render(text => "Hello from $foo." . (scalar keys %$store ? " already received " . join ', ', sort keys %$store : "") );
};
# websocket service with optional parameter
websocket '/ws/tickets/*id' => { id => undef } => sub {
my $ws = shift;
my $id = $ws->param('id');
my $peer = sprintf '%s', $ws->tx;
app->log->debug(sprintf 'Client connected: %s, id=%s', $peer, $id);
$ws_clients->{$peer} = $ws->tx;
$store->{$id} = {};
$ws->on( message => sub {
my ($c, $message) = @_;
app->log->debug(sprintf 'WS received %s from a client', $message);
});
$ws->on( finish => sub {
my ($c, $code, $reason) = @_;
app->log->debug(sprintf 'WS client disconnected: %s - %d - %s', $peer, $code, $reason);
delete $ws_clients->{$peer};
});
};
plugin Cron => ( '* * * * *' => \&update_store );
# Start the Mojolicious command system
app->start;
SIGINT and SIGTERM handlers are redefined at the start of the server. In morbo this is:
local $SIG{INT} = local $SIG{TERM} = sub {
$self->{finished} = 1;
kill 'TERM', $self->{worker} if $self->{worker};
};
In Mojo::Server::Daemon this is:
local $SIG{INT} = local $SIG{TERM} = sub { $loop->stop };
If you redefine SIGINT/SIGTERM's handler yourself at the toplevel, those local
will override them. What I suggest instead, is to redefine them once in a before_dispatch
hook. For instance:
sub add_sigint_handler {
my $old_int = $SIG{INT};
$SIG{INT} = sub {
say "SIGINT"; # to be sure to display something
app->log->info("SIGINT / CTRL-C received. Leaving...");
terminate_clients;
$old_int->(); # Calling the old handler to cleanly exit the server
}
}
app->hook(before_dispatch => sub {
state $unused = add_sigint_handler();
});
Here I'm using state
to make sure that add_sigint_handler
is evaluated only once (since if it was evaluated more than once, $old_int
would not have the correct value after the first time). Another way of writing that could be:
my $flag = 0;
app->hook(before_dispatch => sub {
if ($flag == 0) {
add_sigint_handler();
$flag = 1;
}
});
Or,
app->hook(before_dispatch => sub {
state $flag = 0;
if ($flag == 0) {
add_sigint_handler();
$flag = 1;
}
});