File Coverage

blib/lib/Algorithm/LeakyBucket.pm
Criterion Covered Total %
statement 12 103 11.6
branch 0 40 0.0
condition 0 3 0.0
subroutine 4 16 25.0
pod 0 12 0.0
total 16 174 9.2


line stmt bran cond sub pod time code
1             package Algorithm::LeakyBucket;
2              
3             =head1 NAME
4              
5             Algorithm::LeakyBucket - Perl implementation of leaky bucket rate limiting
6              
7             =head1 SYNOPSIS
8              
9             use Algorithm::LeakyBucket;
10             my $bucket = Algorithm::LeakyBucket->new( ticks => 1, seconds => 1 ); # one per second
11              
12             while($something_happening)
13             {
14             if ($bucket->tick)
15             {
16             # allowed
17             do_something();
18             # maybe decide to change limits?
19             $bucket->ticks(2);
20             $bucket->seconds(5);
21             }
22             }
23              
24              
25             =head1 CONSTRUCTOR
26              
27             There are two required options to get the module to do anything useful. C and C set the number of
28             ticks allowed per that time period. If C is 3 and C is 14, you will be able to run 3 ticks every 14
29             seconds. Optionally you can pass C and C to distribute the limiting across multiple
30             processes.
31              
32              
33             my $bucket = Algorithm::LeakyBucket->new( ticks => $ticks, seconds => $every_x_seconds,
34             memcached_key => 'some_key',
35             memcached_servers => [ { address => 'localhost:11211' } ] );
36              
37             =DESCRIPTION
38              
39             Implements leaky bucket as a rate limiter. While the code will do rate limiting for a single process, it was intended
40             as a limiter for multiple processes. (But see the BUGS section)
41              
42             The syntax of the C argument should be the syntax expected by the local memcache module. If
43             Cache::Memcached::Fast is installed, use its syntax, otherwise you can use the syntax for Cache::Memcached. If
44             neither module is found it will use a locally defined set of vars internally to track rate limiting. Obviously
45             this keeps the code from being used across processes.
46              
47             This is an alpha version of the code. Some early bugs have been ironed out and its in produciton in places, so we would
48             probably transition it to beta once we have seen it work for a bit.
49              
50             =cut
51              
52 1     1   20363 use 5.008008;
  1         3  
  1         30  
53 1     1   5 use strict;
  1         2  
  1         24  
54 1     1   4 use warnings;
  1         5  
  1         33  
55 1     1   5 use Carp qw(cluck);
  1         1  
  1         1012  
56             our $VERSION = '0.08';
57              
58             sub new
59             {
60 0     0 0   my ($class, %args) = @_;
61 0           my $self = {};
62 0           bless ($self, $class);
63              
64 0           eval {
65 0           require Cache::Memcached::Fast;
66 0           $self->{__mc_module_fast} = 1;
67             };
68              
69 0           eval {
70 0           require Cache::Memcached;
71 0           $self->{__mc_module} = 1;
72             };
73              
74 0           while (my($k,$v) = each (%args))
75             {
76 0 0         if ($self->can($k))
77             {
78 0           $self->$k($v);
79             }
80             }
81 0           $self->init(%args);
82              
83              
84 0           return $self;
85             }
86              
87             sub ticks
88             {
89 0     0 0   my ($self, $value) = @_;
90 0 0         if (defined($value))
91             {
92 0           $self->{__ticks} = $value;
93             }
94 0           return $self->{__ticks};
95             }
96              
97             sub seconds
98             {
99 0     0 0   my ($self, $value) = @_;
100 0 0         if (defined($value))
101             {
102 0           $self->{__seconds} = $value;
103             }
104 0           return $self->{__seconds};
105             }
106              
107             sub current_allowed
108             {
109 0     0 0   my ($self, $value) = @_;
110 0 0         if (defined($value))
111             {
112 0           $self->{__current_allowed} = $value;
113             }
114 0           return $self->{__current_allowed};
115             }
116              
117             sub last_tick
118             {
119 0     0 0   my ($self, $value) = @_;
120 0 0         if (defined($value))
121             {
122 0           $self->{__last_tick} = $value;
123             }
124 0           return $self->{__last_tick};
125             }
126              
127             sub memcached_key
128             {
129 0     0 0   my ($self, $value) = @_;
130 0 0         if (defined($value))
131             {
132 0           $self->{__mc_key} = $value;
133             }
134 0           return $self->{__mc_key};
135             }
136              
137             sub memcached
138             {
139 0     0 0   my ($self, $value) = @_;
140 0 0         if (defined($value))
141             {
142 0           $self->{__mc} = $value;
143             }
144 0           return $self->{__mc};
145             }
146              
147             sub memcached_servers
148             {
149 0     0 0   my ($self, $value) = @_;
150              
151 0 0         if (defined($value))
152             {
153 0 0 0       if ((!$self->{__mc_module}) && (!$self->{__mc_module__fast}))
154             {
155 0           croak("No memcached support installed, try installing Cache::Memcached or Cache::Memcached::Fast");
156             }
157 0           $self->{__mc_servers} = $value;
158             }
159 0           return $self->{__mc_servers};
160             }
161              
162             sub tick
163             {
164 0     0 0   my ($self, %args ) = @_;
165              
166 0 0         if ($self->memcached)
167             {
168             # init form mc
169 0           $self->mc_sync;
170             }
171            
172             # seconds since last tick
173 0           my $now = time();
174 0           my $seconds_passed = $now - $self->last_tick;
175 0           $self->last_tick( time() );
176              
177             # add tokens to bucket
178 0           my $current_ticks_allowed = $self->current_allowed + ( $seconds_passed * ( $self->ticks / $self->seconds ));
179 0           $self->current_allowed( $current_ticks_allowed );
180              
181 0 0         if ($current_ticks_allowed > $self->ticks)
    0          
182             {
183 0           $self->current_allowed($self->ticks);
184 0 0         if ($self->memcached)
185             {
186 0           $self->mc_write;
187             }
188 0           return 1;
189             }
190             elsif ($current_ticks_allowed < 1)
191             {
192 0           return 0;
193             }
194             else
195             {
196 0           $self->current_allowed( $current_ticks_allowed - 1);
197 0 0         if ($self->memcached)
198             {
199 0           $self->mc_write;
200             }
201 0           return 1;
202             }
203            
204 0           return;
205             }
206              
207             sub init
208             {
209 0     0 0   my ($self, %args) = @_;
210 0           $self->current_allowed( $self->ticks );
211 0           $self->last_tick( time() );
212 0 0         if ($self->memcached_servers)
213             {
214 0 0         if ($self->{__mc_module_fast})
    0          
215             {
216 0           eval {
217 0           my $mc = Cache::Memcached::Fast->new({ servers => $self->memcached_servers,
218             namespace => 'leaky_bucket:', });
219 0           $self->memcached($mc);
220 0           $self->mc_sync;
221             };
222 0 0         if ($@)
223             {
224 0           cluck($@);
225             }
226             }
227             elsif ($self->{__mc_module})
228             {
229 0           eval {
230 0           my $mc = Cache::Memcached->new({ servers => $self->memcached_servers,
231             namespace => 'leaky_bucket:', });
232 0           $self->memcached($mc);
233 0           $self->mc_sync;
234             };
235 0 0         if ($@)
236             {
237 0           cluck($@);
238             }
239             }
240             }
241 0           return;
242             }
243              
244             sub mc_sync
245             {
246 0     0 0   my ($self, %args) = @_;
247              
248 0           my $packed = $self->memcached->get( $self->memcached_key );
249 0 0         if ($packed)
250             {
251             # current allowed | last tick
252 0           my @vals = split(/\|/,$packed);
253 0           $self->current_allowed($vals[0]);
254 0           $self->last_tick($vals[1]);
255             }
256 0           return;
257             }
258              
259             sub mc_write
260             {
261 0     0 0   my ($self, %args) = @_;
262 0           $self->memcached->set($self->memcached_key, $self->current_allowed . '|' . $self->last_tick);
263 0           return;
264             }
265              
266             =head1 BUGS
267              
268             Probably some. There is a known bug where if you are in an infinite loop you could move faster than
269             memcached could be updated remotely, so you'll likely at that point only bbe limted by the local
270             counters. I'm not sure how im going to fix this yet as this is in early development.
271              
272             =head1 TODO
273              
274             Will need to look at including some actual tests im thinking. Maybe once we get more real usage out
275             of this in our produciton environment some test cases will make themselves obvious.
276            
277             =head1 SEE ALSO
278              
279             http://en.wikipedia.org/wiki/Leaky_bucket
280              
281             =head1 AUTHOR
282              
283             Marcus Slagle, Emarc.slagle@online-rewards.comE
284              
285             =head1 COPYRIGHT AND LICENSE
286              
287             Copyright (C) 2012 by Marcus Slagle
288              
289             This library is free software; you can redistribute it and/or modify
290             it under the same terms as Perl itself, either Perl version 5.8.9 or,
291             at your option, any later version of Perl 5 you may have available.
292              
293             =cut
294              
295             1;
296              
297