NAME
Try::ALRM - Provides try/catch-like semantics for an ALRM thrown by CORE::alarm
SYNOPSIS
The primary method in this module is meant to be retry
,
retry {
my ($attempts) = @_; # @_ is populated as described in this line
printf qq{Attempt %d/%d ... \n}, $attempts, tries;
sleep 5;
}
ALRM {
my ($attempts) = @_; # @_ is populated as described in this line
printf qq{\tTIMED OUT};
if ( $attempts < tries ) {
printf qq{ - Retrying ...\n};
}
else {
printf qq{ - Giving up ...\n};
}
}
finally {
my ( $attempts, $successful ) = @_; # Note: @_ is populated as described in this line when called with retry
my $tries = tries; # "what was the limit on number of tries?" Here it will be 4
my $timeout = timeout; # "what was the timeout allowed?" Here it will be 3
# test and handle ultimate outcome after attempts
if ($successful) {
# timeout did NOT occur after $attempts attempts
}
else {
# timeout DID occur after trying $tries times
}
} timeout => 3, tries => 4;
Which is equivalent to ... well, checkout the implementation of Try::ALRM::retry(&;@)
, because it is equivalent to that :-).
However, it should be pointed out that the module provides a method called, try_once
, that is a reduced case of retry
where tries => 1
. There might be benefits to using retry
instead, but the code might not ready very clearly with the workd retry. Originally, there was a method called try
, but because this might conflict with a more popular module that exports a try
keyword, the decision was made to use try_once
. It's not pretty, but it's clear.
use Try::ALRM;
try_once {
my ($attempts) = @_; # @_ is populated as described in this line
print qq{ doing something that might timeout ...\n};
sleep 6;
}
ALRM {
my ($attempts) = @_; # @_ is populated as described in this line
print qq{ Wake Up!!!!\n};
}
finally {
my ( $attempts, $successful ) = @_; # Note: @_ is populated as described in this line when called with retry
my $tries = tries; # "what was the limit on number of tries?" Here it will be 4
my $timeout = timeout; # "what was the timeout allowed?" Here it will be 3
# test and handle ultimate outcome after attempts
if ($successful) {
# timeout did NOT occur after $attempts attempts
}
else {
# timeout DID occur after trying $tries times
}
} timeout => 1;
Which is essentially equivalent to just,
local $SIG{ALRM} = sub { print qq{ Wake Up!!!!\n} };
alarm 1;
print qq{ doing something that might timeout ...\n};
sleep 6;
alarm 0; # reset alarm, end of 'try' block implies this "reset"
DESCRIPTION
Try::ALRM
provides try/catch-like semantics for handling code being guarded by alarm
. Because it's localized and probably expected, ALRM
signals can be treated as exceptions.
alarm
is extremely useful, but it can be cumbersome do add in code. The goal of this module is to make it more idiomatic, and therefore more accessible. It also allows for the ALRM
signal itself to be treated more semantically as an exception. Which makes it a more natural to write and read in Perl.
Internally, the keywords are implemented as prototypes and uses the same sort of coersion of a lexical bloc to a subroutine reference that is used in Try::Tiny.
EXPORTS
This module exports 6 methods:
NOTE: Try::ALRM::try_once
and Try::ALRM::retry
are mutually exclusive, but one of them is required to invoke any benefits of using this module.
try_once BLOCK
-
Meant to be used instead of
Try::ARLM::retry
.Primary BLOCK, attempted once with a timeout set by
$Try::ALRM::TIMEOUT
. If anALRM
signal is sent, the BLOCK described byALRM
will be called to handle the signal. IfALRM
is not defined, the normal mechanisms of handling$SIG{ALRM}
will be employed. Mutually exclusive ofretry
.Accepts blocks:
ALRM
,finally
; and trailing modifiertimeout => INT
.Note: that
try_once
is essentially a trival case ofretry
withtries => 1
; and in the future it may just become a wrapper around this case. For now it is its own independant implementation. retry BLOCK
-
Meant to be the primary method, not to be used with
Try::ARLM::try_once
.Primary BLOCK, attempted
$Try::ALRM::TRIES
number of times with a timeout governed by$Try::ALRM::TIMEOUT
. If anALRM
signal is sent and the number oftries
has not been exhausted, theretry
BLOCK will be tried again. This continues until anALRM
signal is not triggered or if the number of$Try::ALRM::TRIES
has been reached.Accepts blocks:
ALRM
,finally
; and trailing modifierstimeout => INT
, andtries => INT
.retry
makes values available to eachBLOCK
that is called via@_
, see description of each BLOCK below for more details. This also applies to the BLOCK provided forretry
.NB:
BLOCK is treated as a
CODE
block internally, and is passed a single value that defines what number attempt, please see the examples; all of which contain lines such as,my $attempts = shift; ...
ALRM BLOCK
-
Optional.
Called when an
ALRM
signal is detected. If noALRM
BLOCK is defined and$SIG{ALRM}
is not a assigned aCODE
ref to handle an ALRM signal, then not including theALRM
block ends up being a no-op in most cases.When called with
retry
,@_
contains the number of attempts that have been made so far.retry { ... } ALRM { my ($attempts) = @_; };
NB:
BLOCK is treated as a
CODE
block internally, and is passed a single value that defines what number attempt, please see the examples; all of which contain lines such as,my $attempts = shift; ...
finally BLOCK
-
Optional.
This BLOCK is called unconditionally. When called with
try_once
,@_
contains an indication there being a timeout or not in the attempted block.When called with
retry
,@_
also contains the number of attempts that have been made before the attempts ceased. There is also a value that is passed that indicates ifALRM
had been invoked;... finally { my ($attempts, $succeedful) = @_; };
When used with
try_once
,@_
is empty. Note thattry_once
is essentially a trival case ofretry
withtries => 1
; and in the future it may just become a wrapper around this case.BLOCK is treated as a
CODE
block internally, and is passed a single value that defines what number attempt, please see the examples; all of which contain lines such as,my ($attempts, $successful) = @_; ...
timeout INT
-
Setter/getter for
$Try::ALRM::TIMEOUT
, which governs the default timeout in number of seconds. This can be temporarily overridden using the trailing modifiertimeout => INT
that is supported viatry_once
andretry
.timeout 10; # sets $Try::ALRM::TIMEOUT to 10 try_once { ... } ALRM { my ($attempts) = @_; };
Can be overridden by trailing modifier,
timeout => INT
.The default value is in the code, but at the time of this writing it is set to 60 seconds.
tries INT
-
Setter/getter for
$Try::ALRM::TRIES
, which governs the number of attemptsretry
will make before giving up. This can be temporarily overridden using the trailing modifiertries => INT
that is supported viaretry
.timeout 10; # sets $Try::ALRM::TIMEOUT to 10 tries 12; # sets $Try:::ALRM::TRIES to 12 retry { ... } ALRM { my ($attempts) = @_; };
Can be overridden by trailing modifier,
tries => INT
.The default value is in the code, but at the time of this writing it is set to 3 attempts.
PACKAGE ENVIRONMENT
This module exposes $Try::ALRM::TIMEOUT
and $TRY::ALRM::TRIES
as package variables; they can be modified in traditional ways. The module also provides different ways to set these at both script or package scope (using the timeout
and tries
setters, respectively), and at a local execution scope (using trailing modifiers.
USAGE
Try::ALRM doesn't really have options, it's more of a structure. So this section is meant to descript that structure and ways to control it.
try_once
-
This familiar idiom include the block of code that may run longer than one wishes and is need of an
alarm
signal.# default timeout is $Try::ALRM::TIMEOUT try { this_subroutine_call_may_timeout(); };
If just
try_once
is used here, what happens is functionall equivalent to:alarm 60; # e.g., the default value of $Try::ALRM::TIMEOUT this_subroutine_call_may_timeout(); alarm 0;
And the default handler for
$SIG{ALRM}
is invoked if anALRM
is ssued. retry
-
# default timeout is $Try::ALRM::TIMEOUT # default number of tries is $Try::ALRM::TRIES retry { this_subroutine_call_may_timeout_and_we_want_to_retry(); };
ALRM
-
This keyword is for setting
$SIG{ALRM}
with the block that gets passed to it; e.g.:# default timeout is $Try::ALRM::TIMEOUT try { this_subroutine_call_may_timeout(); } ALRM { print qq{ Alarm Clock!!!!\n}; };
The addition of the
ALRM
block above is functionally equivalent to the typical idiom of usingalarm
and setting$SIG{ALRM}
,local $SIG{ALRM} = sub { print qq{ Alarm Clock!!!!\n} }; alarm 60; # e.g., the default value of $Try::ALRM::TIMEOUT this_subroutine_call_may_timeout(); alarm 0;
So while this module present
alarm
with try/catch semantics, there are no actualy exceptions getting thrown viadie
; the traditional signal handling mechanism is being invoked as the exception handler.
TRAILING MODIFIERS
A side effect of using Perl prototypes to achieve the block structure of this module is that passing options is much more naturally done so as a comma delimited list of trailing key/value pairs at the end of the entire stucture.
As has been show in the previous examples, the modifiers are specifed as follows:
retry {
...
}
ALRM {
...
},
finally {
...
} timeout => 5, tries => 10;
#^^ Note, there is NO comma between the closing '}' and 'timeout'; this
# is due the implementation using a prototype that results in keyword syntax
# similar to grep or map, e.g., map { } key1 => $val1, key2 => $val2;
This style of providing modifiers to the behavior of the retry
/try_once
block is referred to here as trailing modifiers.
This module has two trailing modifiers that can be set.
timeout => INT
-
Due to limitations with the way Perl prototypes work for creating syntactical structures, the most idiomatic solution is to use a setter/getter function to update the package variable:
timeout 10; # changes $Try::ALRM::TIMEOUT to 10 try { this_subroutine_call_may_timeout(); } ALRM { print qq{ Alarm Clock!!!!\n}; };
If used without an input value,
timeout
returns the current value of$Try::ALRM::TIMEOUT
. - Trailing after the last BLOCK
-
try { this_subroutine_call_may_timeout(); } ALRM { print qq{ Alarm Clock!!!!\n}; } timeout => 10; # NB: applies temporarily!
This approach utilizes the effect of defining a Perl prototype,
&
, which coerces a lexical block into a subroutine reference (i.e.,CODE
). The key => value syntax was chosen as a compromise because it makes things a lot more clear and makes the implementation of the blocks a lot easier (use the source to see how, Luke).The timeout value passed to
alarm
internally is controlled with the package variable,$Try::ALRM::TIMEOUT
. So this module presents 2 different ways to control the value of this variable.The addition of this timeout affects $Try::ALRM::TIMEOUT for the duration of the
try_once
block, internally is usinglocal
to set$Try::ALRM::TIMEOUT
. The reason for this is so thattimeout
may continue to function properly as a getter inside of thetry_once
block. tries => INT
-
Sets the number of attempts made by a
retry
block. Impacts the value ofTry::ALRM::TIMEOUT
locally for eachretry
block. See code examples in this document to see what aretry
block withtries => INT
looks like.
try_once
/ALRM
/finally
Examples
Using the two methods above, the following code demonstrats the usage of timeout
and the effect of the trailing timeout value,
# set timeout (persists)
timeout 5;
printf qq{now %d seconds timeout\n}, timeout;
# try/ALRM
try {
printf qq{ doing something that might timeout before %d seconds are up ...\n}, timeout;
sleep 6;
}
ALRM {
print qq{Alarm Clock!!\n};
} timeout => 1; # <~ trailing timeout
# will still be 5 seconds
printf qq{now %d seconds timeout\n}, timeout;
The output of this block is,
default timeout is 60 seconds
timeout is set globally to 5 seconds
timeout is now set locally to 1 seconds
Alarm Clock!!
timeout is set globally to 5 seconds
Setting the Number of Tries
The number of total attempts made by retry
is controlled by the package variable, $Try::ALRM::TRIES
. And it provides similar controls to what is provided for controlling the timeout.
- Using the
tries
keyword will affect the package variable$Try::ALRM::TRIES
if passed an integer value. If passed nothing, the current value of$Try::ALRM::TRIES
will be returned - Trailing value after the last BLOCK
-
An example is best here,
retry { ... } timeout => 10, tries => 5;
Using the trailing values in this way allows the number of attempts to be temporarily set to the RHS value of
tries =>
.
Bugs
Very likey. This project was motivated by a couple of factors: learning more about Perl prototypes (which this author finds awesome) and seeing if ALRM
can be treated as a localized exception (turns out, it can!).
Milage May Vary, as they say. If found, please file issue on GH repo.
The module's purpose is essentially complete, and changes that are made will be strictly to fix bugs in the code or POD. Please report them, and I will find them eventually.
AUTHOR
oodler577
PERL ADVENT 2022
| \__ `\O/ `-- {} \} {/ {} \} {/ {} \}
\ \_(~)/_..___/=____/=____/=____/=____/=____/=____/=____/=*
\=======/ //\\ >\/> || \> //\\ >\/> || \> //\\ >\/>
----`---`--- `` `` ```` `` `` `` `` ```` `` `` ```` ````
ACKNOWLEDGEMENTS
"This module is dedicated to the least of you amongst us and to all of those who have died suddenly."
COPYRIGHT AND LICENSE
Copyright (C) 2022 by oodler577
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.30.0 or, at your option, any later version of Perl 5 you may have available.