Action-CircuitBreaker
view release on metacpan or search on metacpan
SYNOPSIS
# Will execute the code, as the circuit will be closed by default.
# OO interface
use Action::CircuitBreaker;
Action::CircuitBreaker->new()->run(sub { do_stuff; });
ATTRIBUTES
error_if_code
ro, CodeRef
The code to run to check if the error should count towards the circuit
breaker. It defaults to:
# Returns true if there were an exception evaluating to something true
sub { $_[0] }
It will be given these arguments:
* as first argument, a scalar which is the value of any exception
that were raised by the $attempt_code. Otherwise, undef.
attempt_result
It's a scalar, which is the result of $attempt_code. If $attempt_code
returned a list, then the scalar is the reference on this list.
attempt_parameters
It's the reference on the parameters that were given to
$attempt_code.
error_if_code return value will be interpreted as a boolean : true
return value means the execution of $attempt_code was a failure and
should count towards breaking the ciruit. False means it went well.
Here is an example of code that gets the arguments properly:
my $action = Action::CircuitBreaker->new(
error_if_code => sub {
my ($error, $h) = @_;
my $attempt_code_result = $h->{attempt_result};
my $attempt_code_params = $h->{attempt_parameters};
my @results = @$attempt_code_result;
# will contains (2, 4);
my @original_parameters = @$attempt_code_params;
# will contains (1, 2);
}
);
my @results = $action->run(sub { print @_; }, 1, 2);
on_failure_code
ro, CodeRef, optional
If given, will be executed when an execution fails.
It will be given the same arguments as error_if_code. See error_if_code
for their descriptions
on_circuit_open
ro, CodeRef, optional
If given, will be executed the circuit gets opened.
It will be given the same arguments as error_if_code. See error_if_code
for their descriptions
on_circuit_close
ro, CodeRef, optional
If given, will be executed the circuit gets closed again.
It will be given no arguments
METHODS
run
Does the following:
step 1
Tests the value of _circuit_open_until. If it is positive and the
current timestamp is before the value, an error is thrown, because
the circuit is still open. If the value is positive, but before the
current timestamp, the circuit is closed (by setting
_circuit_open_until to 0) and optionally, on_circuit_close is run.
step 2
If the value of _circuit_open_until is 0, the circuit is closed, and
the passed sub gets executed. Then it runs the error_if_code CodeRef
in scalar context, giving it as arguments $error, and the return
values of $attempt_code. If it returns true, we consider that it was
a failure, and move to step 3. Otherwise, we consider it means
success, and return the return values of $attempt_code.
step 3
Increase the value of _current_retries_number and check whether it is
larger than max_retries_number. If it is, then open the circuit by
setting _circuit_open_until to the current time plus open_time, and
optionally run on_circuit_open. Then, die with the $error from
$attempt_code.
step 4
Runs the on_failure_code CodeRef in the proper context, giving it as
arguments $error, and the return values of $attempt_code, and returns
the results back to the caller.
Arguments passed to run() will be passed to $attempt_code. They will
also passed to on_failure_code as well if the case arises.
SEE ALSO
This code is heavily based on Action::Retry.
AUTHOR
lib/Action/CircuitBreaker.pm view on Meta::CPAN
use Time::HiRes qw(gettimeofday);
use Carp;
use base 'Exporter';
our @EXPORT = ((caller())[1] eq '-e' ? @EXPORT_OK : ());
use Moo;
has error_if_code => (
is => 'ro',
required => 1,
isa => sub { ref $_[0] eq 'CODE' },
default => sub { sub { $_[0] }; },
);
has on_failure_code => (
is => 'ro',
isa => sub { ref $_[0] eq 'CODE' },
lib/Action/CircuitBreaker.pm view on Meta::CPAN
if (my $timestamp = $self->_circuit_open_until) {
# we can't execute until the timestamp has done
my ($seconds, $microseconds) = gettimeofday;
$seconds * 1000 + int($microseconds / 1000) >= $timestamp
or die 'The circuit is open and cannot be executed.';
$self->_circuit_open_until(0);
$self->has_on_circuit_close
and $self->on_circuit_close->();
}
my $error;
my @attempt_result;
my $attempt_result;
my $wantarray;
if (wantarray) {
$wantarray = 1;
@attempt_result = eval { $attempt_code->(@_) };
$error = $@;
} elsif ( ! defined wantarray ) {
eval { $attempt_code->(@_) };
$error = $@;
} else {
$attempt_result = eval { $attempt_code->(@_) };
$error = $@;
}
my $h = { action_retry => $self,
attempt_result => ( $wantarray ? \@attempt_result : $attempt_result ),
attempt_parameters => \@_,
};
if ($self->error_if_code->($error, $h)) {
$self->_current_retries_number($self->_current_retries_number + 1);
if ($self->_current_retries_number >= $self->max_retries_number) {
my ($seconds, $microseconds) = gettimeofday;
my $open_until = ($self->open_time * 1000) + ($seconds * 1000 + int($microseconds / 1000));
$self->_circuit_open_until($open_until);
$self->has_on_circuit_open
and $self->on_circuit_open->();
}
die $error;
} else {
return $h->{attempt_result};
}
}
1;
__END__
lib/Action/CircuitBreaker.pm view on Meta::CPAN
=head1 SYNOPSIS
# Will execute the code, as the circuit will be closed by default.
# OO interface
use Action::CircuitBreaker;
Action::CircuitBreaker->new()->run(sub { do_stuff; });
=head1 ATTRIBUTES
=head2 error_if_code
ro, CodeRef
The code to run to check if the error should count towards the circuit breaker. It defaults to:
# Returns true if there were an exception evaluating to something true
sub { $_[0] }
It will be given these arguments:
=over
=item *
lib/Action/CircuitBreaker.pm view on Meta::CPAN
It's a scalar, which is the result of C<$attempt_code>. If C<$attempt_code>
returned a list, then the scalar is the reference on this list.
=item attempt_parameters
It's the reference on the parameters that were given to C<$attempt_code>.
=back
C<error_if_code> return value will be interpreted as a boolean : true return
value means the execution of C<$attempt_code> was a failure and should count
towards breaking the ciruit. False means it went well.
Here is an example of code that gets the arguments properly:
my $action = Action::CircuitBreaker->new(
error_if_code => sub {
my ($error, $h) = @_;
my $attempt_code_result = $h->{attempt_result};
my $attempt_code_params = $h->{attempt_parameters};
my @results = @$attempt_code_result;
# will contains (2, 4);
my @original_parameters = @$attempt_code_params;
# will contains (1, 2);
}
);
my @results = $action->run(sub { print @_; }, 1, 2);
=head2 on_failure_code
ro, CodeRef, optional
If given, will be executed when an execution fails.
It will be given the same arguments as C<error_if_code>. See C<error_if_code> for their descriptions
=head2 on_circuit_open
ro, CodeRef, optional
If given, will be executed the circuit gets opened.
It will be given the same arguments as C<error_if_code>. See C<error_if_code> for their descriptions
=head2 on_circuit_close
ro, CodeRef, optional
If given, will be executed the circuit gets closed again.
It will be given no arguments
=head2 max_retries_number
lib/Action/CircuitBreaker.pm view on Meta::CPAN
=head2 run
Does the following:
=over
=item step 1
Tests the value of C<_circuit_open_until>. If it is positive and the current
timestamp is before the value, an error is thrown, because the circuit is
still open. If the value is positive, but before the current timestamp,
the circuit is closed (by setting C<_circuit_open_until> to 0) and optionally,
C<on_circuit_close> is run.
=item step 2
If the value of C<_circuit_open_until> is 0, the circuit is closed, and the
passed sub gets executed. Then it runs the C<error_if_code> CodeRef in
scalar context, giving it as arguments C<$error>, and the return values
of C<$attempt_code>. If it returns true, we consider that it was a failure,
and move to step 3. Otherwise, we consider it
means success, and return the return values of C<$attempt_code>.
=item step 3
Increase the value of C<_current_retries_number> and check whether it is
larger than C<max_retries_number>. If it is, then open the circuit by setting
C<_circuit_open_until> to the current time plus C<open_time>, and optionally
run C<on_circuit_open>. Then, die with the C<$error> from C<$attempt_code>.
=item step 4
Runs the C<on_failure_code> CodeRef in the proper context, giving it as
arguments C<$error>, and the return values of C<$attempt_code>, and returns the
results back to the caller.
=back
Arguments passed to C<run()> will be passed to C<$attempt_code>. They will also
passed to C<on_failure_code> as well if the case arises.
=head1 SEE ALSO
This code is heavily based on L<Action::Retry>.
t/00-report-prereqs.t view on Meta::CPAN
my $static_prereqs = do './t/00-report-prereqs.dd';
# Merge all prereqs (either with ::Prereqs or a hashref)
my $full_prereqs = _merge_prereqs(
( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ),
$static_prereqs
);
# Add dynamic prereqs to the included modules list (if we can)
my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
my $cpan_meta_error;
if ( $source && $HAS_CPAN_META
&& (my $meta = eval { CPAN::Meta->load_file($source) } )
) {
$full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs);
}
else {
$cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source)
$source = 'static metadata';
}
my @full_reports;
my @dep_errors;
my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs;
# Add static includes into a fake section
for my $mod (@include) {
$req_hash->{other}{modules}{$mod} = 0;
}
for my $phase ( qw(configure build test runtime develop other) ) {
next unless $req_hash->{$phase};
next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING});
t/00-report-prereqs.t view on Meta::CPAN
my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required";
if ($prefix) {
my $have = MM->parse_version( File::Spec->catfile($prefix, $file) );
$have = "undef" unless defined $have;
push @reports, [$mod, $want, $have];
if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) {
if ( $have !~ /\A$lax_version_re\z/ ) {
push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)";
}
elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) {
push @dep_errors, "$mod version '$have' is not in required range '$want'";
}
}
}
else {
push @reports, [$mod, $want, "missing"];
if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) {
push @dep_errors, "$mod is not installed ($req_string)";
}
}
}
if ( @reports ) {
push @full_reports, "=== $title ===\n\n";
my $ml = _max( map { length $_->[0] } @reports );
my $wl = _max( map { length $_->[1] } @reports );
my $hl = _max( map { length $_->[2] } @reports );
t/00-report-prereqs.t view on Meta::CPAN
push @full_reports, "\n";
}
}
}
if ( @full_reports ) {
diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports;
}
if ( $cpan_meta_error || @dep_errors ) {
diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n";
}
if ( $cpan_meta_error ) {
my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n";
}
if ( @dep_errors ) {
diag join("\n",
"\nThe following REQUIRED prerequisites were not satisfied:\n",
@dep_errors,
"\n"
);
}
pass;
# vim: ts=4 sts=4 sw=4 et:
t/20-retry.t view on Meta::CPAN
use Try::Tiny;
use Action::Retry;
{
my $died = 0;
my $action = Action::CircuitBreaker->new(max_retries_number => 9);
try {
my $foo = Action::Retry->new(
attempt_code => sub { $action->run(sub { die "plop"; }) }, # ie. the database failed
on_failure_code => sub { my ($error, $h) = @_; die $error; }, # by default Action::Retry would return undef
)->run();
} catch {
$died = 1;
};
is($died, 1);
}
done_testing;
( run in 0.267 second using v1.01-cache-2.11-cpan-65fba6d93b7 )