File Coverage

blib/lib/Starch/Plugin/ThrottleStore.pm
Criterion Covered Total %
statement 15 15 100.0
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 20 20 100.0


line stmt bran cond sub pod time code
1             package Starch::Plugin::ThrottleStore;
2             our $VERSION = '0.14';
3              
4             =encoding utf8
5              
6             =head1 NAME
7              
8             Starch::Plugin::ThrottleStore - Throttle misbehaving Starch stores.
9              
10             =head1 SYNOPSIS
11              
12             my $starch = Starch->new(
13             plugins => ['::ThrottleStore'],
14             store => {
15             class => ...,
16             throttle_threshold => 2,
17             throttle_duration => 20,
18             },
19             );
20              
21             =head1 DESCRIPTION
22              
23             This plugin detects stores which are throwing errors consistently
24             and disables them for a period of time.
25              
26             When the L number of consecutive errors
27             is reached all store operations will be disabled for
28             L seconds.
29              
30             When the error threshold has been reached an erorr log message
31             will be produced stating that throttling is starting. Each
32             store access for the duration of the throttling will then produce
33             a log message stating which state key is being throttled.
34              
35             =cut
36              
37 1     1   428 use Try::Tiny;
  1         2  
  1         55  
38 1     1   5 use Types::Common::Numeric -types;
  1         2  
  1         23  
39              
40 1     1   1192 use Moo::Role;
  1         1  
  1         8  
41 1     1   340 use strictures 2;
  1         5  
  1         34  
42 1     1   162 use namespace::clean;
  1         2  
  1         6  
43              
44             with 'Starch::Plugin::ForStore';
45              
46             =head1 OPTIONAL STORE ARGUMENTS
47              
48             These arguments are added to classes which consume the
49             L role.
50              
51             =head2 throttle_threshold
52              
53             How many consecutive errors which will trigger throttling.
54             Defaults to C<1>, which means the first error detected will
55             begin throttling.
56              
57             =cut
58              
59             has throttle_threshold => (
60             is => 'ro',
61             isa => PositiveInt,
62             default => 1,
63             );
64              
65             =head2 throttle_duration
66              
67             How many seconds to throttle for once the L
68             has been reached. Default to C<60> (1 minute).
69              
70             =cut
71              
72             has throttle_duration => (
73             is => 'ro',
74             isa => PositiveInt,
75             default => 60,
76             );
77              
78             =head1 STORE ATTRIBUTES
79              
80             These attributes are added to classes which consume the
81             L role.
82              
83             =head2 throttle_error_count
84              
85             Contains the current number of consecutive errors.
86              
87             =cut
88              
89             has throttle_error_count => (
90             is => 'ro',
91             init_Arg => undef,
92             default => 0,
93             writer => '_set_throttle_error_count',
94             );
95              
96             =head2 throttle_start
97              
98             Contains the epoch time of when the L was
99             passed and throttling began.
100              
101             =cut
102              
103             has throttle_start => (
104             is => 'ro',
105             init_arg => undef,
106             writer => '_set_throttle_start',
107             clearer => '_clear_throttle_start',
108             );
109              
110             foreach my $method (qw( set get remove )) {
111             around $method => sub{
112             my $orig = shift;
113             my $self = shift;
114              
115             my $error_count = $self->throttle_error_count();
116             my $start = $self->throttle_start();
117              
118             if ($start) {
119             my $duration = $self->throttle_duration();
120             if ($start + $duration < time()) {
121             $self->_clear_throttle_start();
122             $error_count = 0;
123             }
124             else {
125             my ($id, $namespace) = @_;
126             my $manager = $self->manager();
127             my $key = $self->stringify_key( $id, $namespace );
128             $self->log->errorf(
129             'Throttling %s of state key %s on the %s store for the next %d seconds.',
130             $method, $key, $self->short_store_class_name(), ($start + $duration) - time(),
131             );
132             return {
133             $manager->no_store_state_key() => 1,
134             } if $method eq 'get';
135             return;
136             }
137             }
138              
139             my @args = @_;
140             my ($ret, $error, $errored);
141             try { $ret = $self->$orig( @args ) }
142             catch { $error=$_; $errored=1 };
143              
144             if ($errored) { $error_count ++ }
145             else { $error_count = 0 }
146             $self->_set_throttle_error_count( $error_count );
147              
148             my $threshold = $self->throttle_threshold();
149             if ($error_count >= $threshold) {
150             $self->log->errorf(
151             'Error threshold %d reached on the %s store, throttling for the next %d seconds.',
152             $threshold, $self->short_store_class_name(), $self->throttle_duration(),
153             );
154             $self->_set_throttle_start( time() );
155             }
156              
157             die $error if $errored;
158              
159             return $ret if $method eq 'get';
160             return;
161             };
162             }
163              
164             1;
165             __END__