File Coverage

blib/lib/Test/Tarantool.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package Test::Tarantool;
2              
3 1     1   19797 use 5.006;
  1         4  
  1         47  
4 1     1   12 use strict;
  1         3  
  1         33  
5 1     1   4 use warnings;
  1         5  
  1         36  
6 1     1   781 use IO::Handle qw/autoflush/;
  1         6795  
  1         67  
7 1     1   7 use Scalar::Util 'weaken';
  1         2  
  1         61  
8 1     1   333 use AnyEvent::Handle;
  0            
  0            
9             use Data::Dumper;
10              
11             =head1 NAME
12              
13             Test::Tarantool - The Swiss army knife for tests of Tarantool related Perl and lua code.
14              
15             =head1 VERSION
16              
17             Version 0.03
18              
19             =cut
20              
21             our $VERSION = '0.03';
22             our $Count = 0;
23             our %Schedule;
24              
25             =head1 SYNOPSIS
26              
27             use Test::Tarantool;
28             use AnyEvent;
29              
30             # Clear data and exit on Ctrl+C.
31             my $w = AnyEvent->signal (signal => "INT", cb => sub { exit 0 });
32              
33             my @shards = map {
34             my $n = $_;
35             Test::Tarantool->new(
36             host => '127.17.3.0',
37             spaces => 'space[0] = {
38             enabled = 1,
39             index = [ {
40             type = HASH,
41             unique = 1,
42             key_field = [ { fieldno = 0, type = STR }, ],
43             }, ],
44             }',
45             initlua => do {
46             open my $f, '<', 'init.lua';
47             local $/ = undef;
48             <$f> or "";
49             },
50             on_die => sub { warn "Shard #$n unexpectedly terminated\n"; exit; },
51             );
52             } 1..4;
53              
54             my @cluster = map { [ $_->{host}, $_->{p_port} ] } @shards;
55              
56             {
57             my $cv = AE::cv();
58             $cv->begin for (@shards);
59             $_->start($cv) for (@shards);
60             $cv->recv;
61             }
62              
63             {
64             $_->sync_start() for (@shards);
65             }
66              
67             {
68             my ($status, $reason) = $shards[0]->sync_ro();
69             die $reason unless $status;
70             print (($shards[0]->sync_admin_cmd("show info"))[1]);
71             }
72              
73             # Some test case here
74              
75             $shards[1]->pause();
76              
77             # Some test case here
78              
79             $shards[1]->resume();
80              
81             {
82             my ($status, $reason) = $shards[0]->sync_rw();
83             die $reason unless $status;
84             print (($shards[0]->sync_admin_cmd("show info"))[1]);
85             }
86              
87             # stop tarantools and clear work directoies
88             @shards = ();
89              
90             =head1 SUBROUTINES/METHODS
91              
92             =head2 new option => value,...
93              
94             Create new Tarantool instance. Every call of new method increase counter, below
95             called as I or I.
96              
97             =over 4
98              
99             =item root => $path
100              
101             Tarantool work directory. Default is I<./tnt_E10_random_lowercase_lettersE>
102              
103             =item arena => $size
104              
105             The maximal size of tarantool arena in Gb. Default is I<0.1>
106              
107             =item cleanup => $bool
108              
109             Remove tarantool work directory after garbage collection. Default is I<1>
110              
111             =item spaces => $string
112              
113             Tarantool spaces description. This is only one B argument.
114              
115             =item initlua => $content
116              
117             Content of init.lua file. Be default an empty file created.
118              
119             =item host => $address
120              
121             Address bind to. Default: I<127.0.0.1>
122              
123             =item port => $port
124              
125             Primary port number, base for s_port, a_port and r_port. Default is I<6603+EtnE*4>
126              
127             =item s_port => $port
128              
129             Read-only (secondary) port. Default is I
130              
131             =item a_port => $port
132              
133             Admin port. Default is I
134              
135             =item r_port => $port
136              
137             Replication port. Default is I
138              
139             =item title => $title
140              
141             Part of process name (custom_proc_title) Default is I<"yatEtnE">
142              
143             =item wal_mode => $mode
144              
145             The WAL write mode. See the desctiption of wal_mode tarantool variable. Default
146             is I. Look more about wal_mode in tarantool documentation.
147              
148             =item log_level => $number
149              
150             Tarantool log level. Default is I<5>
151              
152             =item snapshot => $path
153              
154             Path to some snapshot. If given the symbolic link to it will been created in
155             tarantool work directory.
156              
157             =item replication_source => $string
158              
159             If given the server is considered to be a Tarantool replica.
160              
161             =item logger => $sub
162              
163             An subroutine called at every time, when tarantool write some thing in a log.
164             The writed text passed as the first argument. Default is warn.
165              
166             =item on_die => $sub
167              
168             An subroutine called on a unexpected tarantool termination.
169              
170             =back
171              
172             =cut
173              
174             sub new {
175             my $class = shift; $class = (ref $class)? ref $class : $class;
176             # FIXME: must die if no spaces given
177             my $self = {
178             arena => 0.1,
179             cleanup => 1,
180             initlua => '-- init.lua --',
181             host => '127.0.0.1',
182             log_level => 5,
183             logger => sub { warn $_[0] },
184             on_die => sub { warn "Broken pipe, child is dead?"; },
185             port => 6603 + 4 * $Count, # FIXME: auto fitting needed
186             replication_source => '',
187             root => join("", ("tnt_", map { chr(97 + int(rand(26))) } 1..10)),
188             snapshot => '',
189             title => "yat" . $Count,
190             wal_mode => 'none',
191             @_,
192             }; $Count++;
193             $self->{p_port} = $self->{port};
194             $self->{s_port} ||= $self->{port} + 1;
195             $self->{a_port} ||= $self->{port} + 2;
196             $self->{r_port} ||= $self->{port} + 3;
197              
198             bless $self, $class;
199              
200             weaken ($Schedule{$self} = $self);
201              
202             mkdir($self->{root}); # FIXME: need error hadling
203              
204             $self->_config();
205             $self->_init_storage();
206             $self->_initlua();
207             $self;
208             }
209              
210             =head2 start option => $value, $cb->($status, $reason)
211              
212             Run tarantool instance.
213              
214             =over 4
215              
216             =item timeout => $timeout
217              
218             If not After $timeout seconds tarantool will been kelled by the KILL signal if
219             not started.
220              
221             =back
222              
223             =cut
224              
225             sub start {
226             my $self = shift;
227             my $cb = pop;
228             my %arg = (
229             timeout => 60,
230             @_
231             );
232              
233             return $cb->(0, 'Already running') if($self->{pid});
234              
235             pipe my $cr, my $pw or die "pipe filed: $!";
236             pipe my $pr, my $cw or die "pipe filed: $!";
237             autoflush($_) for ($pr, $pw, $cr, $cw);
238              
239             return $cb->(0, "Can't fork: $!") unless defined(my $pid = fork);
240             if ($pid) {
241             close($_) for ($cr, $cw);
242             $self->{pid} = $pid;
243             $self->{rpipe} = $pr;
244             $self->{wpipe} = $pw;
245             $self->{nanny} = AnyEvent->child(
246             pid => $pid,
247             cb => sub {
248             $self->{$_} = undef for qw/pid asleep rpipe wpipe nanny/;
249             # call on_die only for unexpected termination
250             if($self->{dying}) {
251             delete $self->{dying};
252             } else {
253             $self->{on_die}->($self, @_);
254             }
255             });
256             $self->{rh} = AnyEvent::Handle->new(
257             fh => $pr,
258             on_read => sub { $self->{logger}->(delete $_[0]->{rbuf}) },
259             on_error => sub {
260             kill 9, $self->{pid} if ($self->{pid} and kill 0, $self->{pid});
261             },
262             );
263             my $i = int($arg{timeout} / 0.1);
264             $self->{start_timer} = AnyEvent->timer(
265             after => 0.01,
266             interval => 0.1,
267             cb => sub {
268             unless ($self->{pid}) {
269             $self->{start_timer} = undef;
270             $cb->(0, "Process unexpectedly terminated");
271             }
272             open my $fh, "<", "/proc/$self->{pid}/cmdline" or
273             do { $self->{start_timer} = undef; return $cb->(0, "Tarantool died"); };
274             my $status = $self->{replication_source} ? "replica" : "primary";
275             if (<$fh> =~ /$status/) {
276             $self->{start_timer} = undef;
277             $cb->(1, "OK");
278             }
279             unless($i > 0) {
280             kill TERM => $self->{pid};
281             $self->{start_timer} = undef;
282             $cb->(0, "Timeout exceeding. Process terminated");
283             }
284             $i--;
285             }
286             );
287             } else {
288             close($_) for ($pr, $pw);
289             chdir $self->{root};
290             open(STDIN, "<&", $cr) or die "Could not dup filehandle: $!";
291             open(STDOUT, ">&", $cw) or die "Could not dup filehandle: $!";
292             open(STDERR, ">&", $cw) or die "Could not dup filehandle: $!";
293             exec "tarantool_box -v -c tarantool.conf";
294             die "exec: $!";
295             }
296             }
297              
298             =head2 stop option => $value, $cb->($status, $reason)
299              
300             stop tarantool instance
301              
302             =over 4
303              
304             =item timeout => $timeout
305              
306             After $timeout seconds tarantool will been kelled by the KILL signal
307              
308             =back
309              
310             =cut
311              
312             sub stop {
313             my $self = shift;
314             my $cb = pop;
315             my %arg = (
316             timeout => 10,
317             @_
318             );
319              
320             return $cb->(1, "Not Running") unless $self->{pid};
321              
322             $self->resume() if delete $self->{asleep};
323              
324             $self->{dying} = 1;
325              
326             my $i = int($arg{timeout} / 0.1);
327             $self->{stop_timer} = AnyEvent->timer(
328             interval => 0.1,
329             cb => sub {
330             unless ($self->{pid}) {
331             $self->{stop_timer} = undef;
332             $cb->(1, "OK");
333             }
334              
335             unless($i > 0) {
336             $self->{stop_timer} = undef;
337             kill KILL => $self->{pid};
338             $cb->(0, "Killed");
339             }
340             $i--;
341             }
342             );
343             kill TERM => $self->{pid};
344             }
345              
346             =head2 pause
347              
348             Send STOP signal to instance
349              
350             =cut
351              
352             sub pause {
353             my $self = shift;
354             return unless $self->{pid};
355             $self->{asleep} = 1;
356             kill STOP => $self->{pid};
357             }
358              
359             =head2 resume
360              
361             Send CONT signal to instance
362              
363             =cut
364              
365             sub resume {
366             my $self = shift;
367             return unless $self->{pid};
368             $self->{asleep} = undef;
369             kill CONT => $self->{pid};
370             }
371              
372             =head2 ro $cb->($status, $reason)
373              
374             Switch tarantool instance to read only mode.
375              
376             =cut
377              
378             sub ro {
379             my ($self, $cb) = @_;
380             return $cb->(1, "Not Changed") if $self->{replication_source};
381             $self->{replication_source} = "$self->{host}:$self->{port}";
382             $self->_config();
383             $self->admin_cmd("reload configuration", sub {
384             $cb->($_[0], $_[0] ? "OK" : "Failed")
385             });
386             }
387              
388             =head2 rw $cb->($status, $reason)
389              
390             Switch tarantool instance to write mode.
391              
392             =cut
393              
394             sub rw {
395             my ($self, $cb) = @_;
396             return $cb->(1, "Not Changed") unless $self->{replication_source};
397             $self->{replication_source} = "";
398             $self->_config();
399             $self->admin_cmd("reload configuration", sub {
400             $cb->($_[0], $_[0] ? "OK" : "Failed")
401             });
402             }
403              
404             =head2 admin_cmd $cmd, $cb->($status, $response_or_reason)
405              
406             Exec a command via the amind port.
407              
408             =cut
409              
410             sub admin_cmd {
411             my ($self, $cmd, $cb) = @_;
412             return if ($self->{afh});
413             $self->{afh} = AnyEvent::Handle->new (
414             connect => [ $self->{host}, $self->{a_port} ],
415             on_connect => sub {
416             $_[0]->push_write($cmd . "\n");
417             },
418             on_connect_error => sub {
419             warn "Connection error: $_[1]";
420             $_[0]->on_read(undef);
421             $_[0]->destroy();
422             delete $self->{afh};
423             $cb->(0, $_[1]);
424             },
425             on_error => sub {
426             $_[0]->on_read(undef);
427             $_[0]->destroy();
428             delete $self->{afh};
429             $cb->(0, $_[2])
430             },
431             );
432             $self->{afh}->push_read(regex => qr/\x0a\.\.\.\x0a/, sub {
433             $_[0]->destroy();
434             delete $self->{afh};
435             $cb->(1, $_[1]);
436             });
437             }
438              
439             =head2 times
440              
441             Return values of utime and stime from /proc/[pid]/stat, converted to seconds
442              
443             =cut
444              
445             sub times {
446             my $self = shift;
447             return unless $self->{pid};
448             open my $f, "<", "/proc/$self->{pid}/stat";
449             map { $_ / 100 } (split " ", <$f>)[13..14];
450             }
451              
452             =head2 sync_start sync_stop sync_ro sync_rw sync_admin_cmd
453              
454             Aliases for start, stop, ro, rw, admin_cmd respectively, arguments a similar,
455             but cb not passed.
456              
457             =cut
458              
459             {
460             no strict 'refs';
461             for my $method (qw/start stop ro rw admin_cmd/) {
462             *{"Test::Tarantool::sync_$method"} = sub {
463             my $self = shift;
464             my $cv = AE::cv();
465             $self->$method(@_, $cv);
466             return $cv->recv;
467             }
468             }
469             }
470              
471              
472             sub _config {
473             my $self = shift;
474             my $config = do { my $pos = tell DATA; local $/; my $c = ; seek DATA, $pos, 0; $c };
475             $config =~ s/ %\{([^{}]+)\} /$self->{$1}/xsg;
476             $config =~ s/ %\{\{(.*?)\}\} /eval "$1" or ''/exsg;
477             open my $f, '>', $self->{root} . '/' . 'tarantool.conf' or die "Could not create tnt config : $!";;
478             syswrite $f, $config;
479             }
480              
481             sub _spaces {
482             my $self = shift;
483             return $self->{spaces} unless ref $self->{spaces};
484             die 'TODO';
485             }
486              
487             sub _initlua {
488             my $self = shift;
489             die 'TODO' if ref $self->{initlua};
490             open my $f, '>', $self->{root} . '/' . 'init.lua' or die "Could not create init.lua : $!";;
491             syswrite $f, $self->{initlua};
492             }
493              
494             sub _init_storage() {
495             my $self = shift;
496             open my $f, '>', $self->{root} . '/' . '00000000000000000001.snap' or die "Could not create tnt snap: $!";
497             syswrite $f, "\x53\x4e\x41\x50\x0a\x30\x2e\x31\x31\x0a\x0a\x1e\xab\xad\x10";
498             if ($self->{snapshot} =~ m{(?:^|/)([0-9]{20}\.snap)$}) {
499             use Cwd;
500             symlink Cwd::abs_path($self->{snapshot}), $self->{root} . '/' . $1;
501             }
502             }
503              
504             sub DESTROY {
505             my $self = shift;
506             return unless $Schedule{$self};
507             kill TERM => $self->{pid} if $self->{pid};
508             if ($self->{cleanup}) {
509             opendir my $root, $self->{root} or die "opendir: $!";
510             my @unlink = map { (/^[^.]/ && -f "$self->{root}/$_") ? "$self->{root}/$_" : () } readdir($root);
511             local $, = ' ';
512             unlink @unlink or
513             warn "Could not unlink files (@unlink): $!";
514             rmdir($self->{root});
515             }
516             delete $Schedule{$self};
517             warn "$self->{title} destroed\n";
518             }
519              
520             END {
521             for (keys %Schedule) {
522             $Schedule{$_}->DESTROY();
523             }
524             }
525              
526             =head1 AUTHOR
527              
528             Anton Reznikov, C<< >>
529              
530             =head1 BUGS
531              
532             Please report any bugs or feature requests to C<< >>
533              
534              
535              
536             =head1 SUPPORT
537              
538             You can find documentation for this module with the perldoc command.
539              
540             perldoc Test::Tarantool
541              
542             =head1 ACKNOWLEDGEMENTS
543              
544             Mons Anderson - The original idia of the module.
545              
546             =head1 LICENSE AND COPYRIGHT
547              
548             Copyright 2014 Anton Reznikov.
549              
550             This program is released under the following license: GPL
551              
552             =cut
553              
554             1;
555              
556             __DATA__