Scenario
For a certain code-base it’s decided it would be useful to be able to trigger an exception on-demand (mid-way through a process working).
Why?
Perhaps we have a process which loops over a repeated task which takes a reasonable amount of time, and we know that from time to time the input data to this process might need to be quickly updated and we’d like to abort whatever the current loop is doing to read in the latest data.
Meanwhile for another process we’d simply like some diagnostic information (which might be slightly expensive to generate) to be available on demand.
We might also have a standard application design whereby all otherwise unhandled exceptions will be captured at the last moment (before causing a process to fail) and signal sysadmins with emails or management systems with failure states (rather than simply aborting a process with a kill and having to communicate that this has been done).
Signals are helpful here, and we even have SIGUSR1 and SIGUSR2 (see “Standard Signals” in your handy signal(7) man page) allocated for user-defined purposes. Rather than having to build some other signalling method with our application, we can use one of those to trigger an exception by setting up a signal handler.
Core / Shared Code – Putting the Signal Handler everywhere
We define a set of exceptions (more would exist for a full application) and include a special one for SIGUSR1:
$ cat MyAppException.pm # Define exceptions for our application # Base Exception package MyAppException; use Moose; use namespace::autoclean; with 'Throwable'; has msg => ( is => 'ro' ); __PACKAGE__->meta->make_immutable; ### Exception for SIGUSR1 package MyAppException::SIGUSR1; use Moose; use namespace::autoclean; extends 'MyAppException'; __PACKAGE__->meta->make_immutable; # Successful module import: 1;
And then we define a SIGUSR1 signal handler in a core-library (shared across all code in the application) so that this behaviour is common everywhere:
$ cat MyAppCoreLib.pm use Modern::Perl; use MyAppException; # # This MyAppCoreLib module is used across almost all of our application # code and so changes here are available (most) everywhere. # package MyAppCoreLib; use namespace::autoclean; # We've decided that (unless we decide to do something else with SIGUSR1 # that we want to generate a consistent exception. Include some information # in the exception msg in case this would be useful in a log/email/etc. $SIG{USR1} = sub { MyAppException::SIGUSR1->throw({ msg => "Process: $0; PID: $$;" }); } # # ... other core code #
Specifically handling the exception
In some application we might handle exactly the SIGUSR1 (defining some known case – a custom user for the signal, but no extra code is needed to capture that signal):
$ cat handled.pl #!/usr/bin/env perl # This application will take some time - and there was a # requirement to notify another system from time to time # (on demand) which we can use the SIGUSR1 exception # for. use Modern::Perl; use MyAppCoreLib; say "Starting application: $0"; while (1) { eval { say "Processing ..."; sleep 5; }; if ( $@ ) { if ( $@->isa('MyAppException::SIGUSR1') ) { say "Received USR1 [" . $@->msg . "]: notifying subsystem now ... [DONE]"; } else { die $@; } } }
This code outputs (after running and then receiving a SIGUSR1):
bjdean@bob:~/files/wip/blog/perl-signal-exceptions$ ./handled.pl Starting application: ./handled.pl Processing ... Processing ... Received USR1 [Process: ./handled.pl; PID: 28113;]: notifying subsystem now ... [DONE] Processing ... Received USR1 [Process: ./handled.pl; PID: 28113;]: notifying subsystem now ... [DONE] Processing ... ...
Using exception hierarchy – handing any exception
In this case the code will capture the top-level exception class – with this code:
$ cat handle-any.pl #!/usr/bin/env perl # This application will take some time - and there was a # requirement to notify another system from time to time # (on demand) which we can use the SIGUSR1 exception # for. use Modern::Perl; use MyAppCoreLib; say "Starting application: $0"; while (1) { eval { say "Processing ..."; sleep 5; }; if ( $@ ) { if ( $@->isa('MyAppException') ) { say "Received some exception [$@]: resetting the loop"; } else { die $@; } } }
Output example:
$ ./handle-any.pl Starting application: ./handle-any.pl Processing ... Processing ... Processing ... Received some exception [MyAppException::SIGUSR1=HASH(0xbc6f00)]: resetting the loop Processing ... Received some exception [MyAppException::SIGUSR1=HASH(0xbc6e28)]: resetting the loop Processing ... Processing ... ^C
Not handled case
In the case of not capturing exceptions, the SIGUSR1 will result in a process aborting:
$ cat not-handled.pl #!/usr/bin/env perl # This application will take some time - and has not # got any special handling for the exception so a SIGUSR1 # will abort the process. use Modern::Perl; use MyAppCoreLib; say "Starting application: $0"; while (1) { say "Processing ..."; sleep 5; }
Output example:
$ ./not-handled.pl Starting application: ./not-handled.pl Processing ... Processing ... Processing ... MyAppException::SIGUSR1=HASH(0x1e11f00) $