File Coverage

blib/lib/Object/RateLimiter.pm
Criterion Covered Total %
statement 61 62 98.3
branch 10 12 83.3
condition 4 7 57.1
subroutine 20 21 95.2
pod 7 7 100.0
total 102 109 93.5


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