perlsignalsmojoliciousmojolicious-lite

how to manage myself SIGINT and SIGTERM signals?


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:

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;

Solution

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