The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

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 an ALRM signal is sent, the BLOCK described by ALRM will be called to handle the signal. If ALRM is not defined, the normal mechanisms of handling $SIG{ALRM} will be employed. Mutually exclusive of retry.

Accepts blocks: ALRM, finally; and trailing modifier timeout => INT.

Note: that try_once is essentially a trival case of retry with tries => 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 an ALRM signal is sent and the number of tries has not been exhausted, the retry BLOCK will be tried again. This continues until an ALRM signal is not triggered or if the number of $Try::ALRM::TRIES has been reached.

Accepts blocks: ALRM, finally; and trailing modifiers timeout => INT, and tries => INT.

retry makes values available to each BLOCK that is called via @_, see description of each BLOCK below for more details. This also applies to the BLOCK provided for retry.

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 no ALRM BLOCK is defined and $SIG{ALRM} is not a assigned a CODE ref to handle an ALRM signal, then not including the ALRM 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 if ALRM had been invoked;

...
finally {
  my ($attempts, $succeedful) = @_; 
};

When used with try_once, @_ is empty. Note that try_once is essentially a trival case of retry with tries => 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 modifier timeout => INT that is supported via try_once and retry.

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 attempts retry will make before giving up. This can be temporarily overridden using the trailing modifier tries => INT that is supported via retry.

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 an ALRM 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 using alarm 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 via die; 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 using local to set $Try::ALRM::TIMEOUT. The reason for this is so that timeout may continue to function properly as a getter inside of the try_once block.

tries => INT

Sets the number of attempts made by a retry block. Impacts the value of Try::ALRM::TIMEOUT locally for each retry block. See code examples in this document to see what a retry block with tries => 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.