package MR::Tarantool::Box::Singleton; =pod =head1 NAME MR::Tarantool::Box::Singleton - A singleton wrapper for L<MR::Tarantool::Box>. Provides connection-persistence and replica fallback. Please read L<"MR::Tarantool::Box manual"|MR::Tarantool::Box> first. =head1 SYNOPSIS package Some::Tarantool::Box::Singleton; use MR::Tarantool::Box::Singleton; use base 'MR::Tarantool::Box::Singleton'; BEGIN { # generates "TUPLE_$field_name" constants, and methods: FIELDS, FIELDS_HASH __PACKAGE__->mkfields(qw/ id f1 f2 f3 field4 f5 f6 f7 misc_string /); # applicable for DEFAULT_SPACE only } sub SERVER { Some::Config->GetBoxServer() } sub REPLICAS { Some::Config->GetBoxReplicas() } sub DEFAULT_SPACE { 0 } sub SPACES {[{ space => 0, indexes => [ { index_name => 'primary_id', keys => [TUPLE_id], }, { index_name => 'secondary_f1f2', keys => [TUPLE_f1, TUPLE_f2], }, ], format => 'QqLlSsCc&', default_index => 'primary_id', }, { space => 1, indexes => [ { index_name => 'primary_id', keys => [0], }, ], format => '&&&&', fields => [qw/ string1 str2 s3 s4 /], }]} =head1 DESCRIPTION =head2 METHODS =cut use strict; use warnings; use MR::Tarantool::Box; use Class::Singleton; use Carp qw/confess cluck/; use List::Util qw/shuffle/; use base qw/Class::Singleton/; =pod =head3 mkfields BEGIN { $CLASS->mkfields(@names); } =over =item * Generates constants "TUPLE_$fieldname" => $fieldposition in C<$CLASS>. Just Like if you say C<< use constant TUPLE_id => 0, TUPLE_f1 => 1, ...; >> =item * Generates C<$CLASS> variable C<< @fields >> containing field names, and a C<$CLASS> method C<FIELDS> returning C<< @fields >>. =item * Generates C<$CLASS> variable C<< %fields >> containing field names mapping to positions, and a C<$CLASS> method C<FIELDS_HASH> returning C<< \%fields >>. =item * These C<< @fields >> are applied to the C<< DEFAULT_SPACE >>, if I<< fields >> were not set explicitly for that space. =back =cut sub mkfields { my($class, @fields) = @_; no strict 'refs'; confess "Fields are already defined for $class" if @{"${class}::fields"}; @{"${class}::fields"} = @fields; %{"${class}::fields"} = map { $fields[$_] => $_ } 0..$#fields; eval qq{ sub ${class}::TUPLE_$fields[$_] () { $_ } } for 0..$#fields; eval qq{ sub ${class}::FIELDS () { \@${class}::fields } }; eval qq{ sub ${class}::FIELDS_HASH () { \\\%${class}::fields } }; } =pod =head3 declare_stored_procedure $CLASS->declare_stored_procedure(%args); $CLASS->declare_stored_procedure( name => "box.do.something", # internal procedure name, in da box method_name => "CallMyTestingStoredProcedure", # will generate method named options => { default => options }, # MR::Tarantool::Box->Call \%options params => [ qw{ P1 P2 P3 Param4 }], # names unpack_format => [qw/ & L S C /], params_format => [qw{ C S L a* }], params_default => [ 1, 2, undef, 'the_default' ], # undef's are mandatory params ); ... my $data = $CLASS->CallMyTestingStoredProcedure( P1 => $val1, P2 => $val2, P3 => $val3, Param4 => $val3, { option => $value }, # optional ) or warn $CLASS->ErrorStr; Declare a stored procedure. This generates C<$CLASS> method C<< $args{method_name} >> which calls Tarantool/Box procedure C<< $args{name} >>, using C<< $args{options} >> as default C<< \%options >> for C<< MR::Tarantool::Box->Call >> call. The generated method has the following prototype: $CLASS->CallMyTestingStoredProcedure( %sp_params, \%optional_options ); Parameters description: =over =item B<%args>: =over =item B<name> => $tarantool_box_sp_name The name of procedure in Tarantool/Box to call. =item B<method_name> => $class_method_name Class method name to generate. =item B<options> => \%options Options to pass to L<MR::Taranatool::Box->Call|MR::Taranatool::Box/Call> method. =item B<params> => \@names Procedure input parameters' names =item B<params_default> => \@defaults Procedure input parameters default values. Undefined or absent value makes its parameter mandatory. =item B<params_format> => \@format C<< pack() >>-compatible format to pack input parameters. Must match C<params>. =item B<unpack_format> => \@format C<< pack() >>-compatible format to unpack procedure output. =back =item B<%sp_params>: C<< Name => $value >> pairs. =item B<%optional_options>: Options to pass to L<MR::Taranatool::Box->Call|MR::Taranatool::Box/Call> method. This overrides C<< %options >> values key-by-key. =back =cut sub declare_stored_procedure { my($class, %opts) = @_; my $name = delete $opts{name} or confess "No `name` given"; my $options = $opts{options} || {}; confess "no `params` given; it must be an arrayref" if !exists $opts{params} or ref $opts{params} ne 'ARRAY'; my @params = @{$opts{params}}; my $pack; if(my $fn = $opts{pack}) { confess "`params_format` and `params_default` are not applicable while `pack` is in use" if exists $opts{params_format} or exists $opts{params_default}; if(ref $fn) { confess "`pack` can be code ref or a method name, nothing else" unless ref $fn eq 'CODE'; $pack = $fn; } else { confess "`pack` method $fn is not provided by class ${class}" unless $class->can($fn); $pack = sub { $class->$fn(@_) }; } } else { 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; 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); my @fmt = @{$opts{params_format}}; my @def = @{$opts{params_default}||[]}; $pack = sub { my $p = $_[0]; for my $i (0..$#params) { $p->[$i] = $def[$i] if !defined$p->[$i] and $i < @def; confess "All params must be defined" unless defined $p->[$i]; $p->[$i] = pack $fmt[$i], $p->[$i]; } return $p; }; } my $unpack; if(my $fn = $opts{unpack}) { if(ref $fn) { confess "`unpack` can be code ref or a method name, nothing else" unless ref $fn eq 'CODE'; $unpack = $fn; } else { confess "`unpack` method $fn is not provided by class ${class}" unless $class->can($fn); $unpack = sub { $class->$fn(@_) }; } $options->{unpack_format} = [ "a*" ]; } else { confess "no `unpack` nor `unpack_format` given; it must be an arrayref" if !exists $opts{unpack_format} or ref $opts{unpack_format} ne 'ARRAY'; my $f = $opts{unpack_format}; $options->{unpack_format} = $f; } my $method = $opts{method_name} or confess "`method_name` not given"; confess "bad `method_name` $method" unless $method =~ m/^[a-zA-Z]\w*$/; my $fn = "${class}::${method}"; confess "Method $method id already defined in class $class" if defined &{$fn}; do { no strict 'refs'; *$fn = sub { my $p0 = @_ && ref $_[-1] eq 'HASH' ? pop : {}; my $param = { %$options, %$p0 }; my ($class, %params) = @_; my $res = $class->Call($name, $pack->([@params{@params}]), $param) or return; return $res unless $unpack; return $unpack->($res); } }; return $method; } sub Param { confess "bad Param call" unless $_[2]; return $_[2] && @{$_[2]} && ref $_[2]->[-1] eq 'HASH' && pop @{$_[2]} || {}; } =pod =head3 Configuration methods =over =item B<SERVER> Must return a string of ip:port of I<master> server. =item B<REPLICAS> Must return a comma separated string of ip:port pairs of I<replica> servers (see L</is_replica>). Server is chosen from the list randomly. =item B<MR_TARANTOOL_BOX_CLASS> Must return name of the class implementing L<MR::Tarantool::Box> interface, or it's descendant. =item B<SPACES>, B<RAISE>, B<TIMEOUT>, B<SELECT_TIMEOUT>, B<RETRY>, B<SELECT_RETRY>, B<SOFT_RETRY>, B<DEBUG> See corresponding arguments of L<MR::Tarantool::Box->new|MR::Tarantool::Box/new> method. =back =cut sub DEBUG () { 0 } sub IPDEBUG () { 0 } sub TIMEOUT () { 23 } sub SELECT_TIMEOUT () { 2 } sub RAISE () { 1 } sub RETRY () { 1 } sub SELECT_RETRY () { 3 } sub SOFT_RETRY () { 3 } sub RETRY_DELAY () { 1 } sub SERVER () { die } sub REPLICAS () { [] } sub MR_TARANTOOL_BOX_CLASS () { 'MR::Tarantool::Box' } sub SPACES () { die } sub DEFAULT_SPACE () { undef } sub _new_instance { my ($class) = @_; my ($config) = $class->can('_config') ? $class->_config : {}; $config->{param} ||= {}; $config->{servers} ||= $class->SERVER; $config->{param}->{name} ||= $class; $config->{param}->{spaces} ||= my $sp = $class->SPACES; $config->{param}->{default_space} ||= my $defsp = @$sp == 1 ? 0 : $class->DEFAULT_SPACE; $sp->[$defsp]->{fields} ||= [ $class->FIELDS ] if $class->can('FIELDS'); $config->{param}->{raise} = $class->RAISE unless defined $config->{param}->{raise}; $config->{param}->{timeout} ||= $class->TIMEOUT; $config->{param}->{select_timeout} ||= $class->SELECT_TIMEOUT; $config->{param}->{debug} ||= $class->DEBUG; $config->{param}->{ipdebug} ||= $class->IPDEBUG; $config->{param}->{retry} ||= $class->RETRY; $config->{param}->{select_retry} ||= $class->SELECT_RETRY; $config->{param}->{softretry} ||= $class->SOFT_RETRY; $config->{param}->{retry_delay} ||= $class->RETRY_DELAY; $config->{param}->{fields} ||= [ $class->FIELDS ]; my $replicas = delete $config->{replicas} || $class->REPLICAS || []; $replicas = [ split /,/, $replicas ] unless ref $replicas eq 'ARRAY'; return bless { box => $class->MR_TARANTOOL_BOX_CLASS->new({ servers => $config->{servers}, %{$config->{param}} }), replicas => [ map { $class->MR_TARANTOOL_BOX_CLASS->new({ servers => $_, %{$config->{param}} }) } shuffle @$replicas ], }, $class; } =pod =head3 Add, Insert, Replace, UpdateMulti, Delete These methods operate on C<< SERVER >> only. See corresponding methods of L<MR::Tarantool::Box> class. =head3 Select, Call These methods operate on C<< SERVER >> at first, and then B<may> try to query C<< REPLICAS >>. See corresponding methods of L<MR::Tarantool::Box> class. These methods have additional C<< %options >> params: =over =item B<is_replica> => \$is_result_from_replica If this option is set, then if the query to C<< SERVER >> fails, C<< REPLICAS >> will be queried one-by-one until query succeeds or the list ends, and C<< $is_result_from_replica >> will be set to C<< true >>, no matter whether any query succeeds or not. =back =cut BEGIN { foreach my $method (qw/Insert UpdateMulti Delete Add Set Replace Bit Num AndXorAdd Update/) { no strict 'refs'; *$method = sub { use strict; my ($class, @args) = @_; my $param = $class->Param($method, \@args); my $self = $class->instance; $self->{_last_box} = $self->{box}; $self->{box}->$method(@args, $param); }; } foreach my $method (qw/Select SelectUnion Call/) { no strict 'refs'; *$method = sub { use strict; my ($class, @args) = @_; my $param = $class->Param($method, \@args); if ($param->{format}) { my @F; my $F = $class->FIELDS_HASH; my @format = ref $param->{format} eq 'ARRAY' ? @{$param->{format}} : %{$param->{format}}; confess "Odd number of elements in format" if @format % 2; $param->{format} = []; while( my ($field, $fmt) = splice(@format, 0, 2) ) { confess "Bad format for field `$field'" unless $fmt; confess "Unknown field `$field'" unless exists $F->{$field}; push @F, $field; push @{$param->{format}}, { field => $F->{$field}, $fmt eq 'full' ? ( full => 1, ) : ( offset => $fmt->{offset} || 0, length => (exists $fmt->{length} ? $fmt->{length}||0 : 'max'), ), }; } $param->{hashify} = sub { $class->_hashify(\@F, @_) }; } die "${class}\->${method}: is_replica must be a SCALARREF" if exists $param->{is_replica} && ref $param->{is_replica} ne 'SCALAR'; my $is_rep = delete $param->{is_replica}; $$is_rep = 0 if $is_rep; my $self = $class->instance; my @rep = $is_rep ? @{ $self->{replicas} } : (); my ($ret,@ret); for(my $box = $self->{box}; $box; $box = shift @rep) { $self->{_last_box} = $box; if(wantarray) { @ret = $box->$method(@args, $param); } elsif(defined wantarray) { $ret = $box->$method(@args, $param); } else { $box->$method(@args, $param); } last if !$box->Error or !$is_rep or !@rep; ++$$is_rep; } return wantarray ? @ret : $ret; }; } } =pod =head3 B<Error>, B<ErrorStr> Return error code or description (see <MR::Tarantool::Box|MR::Tarantool::Box/Error>). =cut sub Error { my ($class, @args) = @_; $class->instance->{_last_box}->Error(@args); } sub ErrorStr { my ($class, @args) = @_; $class->instance->{_last_box}->ErrorStr(@args); } =pod =head1 LICENCE AND COPYRIGHT This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =head1 SEE ALSO L<http://tarantool.org> L<MR::Tarantool::Box> =cut 1;