how to manage myself SIGINT and SIGTERM signals?

1.9k Views Asked by At

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 config
  • SIGINT Ctrl-C
  • SIGQUIT Ctrl-\
  • SIGABRT e.g abnormal library termination
  • SIGTERM external termination request - "friendly" kill (by opposition of brutal kill -9
  • TSTP suspend with Ctrl-Z
  • CONT 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;
1

There are 1 best solutions below

0
On BEST ANSWER

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;
    }
});