File Coverage

blib/lib/Cache/Elasticache/Memcache.pm
Criterion Covered Total %
statement 80 83 96.3
branch 21 28 75.0
condition 1 6 16.6
subroutine 12 13 92.3
pod 3 4 75.0
total 117 134 87.3


line stmt bran cond sub pod time code
1             package Cache::Elasticache::Memcache;
2              
3 3     3   170879 use strict;
  3         6  
  3         81  
4 3     3   15 use warnings;
  3         5  
  3         105  
5              
6             =pod
7              
8             =begin html
9              
10            

11            
12            

13              
14             =end html
15              
16             =head1 NAME
17              
18             Cache::Elasticache::Memcache - A wrapper for L with support for AWS's auto reconfiguration mechanism
19              
20             =head1 SYNOPSIS
21              
22             use Cache::Elasticache::Memcache;
23              
24             my $memd = new Cache::Elasticache::Memcache->new({
25             config_endpoint => 'foo.bar',
26             update_period => 180,
27             # All other options are passed on to Cache::Memcached::Fast
28             ...
29             });
30              
31             # Will update the server list from the configuration endpoint
32             $memd->updateServers();
33              
34             # Will update the serverlist from the configuration endpoint if the time since
35             # the last time the server list was checked is greater than the update period
36             # specified when the $memd object was created.
37             $memd->checkServers();
38              
39             # Class method to retrieve a server list from a configuration endpoint.
40             Cache::Elasticache::Memcache->getServersFromEndpoint('foo.bar');
41              
42             # All other supported methods are handled by Cache::Memcached::Fast
43              
44             # N.B. This library is currently under development
45              
46             =head1 DESCRIPTION
47              
48             A wrapper for L with support for AWS's auto reconfiguration mechanism. It makes use of an AWS elasticache memcached cluster's configuration endpoint to disctover the memcache servers in the cluster and periodically check the current server list to adapt to a changing cluster.
49              
50             =head1 UNDER DEVELOPMENT DISCALIMER
51              
52             N.B. This module is still under development. It should work, but things may change under the hood. I plan to imporove the resiliance with better timeout handling of communication when updating the server list. Also I'm investigating switching to Dist::Milla. I'm open to suggestions, ideas and pull requests.
53              
54             =cut
55              
56 3     3   14 use Carp;
  3         9  
  3         162  
57 3     3   3307 use IO::Socket::IP;
  3         130475  
  3         17  
58 3     3   5986 use Cache::Memcached::Fast;
  3         21624  
  3         1029  
59              
60             our $VERSION = '0.0.3';
61              
62             =pod
63              
64             =head1 CONSTRUCTOR
65              
66             Cache::Elasticache::Memcache->new({
67             config_endpoint => 'foo.bar',
68             update_period => 180,
69             ...
70             })
71              
72             =head2 Constructor parameters
73              
74             =over
75              
76             =item config_endpoint
77              
78             AWS elasticache memcached cluster config endpoint location
79              
80             =item update_period
81              
82             The minimum period (in seconds) to wait between updating the server list. Defaults to 180 seconds
83              
84             =back
85              
86             =cut
87              
88             sub new {
89 9     9 0 103864 my $class = shift;
90 9         19 my ($conf) = @_;
91 9         27 my $self = bless {}, $class;
92              
93 9 50       49 my $args = (@_ == 1) ? shift : { @_ }; # hashref-ify args
94              
95 9 100       55 croak "config_endpoint must be speccified" if (!defined $args->{'config_endpoint'});
96 8 100       53 croak "servers is not a valid constructors parameter" if (defined $args->{'servers'});
97              
98 7         15 $self->{'config_endpoint'} = delete @{$args}{'config_endpoint'};
  7         40  
99              
100 7 50       57 $args->{servers} = $class->getServersFromEndpoint($self->{'config_endpoint'}) if(defined $self->{'config_endpoint'});
101 7         38 $self->{_last_update} = time;
102              
103 7 100       38 $self->{update_period} = exists $args->{update_period} ? $args->{update_period} : 180;
104              
105 7         18 $self->{'_args'} = $args;
106 7         36 $self->{_memd} = Cache::Memcached::Fast->new($args);
107 7         142 $self->{servers} = $args->{servers};
108              
109 7         40 return $self;
110             }
111              
112             =pod
113              
114             =head1 METHODS
115              
116             =over
117              
118             =item Supported Cache::Memcached::Fast methods
119              
120             These methods can be called on a Cache::Elasticache::Memcache object. The object will call checkServers, then the call will be passed on to the appropriate L code. Please see the L documentation for further details regarding these methods.
121              
122             $memd->enable_compress($enable)
123             $memd->namespace($string)
124             $memd->set($key, $value)
125             $memd->set_multi([$key, $value],[$key, $value, $expiration_time])
126             $memd->cas($key, $cas, $value)
127             $memd->cas_multi([$key, $cas, $value],[$key, $cas, $value])
128             $memd->add($key, $value)
129             $memd->add_multi([$key, $value],[$key, $value])
130             $memd->replace($key, $value)
131             $memd->replace_multi([$key, $value],[$key, $value])
132             $memd->append($key, $value)
133             $memd->append_multi([$key, $value],[$key, $value])
134             $memd->prepend($key, $value)
135             $memd->prepend_multi([$key, $value],[$key, $value])
136             $memd->get($key)
137             $memd->get_multi(@keys)
138             $memd->gets($key)
139             $memd->gets_multi(@keys)
140             $memd->incr($key)
141             $memd->incr_multi(@keys)
142             $memd->decr($key)
143             $memd->decr_multi(@keys)
144             $memd->delete($key)
145             $memd->delete_multi(@keys)
146             $memd->touch($key, $expiration_time)
147             $memd->touch_multi([$key],[$key, $expiration_time])
148             $memd->flush_all($delay)
149             $memd->nowait_push()
150             $memd->server_versions()
151             $memd->disconnect_all()
152              
153             =cut
154              
155             my @methods = qw(
156             enable_compress
157             namespace
158             set
159             set_multi
160             cas
161             cas_multi
162             add
163             add_multi
164             replace
165             replace_multi
166             append
167             append_multi
168             prepend
169             prepend_multi
170             get
171             get_multi
172             gets
173             gets_multi
174             incr
175             incr_multi
176             decr
177             decr_multi
178             delete
179             delete_multi
180             touch
181             touch_multi
182             flush_all
183             nowait_push
184             server_versions
185             disconnect_all
186             );
187              
188             foreach my $method (@methods) {
189             my $method_name = "Cache::Elasticache::Memcache::$method";
190 3     3   26 no strict 'refs';
  3         6  
  3         2336  
191             *$method_name = sub {
192 30     30   85316 my $self = shift;
193 30         88 $self->checkServers;
194 30         375 return $self->{'_memd'}->$method(@_);
195             };
196             }
197              
198             =pod
199              
200             =item checkServers
201              
202             my $memd = Cache::Elasticache::Memcache->new({
203             config_endpoint => 'foo.bar'
204             })
205              
206             ...
207              
208             $memd->checkServers();
209              
210             Trigger the the server list to be updated if the time passed since the server list was last updated is greater than the update period (default 180 seconds).
211              
212             =cut
213              
214             sub checkServers {
215 0     0 1 0 my $self = shift;
216 0 0 0     0 if ( defined $self->{'config_endpoint'} && (time - $self->{_last_update}) > $self->{update_period} ) {
217 0         0 $self->updateServers();
218             }
219             }
220              
221             =pod
222              
223             =item updateServers
224              
225             my $memd = Cache::Elasticache::Memcache->new({
226             config_endpoint => 'foo.bar'
227             })
228              
229             ...
230              
231             $memd->updateServers();
232              
233             This method will update the server list regardles of how much time has passed since the server list was last checked.
234              
235             =cut
236              
237             sub updateServers {
238 4     4 1 6019479 my $self = shift;
239              
240 4         31 my $servers = $self->getServersFromEndpoint($self->{'config_endpoint'});
241              
242             ## Cache::Memcached::Fast does not support updating the server list after creation
243             ## Therefore we must create a new object.
244              
245 4 100       20 if ( $self->_hasServerListChanged($servers) ) {
246 2         6 $self->{_args}->{servers} = $servers;
247 2         14 $self->{_memd} = Cache::Memcached::Fast->new($self->{'_args'});
248             }
249              
250 4         58 $self->{servers} = $servers;
251 4         22 $self->{_last_update} = time;
252             }
253              
254             sub _hasServerListChanged {
255 4     4   10 my $self = shift;
256 4         9 my $servers = shift;
257              
258 4 100       10 return 1 unless(scalar(@$servers) == scalar(@{$self->{'servers'}}));
  4         30  
259              
260 2         8 foreach my $server (@$servers) {
261 6 50       14 return 1 unless ( grep { $server eq $_ } @{$self->{'servers'}} );
  18         74  
  6         21  
262             }
263              
264 2         12 return 0;
265             }
266              
267             =pod
268              
269             =back
270              
271             =head1 CLASS METHODS
272              
273             =over
274              
275             =item getServersFromEndpoint
276              
277             Cache::Elasticache::Memcache->getServersFromEndpoint('foo.bar');
278              
279             This class method will retrieve the server list for a given configuration endpoint.
280              
281             =cut
282              
283             sub getServersFromEndpoint {
284 13     13 1 17222 my $class = shift;
285 13         26 my $config_endpoint = shift;
286             # TODO: Use IO::Socket::Timeout to handle timing outsocke reads sensibly
287             # TODO: make use of "connect_timeout" (default 0.5s) and "io_timeout" (default 0.2s) constructor parameters
288             # my $args = shift;
289             # $connect_timeout = exists $args->{connect_timeout} ? $args->{connect_timeout} : $class::default_connect_timeout;
290             # $io_timeout = exists $args->{io_timeout} ? $args->{io_timeout} : $class::default_io_timeout;
291 13         74 my $socket = IO::Socket::IP->new(PeerAddr => $config_endpoint, Timeout => 10, Proto => 'tcp');
292 13 50       571 croak "Unable to connect to server: ".$config_endpoint." - $!" unless $socket;
293              
294 13         169 $socket->autoflush(1);
295 13         1134 $socket->send("config get cluster\r\n");
296 13         862 my $data = "";
297 13         24 my $count = 0;
298 13         51 until ($data =~ m/END/) {
299 183         1107 my $line = $socket->getline();
300 183 100       11070 if (defined $line) {
301 128         278 $data .= $line;
302             }
303 183         302 $count++;
304 183 100       802 last if ( $count == 30 );
305             }
306 13         80 $socket->close();
307 13         740 return $class->_parseConfigResponse($data);
308             }
309              
310             sub _parseConfigResponse {
311 13     13   44 my $class = shift;
312 13         29 my $data = shift;
313 13 50 33     101 return [] unless (defined $data && $data ne '');
314 13         119 my @response_lines = split(/[\r\n]+/,$data);
315 13         32 my @servers = ();
316 13         28 my $node_regex = '([-.a-zA-Z0-9]+)\|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\|(\d+)';
317 13         26 foreach my $line (@response_lines) {
318 56 100       302 if ($line =~ m/$node_regex/) {
319 13         52 foreach my $node (split(' ', $line)) {
320 33         136 my ($host, $ip, $port) = split('\|',$node);
321 33         164 push(@servers,$ip.':'.$port);
322             }
323             }
324             }
325 13         64 return \@servers;
326             }
327              
328             1;
329             __END__