File Coverage

blib/lib/Cache/MemcachedBinary.pm
Criterion Covered Total %
statement 93 343 27.1
branch 6 88 6.8
condition 0 78 0.0
subroutine 27 62 43.5
pod 0 27 0.0
total 126 598 21.0


line stmt bran cond sub pod time code
1             package Cache::MemcachedBinary;
2            
3 1     1   105942 use strict;
  1         2  
  1         36  
4 1     1   6 use warnings;
  1         2  
  1         62  
5            
6 1     1   6 no strict 'refs';
  1         2  
  1         72  
7 1     1   584 use IO::Socket;
  1         27645  
  1         5  
8 1     1   1095 use Encode;
  1         18986  
  1         151  
9             $| = 1;
10            
11 1     1   9 use vars qw($VERSION);
  1         2  
  1         58  
12             $VERSION = "1.02";
13            
14 1     1   5 use constant HOST => '127.0.0.1';
  1         1  
  1         75  
15 1     1   5 use constant PORT => 11211;
  1         2  
  1         54  
16 1     1   5 use constant LOGIN => 'login';
  1         1  
  1         39  
17 1     1   4 use constant PASSWORD => 'password';
  1         2  
  1         32  
18 1     1   5 use constant TIMEOUT => 2;
  1         1  
  1         39  
19 1     1   5 use constant DEBUG => 0;
  1         2  
  1         34  
20 1     1   4 use constant ERROR_FLAG => 1;
  1         2  
  1         57  
21            
22 1     1   6 use constant PROTOCOL_BINARY_REQ => 0x80;
  1         2  
  1         38  
23 1     1   69 use constant PROTOCOL_BINARY_RES => 0x81;
  1         2  
  1         58  
24 1     1   5 use constant PROTOCOL_BINARY_RESPONSE_SUCCESS => 0x00;
  1         2  
  1         36  
25 1     1   4 use constant PROTOCOL_BINARY_RESPONSE_AUTH_ERROR => 0x20;
  1         2  
  1         41  
26 1     1   5 use constant PROTOCOL_BINARY_CMD_GET => 0x00;
  1         2  
  1         34  
27 1     1   4 use constant PROTOCOL_BINARY_CMD_SET => 0x01;
  1         1  
  1         32  
28 1     1   3 use constant PROTOCOL_BINARY_CMD_ADD => 0x02;
  1         2  
  1         39  
29 1     1   4 use constant PROTOCOL_BINARY_CMD_DELETE => 0x04;
  1         2  
  1         33  
30 1     1   4 use constant PROTOCOL_BINARY_CMD_INCREMENT => 0x05;
  1         2  
  1         40  
31 1     1   4 use constant PROTOCOL_BINARY_CMD_QUIT => 0x07;
  1         2  
  1         34  
32 1     1   4 use constant PROTOCOL_BINARY_CMD_FLUSH => 0x08;
  1         2  
  1         43  
33 1     1   5 use constant PROTOCOL_BINARY_CMD_SASL_LIST_MECHS => 0x20;
  1         1  
  1         47  
34 1     1   5 use constant PROTOCOL_BINARY_CMD_SASL_AUTH => 0x21;
  1         2  
  1         3817  
35            
36             sub new {
37 1     1 0 203646 my $class = shift;
38 1         4 my %param = @_;
39            
40 1 50       7 if (! exists $param{host}) {$param{host} = HOST;}
  1         4  
41 1 50       5 if (! exists $param{port}) {$param{port} = PORT;}
  1         3  
42 1 50       5 if (! exists $param{timeout}) {$param{timeout} = TIMEOUT;}
  1         3  
43 1 50       14 if (! exists $param{login}) {$param{login} = LOGIN;}
  1         4  
44 1 50       4 if (! exists $param{password}) {$param{password} = PASSWORD;}
  1         4  
45 1 50       4 if (! exists $param{debug}) {$param{debug} = DEBUG;}
  1         3  
46            
47 1         11 bless {%param}, $class;
48             }
49            
50             sub add {
51 0     0 0   my $self = shift;
52 0           $self->_set(PROTOCOL_BINARY_CMD_ADD, @_);
53             }
54            
55             sub set {
56 0     0 0   my $self = shift;
57 0           $self->_set(PROTOCOL_BINARY_CMD_SET, @_);
58             }
59            
60             sub _set {
61 0     0     my $self = shift;
62 0           my ($opcode, $key, $val, $sec) = @_;
63 0   0       $sec //= 0;
64            
65 0 0         if ($opcode == PROTOCOL_BINARY_CMD_ADD) {$self->_log("ADD key:$key, seconds:$sec");}
  0 0          
66 0           elsif ($opcode == PROTOCOL_BINARY_CMD_SET) {$self->_log("SET key:$key, seconds:$sec");}
67            
68 0   0       my $socket = $self->_get_socket() || return;
69            
70 0 0         if (Encode::is_utf8($key)) {Encode::_utf8_off($key);}
  0            
71 0 0         if (Encode::is_utf8($val)) {Encode::_utf8_off($val);}
  0            
72            
73 0           my $sec_binary = sprintf '%08x', $sec;
74            
75 0           $self->set_err(undef);
76            
77 0           $self->_request_packet({
78             Opcode => $opcode,
79             Extras => 'deadbeef' . $sec_binary,
80             Key => join("", map sprintf('%x', $_), unpack("C*", $key)),
81             Value => join("", map sprintf('%x', $_), unpack("C*", $val)),
82             });
83            
84 0           my $data = $self->_read_responce();
85 0 0 0       if ( $$data{MagicUnpack} == PROTOCOL_BINARY_RES && $$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_SUCCESS ) {
86 0           $self->_log("Result: success");
87 0           return 1;
88             }
89            
90 0           $self->_log("Result: fail, server answer: $$data{ValueUnpack}");
91 0           $self->set_err($$data{ValueUnpack});
92            
93 0 0         if ($$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_AUTH_ERROR) { # auth error, try auth
94 0           $self->_socket_destroy();
95 0 0         return $self->_set(@_) if $self->auth();
96             }
97            
98 0           return;
99             }
100            
101             sub get {
102 0     0 0   my $self = shift;
103 0   0       my $key = shift || return;
104            
105 0           $self->_log("GET key:$key");
106            
107 0   0       my $socket = $self->_get_socket() || return;
108            
109 0 0         if (Encode::is_utf8($key)) {Encode::_utf8_off($key);}
  0            
110            
111 0           $self->set_err(undef);
112            
113 0           $self->_request_packet({
114             Opcode => PROTOCOL_BINARY_CMD_GET,
115             Key => join("", map sprintf('%x', $_), unpack("C*", $key)),
116             });
117            
118 0           my $data = $self->_read_responce();
119 0 0 0       if ( $$data{MagicUnpack} == PROTOCOL_BINARY_RES && $$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_SUCCESS ) {
120 0           $self->_log("Result: success");
121 0           return $$data{ValueUnpack};
122             }
123            
124 0           $self->_log("Result: fail, server answer: $$data{ValueUnpack}");
125 0           $self->set_err($$data{ValueUnpack});
126            
127 0 0         if ($$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_AUTH_ERROR) { # auth error, try auth
128 0           $self->_socket_destroy();
129 0 0         return $self->get($key) if $self->auth();
130             }
131            
132 0           return;
133             }
134            
135             sub delete {
136 0     0 0   my $self = shift;
137 0   0       my $key = shift || return;
138            
139 0           $self->_log("DELETE key:$key");
140            
141 0   0       my $socket = $self->_get_socket() || return;
142            
143 0 0         if (Encode::is_utf8($key)) {Encode::_utf8_off($key);}
  0            
144            
145 0           $self->set_err(undef);
146            
147 0           $self->_request_packet({
148             Opcode => PROTOCOL_BINARY_CMD_DELETE,
149             Key => join("", map sprintf('%x', $_), unpack("C*", $key)),
150             });
151            
152 0           my $data = $self->_read_responce();
153 0 0 0       if ( $$data{MagicUnpack} == PROTOCOL_BINARY_RES && $$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_SUCCESS ) {
154 0           $self->_log("Result: success");
155 0           return 1;
156             }
157            
158 0           $self->_log("Result: fail, server answer: $$data{ValueUnpack}");
159 0           $self->set_err($$data{ValueUnpack});
160            
161 0 0         if ($$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_AUTH_ERROR) { # auth error, try auth
162 0           $self->_socket_destroy();
163 0 0         return $self->delete($key) if $self->auth();
164             }
165            
166 0           return;
167             }
168            
169             sub incr {
170 0     0 0   my $self = shift;
171 0   0       my $key = shift || return;
172            
173 0           $self->_log("INCREMENT key:$key");
174            
175 0   0       my $socket = $self->_get_socket() || return;
176            
177 0 0         if (Encode::is_utf8($key)) {Encode::_utf8_off($key);}
  0            
178            
179 0           $self->set_err(undef);
180            
181 0           $self->_request_packet({
182             Opcode => PROTOCOL_BINARY_CMD_INCREMENT,
183             Key => join("", map sprintf('%x', $_), unpack("C*", $key)),
184             Extras => '0000000000000001' . '0000000000000000' . '00000000',
185             });
186            
187 0           my $data = $self->_read_responce();
188 0 0 0       if ( $$data{MagicUnpack} == PROTOCOL_BINARY_RES && $$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_SUCCESS ) {
189 0           $self->_log("Result: success");
190 0           my @mess = unpack('N2', $$data{Value});
191 0           return $mess[1];
192 0           return 1;
193             }
194            
195 0           $self->_log("Result: fail, server answer: $$data{ValueUnpack}");
196 0           $self->set_err($$data{ValueUnpack});
197            
198 0 0         if ($$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_AUTH_ERROR) { # auth error, try auth
199 0           $self->_socket_destroy();
200 0 0         return $self->incr($key) if $self->auth();
201             }
202            
203 0           return;
204             }
205            
206             sub flush {
207 0     0 0   my $self = shift;
208 0   0       my $sec = shift // 0;
209            
210 0           $self->_log("FLUSH seconds:$sec");
211            
212 0   0       my $socket = $self->_get_socket() || return;
213            
214 0           my $sec_binary = sprintf '%08x', $sec;
215            
216 0           $self->set_err(undef);
217            
218 0           $self->_request_packet({
219             Opcode => PROTOCOL_BINARY_CMD_FLUSH,
220             Extras => $sec_binary,
221             });
222            
223 0           my $data = $self->_read_responce();
224 0 0 0       if ( $$data{MagicUnpack} == PROTOCOL_BINARY_RES && $$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_SUCCESS ) {
225 0           $self->_log("Result: success");
226 0           return 1;
227             }
228            
229 0           $self->_log("Result: fail, server answer: $$data{ValueUnpack}");
230 0           $self->set_err($$data{ValueUnpack});
231            
232 0 0         if ($$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_AUTH_ERROR) { # auth error, try auth
233 0           $self->_socket_destroy();
234 0 0         return $self->flush($sec) if $self->auth();
235             }
236            
237 0           return;
238             }
239            
240             sub quit {
241 0     0 0   my $self = shift;
242            
243 0           $self->_log("QUIT");
244            
245 0   0       my $socket = $self->_get_socket() || return;
246            
247 0           $self->set_err(undef);
248            
249 0           $self->_request_packet({
250             Opcode => PROTOCOL_BINARY_CMD_QUIT,
251             });
252            
253 0           my $data = $self->_read_responce();
254 0 0 0       if ( $$data{MagicUnpack} == PROTOCOL_BINARY_RES && $$data{StatusUnpack} == PROTOCOL_BINARY_RESPONSE_SUCCESS ) {
255 0           $self->_log("Result: success");
256 0           $self->_socket_destroy();
257 0           return 1;
258             }
259            
260 0           $self->_log("Result: fail, server answer: $$data{ValueUnpack}");
261 0           $self->set_err($$data{ValueUnpack});
262            
263 0           return;
264             }
265            
266             sub auth {
267 0     0 0   my $self = shift;
268            
269 0           $self->_log("AUTH");
270            
271 0   0       my $socket = $self->_get_socket() || return;
272            
273 0           $self->_request_packet({
274             Opcode => PROTOCOL_BINARY_CMD_SASL_LIST_MECHS,
275             });
276            
277 0           my $data = $self->_read_responce();
278            
279 0           my $methods = unpack('A*', $$data{Value});
280 0           $self->_log("Server support auth SASL methods: $methods");
281 0 0         if ( $methods !~ m/PLAIN/ ) {
282 0           $self->set_err("Server not support SASL PLAIN");
283 0           $self->_log("Server not support SASL PLAIN", ERROR_FLAG);
284 0           return;
285             }
286            
287 0           my $login = join "", map sprintf('%x', $_), unpack("C*", $self->login);
288 0           my $password = join "", map sprintf('%x', $_), unpack("C*", $self->password);
289 0           $self->_request_packet({
290             Opcode => PROTOCOL_BINARY_CMD_SASL_AUTH,
291             Key => join("", map sprintf('%x', $_), unpack("C*", 'PLAIN')),
292             Value => $login . '00' . $login . '00' . $password,
293             });
294            
295 0           $data = $self->_read_responce();
296 0           my $mess = unpack('A*', $$data{Value});
297 0           $self->_log("Server answer: $mess");
298 0 0         if ( $mess ne 'Authenticated' ) {
299 0           $self->set_err("Auth failure");
300 0           $self->_log("Auth failure with login: " . $self->login, ERROR_FLAG);
301 0           return;
302             }
303            
304 0           return 1;
305             }
306            
307             sub _request_packet {
308 0     0     my $self = shift;
309 0   0       my $data = shift || return;
310            
311 0   0       my $socket = $self->_get_socket() || return;
312            
313 0           $$data{Magic} = sprintf '%x', PROTOCOL_BINARY_REQ;
314 0           $$data{Opcode} = sprintf '%x', $$data{Opcode};
315 0           $$data{DataType} = '00'; # Raw bytes
316            
317 0   0       $$data{Extras} //= '';
318 0   0       $$data{Key} //= '';
319 0   0       $$data{Value} //= '';
320            
321 0           $$data{ExtrasLength} = length($$data{Extras}) / 2;
322 0           $$data{KeyLength} = length($$data{Key}) / 2;
323 0           $$data{TotalBodyLength} = length($$data{Value}) / 2 + $$data{ExtrasLength} + $$data{KeyLength};
324            
325 0   0       $$data{Opcode} = sprintf '%02d', ( $$data{Opcode} // '00' );
326 0   0       $$data{KeyLength} = sprintf '%04x', ( $$data{KeyLength} // '0000' );
327 0   0       $$data{ExtrasLength} = sprintf '%02x', ( $$data{ExtrasLength} // '00' );
328 0   0       $$data{VbucketId} = sprintf '%04d', ( $$data{VbucketId} // '0000' );
329 0   0       $$data{TotalBodyLength} = sprintf '%08x', ( $$data{TotalBodyLength} // '00000000' );
330 0   0       $$data{Opaque} = sprintf '%08d', ( $$data{Opaque} // '00000000' );
331 0   0       $$data{CAS} = sprintf '%016d', ( $$data{CAS} // '0000000000000000' );
332            
333 0           my $packet = join "", map( $$data{$_}, (qw(Magic Opcode KeyLength ExtrasLength DataType VbucketId TotalBodyLength Opaque CAS Extras Key Value)));
334            
335 0           $self->_log(">> $packet");
336 0           send($socket, pack('H*', $packet), 0);
337            
338 0           return 1;
339             }
340            
341             sub _read_responce {
342 0     0     my $self = shift;
343            
344 0   0       my $socket = $self->_get_socket() || return;
345            
346 0           my %data;
347            
348 0           eval {
349 0     0     local $SIG{ALRM} = sub { die "alarm\n" };
  0            
350 0           alarm $self->timeout;
351            
352 0           sysread($socket, $data{Magic}, 1);
353 0           sysread($socket, $data{Opcode}, 1);
354 0           sysread($socket, $data{KeyLength}, 2);
355 0           sysread($socket, $data{ExtrasLength}, 1);
356 0           sysread($socket, $data{DataType}, 1);
357 0           sysread($socket, $data{Status}, 2);
358 0           sysread($socket, $data{TotalBodyLength}, 4);
359 0           sysread($socket, $data{Opaque}, 4);
360 0           sysread($socket, $data{CAS}, 8);
361            
362 0           $data{Key} = '';
363 0           $data{Extras} = '';
364 0           $data{Value} = '';
365            
366 0           my $len_key = sprintf('%d', unpack( 'H*', $data{KeyLength} ));
367 0           my $len_extras = hex(unpack( 'H*', $data{ExtrasLength} ));
368 0   0       my $len_total = unpack('N*', $data{TotalBodyLength}) || 0;
369 0           my $len_value = $len_total - $len_extras - $len_key;
370            
371 0 0         if ($len_extras) {
372 0           sysread($socket, $data{Extras}, $len_extras);
373             }
374            
375 0 0         if ($len_key) {
376 0           sysread($socket, $data{Key}, $len_key);
377             }
378            
379 0 0         if ($len_value) {
380 0           sysread($socket, $data{Value}, $len_value);
381             }
382            
383 0           $data{MagicUnpack} = hex(unpack('H*', $data{Magic}));
384 0           $data{OpcodeUnpack} = hex(unpack('H*', $data{Opcode}));
385 0           $data{StatusUnpack} = unpack('n1', $data{Status});
386 0           $data{ValueUnpack} = unpack('A*', $data{Value});
387            
388 0           alarm 0;
389             };
390            
391 0           $self->_log("<< " . ( join "", map { unpack('H*', $data{$_}) } (qw(Magic Opcode KeyLength ExtrasLength DataType Status TotalBodyLength Opaque CAS Extras Key Value)) ));
  0            
392            
393 0 0         if ( $data{OpcodeUnpack} == PROTOCOL_BINARY_CMD_GET ) { # clean binary data from Value section, maybe big data
394 0           $data{Value} = '';
395             }
396            
397 0           return \%data;
398             }
399            
400             sub _get_socket {
401 0     0     my $self = shift;
402            
403 0 0 0       return $self->socket if $self->socket && $self->socket->connected();
404 0           return $self->_connect();
405             }
406            
407             sub _connect {
408 0     0     my $self = shift;
409            
410 0           my $socket = IO::Socket::INET->new(
411             PeerAddr => $self->host,
412             PeerPort => $self->port,
413             Proto => 'tcp',
414             Timeout => $self->timeout,
415             Type => SOCK_STREAM
416             );
417            
418 0           my $error = $!;
419 0 0         if ($error) {Encode::_utf8_on($error);}
  0            
420            
421 0           $self->_log( sprintf "Connect to host: %s, port: %s, timeout: %s", $self->host, $self->port, $self->timeout );
422 0 0         $self->_log("result: " . ($socket ? 'success' : 'error: ' . $error));
423            
424 0 0         if ($socket) {
425 0           binmode $socket;
426 0           $self->set_socket($socket);
427 0           return $socket;
428             }
429            
430 0           $self->set_err($!);
431 0           $self->_log( sprintf("Don't connect to %s:%s, error: %s", $self->host, $self->port, $error), ERROR_FLAG );
432 0           return;
433             }
434            
435             sub _socket_destroy {
436 0     0     my $self = shift;
437 0           $self->_log("socket destroy " . $self->socket);
438 0 0         close($self->socket) if $self->socket;
439 0           $self->set_socket(undef);
440 0           return;
441             }
442            
443             sub _log {
444 0     0     my ($self, $str, $flag) = @_;
445 0 0         return unless $str;
446            
447 0   0       $flag //= 0;
448            
449 0 0         if (! $self->logger) {
450 0     0     $self->set_logger( sub { print STDERR shift() . "\n" } );
  0            
451             }
452            
453 0 0         if (ref $self->logger ne 'CODE') {
454 0           print STDERR "param 'logger' has type of reference not function\n";
455 0           return;
456             }
457            
458 0 0 0       if ($self->debug || $flag == ERROR_FLAG) {
459 0           $self->logger->($str);
460             }
461            
462 0           return;
463             }
464            
465 0     0 0   sub host {return $_[0]->{host}}
466 0     0 0   sub port {return $_[0]->{port}}
467 0     0 0   sub timeout {return $_[0]->{timeout}}
468 0     0 0   sub login {return $_[0]->{login}}
469 0     0 0   sub password {return $_[0]->{password}}
470 0     0 0   sub socket {return $_[0]->{socket}}
471 0     0 0   sub debug {return $_[0]->{debug}}
472 0     0 0   sub logger {return $_[0]->{logger}}
473 0     0 0   sub err {return $_[0]->{err}}
474            
475 0     0 0   sub set_host { $_[0]->{err} = $_[1]; }
476 0     0 0   sub set_port { $_[0]->{port} = $_[1]; }
477 0     0 0   sub set_timeout { $_[0]->{timeout} = $_[1]; }
478 0     0 0   sub set_login { $_[0]->{login} = $_[1]; }
479 0     0 0   sub set_password { $_[0]->{password} = $_[1]; }
480 0     0 0   sub set_socket { $_[0]->{socket} = $_[1]; }
481 0     0 0   sub set_debug { $_[0]->{debug} = $_[1]; }
482 0     0 0   sub set_logger { $_[0]->{logger} = $_[1]; }
483 0     0 0   sub set_err { $_[0]->{err} = $_[1]; }
484            
485             1;
486             __END__