File Coverage

blib/lib/Object/RateLimiter.pm
Criterion Covered Total %
statement 60 61 98.3
branch 12 14 85.7
condition 4 7 57.1
subroutine 20 21 95.2
pod 7 7 100.0
total 103 110 93.6


line stmt bran cond sub pod time code
1             package Object::RateLimiter;
2             {
3             $Object::RateLimiter::VERSION = '1.001002';
4             }
5 1     1   34159 use strict; use warnings FATAL => 'all';
  1     1   3  
  1         34  
  1         5  
  1         2  
  1         32  
6              
7 1     1   6 use Carp 'confess';
  1         1  
  1         110  
8 1     1   1224 use List::Objects::WithUtils 'array';
  1         12944  
  1         16  
9 1     1   46693 use Scalar::Util 'blessed';
  1         3  
  1         118  
10 1     1   1172 use Time::HiRes 'time';
  1         2147  
  1         6  
11              
12 1     1   1044 use namespace::clean;
  1         20218  
  1         9  
13              
14             use overload
15 0     0   0 bool => sub { 1 },
16             '&{}' => sub {
17 1     1   1 my $self = shift;
18 1     1   5 sub { $self->delay }
19 1         6 },
20 1     1   314 fallback => 1;
  1         3  
  1         17  
21              
22             use Object::ArrayType::New
23 1     1   1401 [ events => '', seconds => 'SECS', '' => 'QUEUE' ];
  1         1122  
  1         9  
24 16     16 1 3059 sub seconds { $_[0]->[SECS] }
25 14     14 1 493 sub events { $_[0]->[EVENTS] }
26 9     9   52 sub _queue { $_[0]->[QUEUE] }
27              
28 1     1   1853 use Class::Method::Modifiers;
  1         2410  
  1         695  
29             around new => sub {
30             my ($orig, $class) = splice @_, 0, 2;
31             my $self = $class->$orig(@_);
32             confess "Constructor requires 'seconds =>' and 'events =>' parameters"
33             unless defined $self->seconds and defined $self->events;
34             $self
35             };
36              
37             sub clone {
38 2     2 1 839 my ($self, %params) = @_;
39 2 100       10 $params{events} = $self->events
40             unless defined $params{events};
41 2 100       8 $params{seconds} = $self->seconds
42             unless defined $params{seconds};
43              
44 2         89 my $cloned = $self->new(%params);
45 2 50       10 if (my $currentq = $self->_queue) {
46 2         10 $cloned->[QUEUE] = array( $currentq->all )
47             }
48             $cloned
49 2         44 }
50              
51              
52             sub delay {
53 7     7 1 850 my ($self) = @_;
54 7   66     46 my $thisq = $self->[QUEUE] ||= array;
55 7         75 my $ev_limit = $self->events;
56              
57 7 100       30 if ((my $ev_count = $thisq->count) >= $ev_limit) {
58 2         16 my $oldest_ts = $thisq->get(0);
59              
60 2         10 my $delayed = (
61             $oldest_ts
62             + ( $ev_count * $self->seconds / $ev_limit )
63             ) - time;
64              
65 2 50       11 $delayed > 0 ? return $delayed : $thisq->shift
66             }
67              
68 5         64 $thisq->push( time );
69              
70 5         81 0
71             }
72              
73              
74 2     2 1 7 sub clear { $_[0]->[QUEUE] = undef; 1 }
  2         27  
75              
76             sub expire {
77 2     2 1 6 my ($self) = @_;
78 2 100       7 $self->is_expired ? $self->clear : ()
79             }
80              
81             sub is_expired {
82 4     4 1 1000173 my ($self) = @_;
83 4   50     17 my $thisq = $self->_queue || return;
84 4   50     22 my $latest = $thisq->get(-1) || return;
85              
86 4 100       45 time - $latest > $self->seconds ? 1 : ()
87             }
88              
89             1;
90              
91             =pod
92              
93             =for Pod::Coverage EVENTS QUEUE SECS
94              
95             =head1 NAME
96              
97             Object::RateLimiter - A flood control (rate limiter) object
98              
99             =head1 SYNOPSIS
100              
101             use Object::RateLimiter;
102              
103             my $ctrl = Object::RateLimiter->new(
104             events => 3,
105             seconds => 5
106             );
107              
108             # Run some subs, as a contrived example;
109             # no more than 3 in 5 seconds, per our constructor above:
110             my @work = (
111             sub { "foo" }, sub { "bar" },
112             sub { "baz" }, sub { "cake" },
113             # ...
114             );
115              
116             while (my $some_item = shift @work) {
117             if (my $delay = $ctrl->delay) {
118             # Delayed $delay (fractional) seconds.
119             # (You might want Time::HiRes::sleep, or yield to event loop, etc)
120             sleep $delay;
121             } else {
122             # No delay.
123             print $some_item->()
124             }
125             }
126              
127             # Clear the event history if it's stale:
128             $ctrl->expire;
129              
130             # Clear the event history unconditionally:
131             $ctrl->clear;
132              
133             # Same as calling ->delay:
134             my $delayed = $ctrl->();
135              
136             =head1 DESCRIPTION
137              
138             This is a generic rate-limiter object, implementing the math described in
139             L via light-weight
140             array-type objects.
141              
142             The algorithm is fairly simple; the article linked above contains an in-depth
143             discussion by Vladi Belperchinov-Shabanski (CPAN:
144             L):
145              
146             $delay =
147             (
148             $oldest_timestamp +
149             ( $seen_events * $limit_secs / $event_limit )
150             )
151             - time()
152              
153             This module uses L to provide support for fractional seconds.
154              
155             See L for a similar module with a functional
156             interface & persistent on-disk storage features (for use with CGI
157             applications).
158              
159             =head2 new
160              
161             my $ctrl = Object::RateLimiter->new(
162             events => 3,
163             seconds => 5
164             );
165              
166             Constructs a new rate-limiter with a clean event history.
167              
168             =head2 clear
169              
170             $ctrl->clear;
171              
172             Clears the event history. Always returns true.
173              
174             =head2 clone
175              
176             my $new_ctrl = $ctrl->clone( events => 4 );
177              
178             Clones an existing rate-limiter; new options can be provided, overriding
179             previous settings.
180              
181             The new limiter contains a clone of the event history; the old rate-limiter is
182             left untouched.
183              
184             =head2 delay
185              
186             if (my $delay = $ctrl->delay) {
187             sleep $delay; # ... or do something else
188             } else {
189             # Not delayed.
190             do_work;
191             }
192              
193             # Same as calling ->delay:
194             my $delay = $ctrl->();
195              
196             The C method determines if some work can be done now, or should wait.
197              
198             When called, event timestamps are considered; if we have exceeded our limit,
199             the delay in (possibly fractional) seconds until the event would be
200             allowed is returned.
201              
202             A return value of 0 indicates that the event does not need to wait.
203              
204             =head2 events
205              
206             Returns the B limit the object was constructed with.
207              
208             =head2 expire
209              
210             $ctrl->expire;
211              
212             Clears the event history if L is true.
213              
214             Returns true if L was called.
215              
216             You're not required to call C, but it can be useful to save a little
217             memory (a 10 event history uses about 1kb here).
218              
219             =head2 is_expired
220              
221             Returns true if the last seen event is outside of our time window (in other
222             words, the event history is stale) or there is no event history.
223              
224             Also see L
225              
226             =head2 seconds
227              
228             Returns the B limit the object was constructed with.
229              
230             =head1 AUTHOR
231              
232             Jon Portnoy
233              
234             Based on the math from L as described in an article
235             written by the author:
236             L
237              
238             Licensed under the same terms as Perl.
239              
240             =cut