Action-CircuitBreaker

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN

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.

README  view on Meta::CPAN

    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

README  view on Meta::CPAN


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 )