File Coverage

blib/lib/MR/Tarantool/Box/Singleton.pm
Criterion Covered Total %
statement 43 189 22.7
branch 0 92 0.0
condition 0 82 0.0
subroutine 14 34 41.1
pod 7 11 63.6
total 64 408 15.6


line stmt bran cond sub pod time code
1             package MR::Tarantool::Box::Singleton;
2              
3             =pod
4              
5             =head1 NAME
6              
7             MR::Tarantool::Box::Singleton - A singleton wrapper for L.
8              
9             Provides connection-persistence and replica fallback.
10             Please read L<"MR::Tarantool::Box manual"|MR::Tarantool::Box> first.
11              
12             =head1 SYNOPSIS
13              
14             package Some::Tarantool::Box::Singleton;
15             use MR::Tarantool::Box::Singleton;
16             use base 'MR::Tarantool::Box::Singleton';
17              
18             BEGIN { # generates "TUPLE_$field_name" constants, and methods: FIELDS, FIELDS_HASH
19             __PACKAGE__->mkfields(qw/ id f1 f2 f3 field4 f5 f6 f7 misc_string /); # applicable for DEFAULT_SPACE only
20             }
21              
22             sub SERVER { Some::Config->GetBoxServer() }
23             sub REPLICAS { Some::Config->GetBoxReplicas() }
24              
25             sub DEFAULT_SPACE { 0 }
26              
27             sub SPACES {[{
28             space => 0,
29             indexes => [ {
30             index_name => 'primary_id',
31             keys => [TUPLE_id],
32             }, {
33             index_name => 'secondary_f1f2',
34             keys => [TUPLE_f1, TUPLE_f2],
35             }, ],
36             format => 'QqLlSsCc&',
37             default_index => 'primary_id',
38             }, {
39             space => 1,
40             indexes => [ {
41             index_name => 'primary_id',
42             keys => [0],
43             }, ],
44             format => '&&&&',
45             fields => [qw/ string1 str2 s3 s4 /],
46             }]}
47              
48             =head1 DESCRIPTION
49              
50             =head2 METHODS
51              
52             =cut
53              
54 1     1   2610 use strict;
  1         3  
  1         104  
55 1     1   10 use warnings;
  1         6  
  1         40  
56              
57 1     1   6 use MR::Tarantool::Box;
  1         2  
  1         27  
58 1     1   6441 use Class::Singleton;
  1         581  
  1         39  
59 1     1   7 use Carp qw/confess cluck/;
  1         4  
  1         86  
60 1     1   5 use List::Util qw/shuffle/;
  1         3  
  1         121  
61              
62 1     1   7 use base qw/Class::Singleton/;
  1         2  
  1         182  
63              
64             =pod
65              
66             =head3 mkfields
67              
68             BEGIN {
69             $CLASS->mkfields(@names);
70             }
71              
72             =over
73              
74             =item *
75              
76             Generates constants "TUPLE_$fieldname" => $fieldposition in C<$CLASS>.
77             Just Like if you say C<< use constant TUPLE_id => 0, TUPLE_f1 => 1, ...; >>
78              
79             =item *
80              
81             Generates C<$CLASS> variable C<< @fields >> containing field names,
82             and a C<$CLASS> method C returning C<< @fields >>.
83              
84             =item *
85              
86             Generates C<$CLASS> variable C<< %fields >> containing field names mapping to positions,
87             and a C<$CLASS> method C returning C<< \%fields >>.
88              
89             =item *
90              
91             These C<< @fields >> are applied to the C<< DEFAULT_SPACE >>,
92             if I<< fields >> were not set explicitly for that space.
93              
94             =back
95              
96             =cut
97              
98             sub _mkfields {
99 0     0     my($class, $f, $F, @fields) = @_;
100 1     1   7 no strict 'refs';
  1         3  
  1         1162  
101 0 0         confess "$f are already defined for $class" if @{"${class}::${f}"};
  0            
102 0           @{"${class}::${f}"} = @fields;
  0            
103 0           %{"${class}::${f}"} = map { $fields[$_] => $_ } 0..$#fields;
  0            
  0            
104 0           eval qq{ sub ${class}::${F}TUPLE_$fields[$_] () { $_ } } for 0..$#fields;
105 0           eval qq{ sub ${class}::${F}FIELDS () { \@${class}::${f} } };
106 0           eval qq{ sub ${class}::${F}FIELDS_HASH () { \\\%${class}::${f} } };
107             }
108              
109 0     0 1   sub mkfields { $_[0]->_mkfields('fields', '', @_[1..$#_]) }
110 0     0 0   sub mklongfields { $_[0]->_mkfields('long_fields', 'LONG_', @_[1..$#_]) }
111              
112             =pod
113              
114             =head3 declare_stored_procedure
115              
116             $CLASS->declare_stored_procedure(%args);
117            
118             $CLASS->declare_stored_procedure(
119             name => "box.do.something", # internal procedure name, in da box
120             method_name => "CallMyTestingStoredProcedure", # will generate method named
121             options => { default => options }, # MR::Tarantool::Box->Call \%options
122             params => [ qw{ P1 P2 P3 Param4 }], # names
123            
124             unpack_format => "&LSC(L$)*",
125            
126             params_format => [qw{ C S L a* }],
127             params_default => [ 1, 2, undef, 'the_default' ], # undef's are mandatory params
128             );
129            
130             ...
131            
132             my $data = $CLASS->CallMyTestingStoredProcedure(
133             P1 => $val1,
134             P2 => $val2,
135             P3 => $val3,
136             Param4 => $val3,
137             { option => $value }, # optional
138             ) or warn $CLASS->ErrorStr;
139              
140             Declare a stored procedure. This generates C<$CLASS> method C<< $args{method_name} >> which
141             calls Tarantool/Box procedure C<< $args{name} >>, using C<< $args{options} >> as default
142             C<< \%options >> for C<< MR::Tarantool::Box->Call >> call. The generated method has the following
143             prototype:
144              
145             $CLASS->CallMyTestingStoredProcedure( %sp_params, \%optional_options );
146              
147             Parameters description:
148              
149             =over
150              
151             =item B<%args>:
152              
153             =over
154              
155             =item B => $tarantool_box_sp_name
156              
157             The name of procedure in Tarantool/Box to call.
158              
159             =item B => $class_method_name
160              
161             Class method name to generate.
162              
163             =item B => \%options
164              
165             Options to pass to LCall|MR::Taranatool::Box/Call> method.
166              
167             =item B => \@names
168              
169             Procedure input parameters' names
170              
171             =item B => \@defaults
172              
173             Procedure input parameters default values. Undefined or absent value makes
174             its parameter mandatory.
175              
176             =item B => \@format
177              
178             C<< pack() >>-compatible format to pack input parameters. Must match C.
179              
180             =item B => $format
181              
182             C<< pack() >>-compatible format to unpack procedure output.
183              
184             =back
185              
186             =item B<%sp_params>:
187              
188             C<< Name => $value >> pairs.
189              
190             =item B<%optional_options>:
191              
192             Options to pass to LCall|MR::Taranatool::Box/Call> method.
193             This overrides C<< %options >> values key-by-key.
194              
195             =back
196              
197             =cut
198              
199             sub declare_stored_procedure {
200 0     0 1   my($class, %opts) = @_;
201 0 0         my $name = delete $opts{name} or confess "No `name` given";
202 0   0       my $options = $opts{options} || {};
203              
204 0 0 0       confess "no `params` given; it must be an arrayref" if !exists $opts{params} or ref $opts{params} ne 'ARRAY';
205 0           my @params = @{$opts{params}};
  0            
206              
207 0           my $pack;
208 0 0         if(my $fn = $opts{pack}) {
209 0 0 0       confess "`params_format` and `params_default` are not applicable while `pack` is in use" if exists $opts{params_format} or exists $opts{params_default};
210 0 0         if(ref $fn) {
211 0 0         confess "`pack` can be code ref or a method name, nothing else" unless ref $fn eq 'CODE';
212 0           $pack = $fn;
213             } else {
214 0 0         confess "`pack` method $fn is not provided by class ${class}" unless $class->can($fn);
215 0     0     $pack = sub { $class->$fn(@_) };
  0            
216             }
217             } else {
218 0 0 0       confess "no `pack` nor `params_format` given; it must be an arrayref with number of elements exactly as in `params`" if !exists $opts{params_format} or ref $opts{params_format} ne 'ARRAY' or @{$opts{params_format}} != @params;
  0   0        
219 0 0 0       confess "`params_default` is given but it must be an arrayref with number of elements no more then in `params`" if exists $opts{params_format} and (ref $opts{params_format} ne 'ARRAY' or @{$opts{params_format}} > @params);
      0        
220 0           my @fmt = @{$opts{params_format}};
  0            
221 0 0         my @def = @{$opts{params_default}||[]};
  0            
222             $pack = sub {
223 0     0     my $p = $_[0];
224 0           for my $i (0..$#params) {
225 0 0 0       $p->[$i] = $def[$i] if !defined$p->[$i] and $i < @def;
226 0 0         confess "All params must be defined" unless defined $p->[$i];
227 0           $p->[$i] = pack $fmt[$i], $p->[$i];
228             }
229 0           return $p;
230 0           };
231             }
232              
233 0           my $unpack;
234 0 0         if(my $fn = $opts{unpack}) {
235 0 0         if(ref $fn) {
236 0 0         confess "`unpack` can be code ref or a method name, nothing else" unless ref $fn eq 'CODE';
237 0           $unpack = $fn;
238             } else {
239 0 0         confess "`unpack` method $fn is not provided by class ${class}" unless $class->can($fn);
240 0     0     $unpack = sub { $class->$fn(@_) };
  0            
241             }
242 0 0         if ($opts{unpack_raw}) {
243 0           $options->{unpack} = $unpack;
244 0           undef $unpack;
245             }
246 0           $options->{unpack_format} = '&*';
247             } else {
248 0 0         confess "no `unpack` nor `unpack_format` given" if !exists $opts{unpack_format};
249 0           my $f = $opts{unpack_format};
250 0 0         $f = join '', @$f if ref $f;
251 0           $options->{unpack_format} = $f;
252             }
253              
254 0 0         my $method = $opts{method_name} or confess "`method_name` not given";
255 0 0         confess "bad `method_name` $method" unless $method =~ m/^[a-zA-Z]\w*$/;
256 0           my $fn = "${class}::${method}";
257 0 0         confess "Method $method is already defined in class $class" if defined &{$fn};
  0            
258 0           do {
259 1     1   8 no strict 'refs';
  1         7  
  1         1113  
260             *$fn = sub {
261 0 0 0 0     my $p0 = @_ && ref $_[-1] eq 'HASH' ? pop : {};
262 0           my $param = { %$options, %$p0 };
263 0           my ($class, %params) = @_;
264 0 0         my $res = $class->Call($name, $pack->([@params{@params}]), $param) or return;
265 0 0         return $res unless $unpack;
266 0           return $unpack->($res);
267             }
268 0           };
269 0           return $method;
270             }
271              
272             sub Param {
273 0 0   0 0   confess "bad Param call" unless $_[2];
274 0   0       return $_[2] && @{$_[2]} && ref $_[2]->[-1] eq 'HASH' && pop @{$_[2]} || {};
275             }
276              
277             =pod
278              
279             =head3 Configuration methods
280              
281             =over
282              
283             =item B
284              
285             Must return a string of ip:port of I server.
286              
287             =item B
288              
289             Must return a comma separated string of ip:port pairs of I servers (see L).
290             Server is chosen from the list randomly.
291              
292             =item B
293              
294             Must return name of the class implementing L interface, or it's descendant.
295              
296             =item B, B, B, B, B, B, B, B
297              
298             See corresponding arguments of Lnew|MR::Tarantool::Box/new> method.
299              
300             =back
301              
302             =cut
303              
304             sub DEBUG () { 0 }
305             sub IPDEBUG () { 0 }
306              
307             sub TIMEOUT () { 23 }
308             sub SELECT_TIMEOUT () { 2 }
309              
310             sub RAISE () { 1 }
311              
312             sub RETRY () { 1 }
313             sub SELECT_RETRY () { 3 }
314             sub SOFT_RETRY () { 3 }
315             sub RETRY_DELAY () { 1 }
316              
317 0     0 1   sub SERVER () { die }
318 0     0 1   sub REPLICAS () { [] }
319              
320             sub MR_TARANTOOL_BOX_CLASS () { 'MR::Tarantool::Box' }
321              
322 0     0 1   sub SPACES () { die }
323 0     0 0   sub DEFAULT_SPACE () { undef }
324              
325             sub _new_instance {
326 0     0     my ($class) = @_;
327 0 0         my ($config) = $class->can('_config') ? $class->_config : {};
328 0   0       $config->{param} ||= {};
329              
330 0   0       $config->{servers} ||= $class->SERVER;
331              
332 0   0       $config->{param}->{name} ||= $class;
333 0   0       $config->{param}->{spaces} ||= $class->SPACES;
334 0 0 0       $config->{param}->{default_fields} ||= [ $class->FIELDS ] if $class->can('FIELDS');
335 0 0 0       $config->{param}->{default_long_fields}||= [ $class->LONG_FIELDS ] if $class->can('LONG_FIELDS');
336              
337 0 0         $config->{param}->{raise} = $class->RAISE unless defined $config->{param}->{raise};
338 0   0       $config->{param}->{timeout} ||= $class->TIMEOUT;
339 0   0       $config->{param}->{select_timeout} ||= $class->SELECT_TIMEOUT;
340 0   0       $config->{param}->{debug} ||= $class->DEBUG;
341 0   0       $config->{param}->{ipdebug} ||= $class->IPDEBUG;
342              
343 0   0       $config->{param}->{retry} ||= $class->RETRY;
344 0   0       $config->{param}->{select_retry} ||= $class->SELECT_RETRY;
345 0   0       $config->{param}->{softretry} ||= $class->SOFT_RETRY;
346 0   0       $config->{param}->{retry_delay} ||= $class->RETRY_DELAY;
347              
348 0   0       my $replicas = delete $config->{replicas} || $class->REPLICAS || [];
349 0 0         $replicas = [ split /,/, $replicas ] unless ref $replicas eq 'ARRAY';
350              
351 0           $class->CheckConfig($config);
352              
353 0           return bless {
354 0           box => $class->MR_TARANTOOL_BOX_CLASS->new({ servers => $config->{servers}, %{$config->{param}} }),
355 0           replicas => [ map { $class->MR_TARANTOOL_BOX_CLASS->new({ servers => $_, %{$config->{param}} }) } shuffle @$replicas ],
  0            
356             }, $class;
357             }
358              
359 0     0 0   sub CheckConfig {}
360              
361             =pod
362              
363             =head3 Add, Insert, Replace, UpdateMulti, Delete
364              
365             These methods operate on C<< SERVER >> only.
366             See corresponding methods of L class.
367              
368             =head3 Select, Call
369              
370             These methods operate on C<< SERVER >> at first, and then B
371             try to query C<< REPLICAS >>.
372              
373             See corresponding methods of L class.
374              
375             These methods have additional C<< %options >> params:
376              
377             =over
378              
379             =item B => \$is_result_from_replica
380              
381             If this option is set, then if the query to C<< SERVER >> fails,
382             C<< REPLICAS >> will be queried one-by-one until query succeeds or
383             the list ends, and C<< $is_result_from_replica >> will be set to
384             C<< true >>, no matter whether any query succeeds or not.
385              
386             =back
387              
388             =cut
389              
390             BEGIN {
391              
392 1     1   5 foreach my $method (qw/Insert UpdateMulti Delete Add Set Replace Bit Num AndXorAdd Update/) {
393 1     1   9 no strict 'refs';
  1         2  
  1         110  
394             *$method = sub {
395 1     1   7 use strict;
  1         11  
  1         136  
396 0     0   0 my ($class, @args) = @_;
397 0         0 my $param = $class->Param($method, \@args);
398 0         0 my $self = $class->instance;
399 0         0 $self->{_last_box} = $self->{box};
400 0         0 $self->{box}->$method(@args, $param);
401 10         100 };
402             }
403              
404 1         4 foreach my $method (qw/Select SelectUnion Call/) {
405 1     1   6 no strict 'refs';
  1         2  
  1         40  
406             *$method = sub {
407 1     1   6 use strict;
  1         3  
  1         567  
408 0     0     my ($class, @args) = @_;
409 0           my $param = $class->Param($method, \@args);
410              
411 0 0         if ($param->{format}) {
412 0           my @F;
413 0           my $F = $class->FIELDS_HASH;
414 0 0         my @format = ref $param->{format} eq 'ARRAY' ? @{$param->{format}} : %{$param->{format}};
  0            
  0            
415 0 0         confess "Odd number of elements in format" if @format % 2;
416 0           $param->{format} = [];
417 0           while( my ($field, $fmt) = splice(@format, 0, 2) ) {
418 0 0         confess "Bad format for field `$field'" unless $fmt;
419 0 0         confess "Unknown field `$field'" unless exists $F->{$field};
420 0           push @F, $field;
421 0 0 0       push @{$param->{format}}, {
  0 0 0        
422             field => $F->{$field},
423             $fmt eq 'full' ? (
424             full => 1,
425             ) : (
426             offset => $fmt->{offset} || 0,
427             length => (exists $fmt->{length} ? $fmt->{length}||0 : 'max'),
428             ),
429             };
430             }
431 0     0     $param->{hashify} = sub { $class->_hashify(\@F, @_) };
  0            
432             }
433              
434 0 0 0       die "${class}\->${method}: is_replica must be a SCALARREF" if exists $param->{is_replica} && ref $param->{is_replica} ne 'SCALAR';
435 0           my $is_rep = delete $param->{is_replica};
436 0 0         $$is_rep = 0 if $is_rep;
437 0           my $self = $class->instance;
438 0 0         my @rep = $is_rep ? @{ $self->{replicas} } : ();
  0            
439 0           my ($ret,@ret);
440 0           for(my $box = $self->{box}; $box; $box = shift @rep) {
441 0           $self->{_last_box} = $box;
442 0 0         if(wantarray) {
    0          
443 0           @ret = $box->$method(@args, $param);
444             } elsif(defined wantarray) {
445 0           $ret = $box->$method(@args, $param);
446             } else {
447 0           $box->$method(@args, $param);
448             }
449 0 0 0       last if !$box->Error or !$is_rep or !@rep;
      0        
450 0           ++$$is_rep;
451             }
452 0 0         return wantarray ? @ret : $ret;
453 3         247 };
454             }
455             }
456              
457             =pod
458              
459             =head3 B, B
460              
461             Return error code or description (see ).
462              
463             =cut
464              
465             sub Error {
466 0     0 1   my ($class, @args) = @_;
467 0           $class->instance->{_last_box}->Error(@args);
468             }
469              
470             sub ErrorStr {
471 0     0 1   my ($class, @args) = @_;
472 0           $class->instance->{_last_box}->ErrorStr(@args);
473             }
474              
475             =pod
476              
477             =head1 LICENCE AND COPYRIGHT
478              
479             This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
480              
481             =head1 SEE ALSO
482              
483             L
484              
485             L
486              
487             =cut
488              
489              
490             1;