File Coverage

blib/lib/DBIx/QuickDB/Driver.pm
Criterion Covered Total %
statement 34 238 14.2
branch 0 116 0.0
condition 0 62 0.0
subroutine 12 50 24.0
pod 28 33 84.8
total 74 499 14.8


line stmt bran cond sub pod time code
1             package DBIx::QuickDB::Driver;
2 26     26   174593 use strict;
  26         80  
  26         769  
3 26     26   136 use warnings;
  26         43  
  26         1572  
4              
5             our $VERSION = '0.000050';
6              
7 26     26   109 use Carp qw/croak confess/;
  26         36  
  26         1420  
8 26     26   172 use File::Path qw/remove_tree/;
  26         68  
  26         1146  
9 26     26   2288 use File::Temp qw/tempdir/;
  26         29592  
  26         1046  
10 26     26   118 use POSIX ":sys_wait_h";
  26         36  
  26         222  
11 26     26   15075 use Scalar::Util qw/blessed/;
  26         41  
  26         1094  
12 26     26   115 use Time::HiRes qw/sleep time/;
  26         52  
  26         175  
13              
14 26     26   4854 use DBIx::QuickDB::Util qw/clone_dir env_timeout/;
  26         54  
  26         225  
15              
16 26     26   11721 use DBIx::QuickDB::Watcher;
  26         62  
  26         1078  
17              
18 26         1345 use DBIx::QuickDB::Util::HashBase qw{
19             -root_pid
20             -dir
21             -_cleanup
22             -autostop -autostart
23             verbose
24             -_log_id
25             username
26             password
27             env_vars
28            
29            
30             -fast_destroy
31 26     26   139 };
  26         36  
32              
33 0     0 1 0 sub viable { (0, "viable() is not implemented for the " . $_[0]->name . " driver") }
34              
35 0     0 1 0 sub socket { confess "socket() is not implemented for the " . $_[0]->name . " driver" }
36 0     0 1 0 sub load_sql { confess "load_sql() is not implemented for the " . $_[0]->name . " driver" }
37 0     0   0 sub bootstrap { confess "bootstrap() is not implemented for the " . $_[0]->name . " driver" }
38 0     0 1 0 sub connect_string { confess "connect_string() is not implemented for the " . $_[0]->name . " driver" }
39 0     0 1 0 sub start_command { confess "start_command() is not implemented for the " . $_[0]->name . " driver" }
40 0     0 1 0 sub shell_command { confess "shell_command() is not implemented for the " . $_[0]->name . " driver" }
41              
42 0     0 0 0 sub error_log { undef }
43              
44             sub read_error_log {
45 0     0 0 0 my $self = shift;
46 0 0       0 my $log = $self->error_log or return "";
47 0 0       0 return "" unless -f $log;
48 0 0       0 open(my $fh, '<', $log) or return "Could not open error log '$log': $!";
49 0         0 return join "" => <$fh>;
50             }
51              
52             # Slurp a file for diagnostics; returns "" if it is missing or unreadable.
53             sub _read_file {
54 0     0   0 my $self = shift;
55 0         0 my ($file) = @_;
56 0 0 0     0 return "" unless $file && -f $file;
57 0 0       0 open(my $fh, '<', $file) or return "";
58 0         0 return join "" => <$fh>;
59             }
60              
61 0     0 1 0 sub list_env_vars { qw/DBI_USER DBI_PASS DBI_DSN/ }
62              
63 0     0 1 0 sub version_string { 'unknown' }
64              
65 0     0 1 0 sub stop_sig { 'TERM' }
66              
67             # Signal the watcher sends to the server for fast (disposable) teardown via
68             # destroy_quietly()/fast_destroy. The default SIGKILL is the quickest possible
69             # stop. Drivers whose server leaks an OS resource on a hard kill should override
70             # this with a clean-but-immediate shutdown signal (see PostgreSQL, which returns
71             # 'QUIT' so the postmaster releases its SysV semaphores instead of orphaning
72             # them). The watcher escalates to SIGKILL if the chosen signal does not exit the
73             # server promptly, so teardown always completes.
74 1     1 1 2399 sub fast_stop_sig { 'KILL' }
75              
76       0 1   sub write_config {}
77              
78             sub do_in_env {
79 0     0 1   my $self = shift;
80 0           my ($code) = @_;
81              
82 0           my $old = $self->mask_env_vars;
83              
84 0           my $ok = eval { $code->(); 1 };
  0            
  0            
85 0           my $err = $@;
86              
87 0           $self->unmask_env_vars($old);
88              
89 0 0         die $err unless $ok;
90              
91 0           return;
92             }
93              
94             sub mask_env_vars {
95 0     0 1   my $self = shift;
96              
97 0           my %old;
98              
99 0           for my $var ($self->list_env_vars) {
100 0 0         next unless defined $ENV{$var};
101 0           $old{$var} = delete $ENV{$var};
102             }
103              
104 0   0       my $env_vars = $self->env_vars || {};
105 0           for my $var (keys %$env_vars) {
106 0 0         $old{$var} = delete $ENV{$var} unless defined $old{$var};
107 0           $ENV{$var} = $env_vars->{$var};
108             }
109              
110 0           return \%old;
111             }
112              
113             sub unmask_env_vars {
114 0     0 1   my $self = shift;
115 0           my ($old) = @_;
116              
117 0           for my $var (keys %$old) {
118 0           my $val = $old->{$var};
119              
120 0 0         if (defined $val) {
121 0           $ENV{$var} = $val;
122             }
123             else {
124 0           delete $ENV{$var};
125             }
126             }
127              
128 0           return;
129             }
130              
131             sub name {
132 0     0 1   my $in = shift;
133 0   0       my $type = blessed($in) || $in;
134              
135 0           $type =~ s/^DBIx::QuickDB::Driver:://;
136              
137 0           return $type;
138             }
139              
140             sub init {
141 0     0 1   my $self = shift;
142              
143 0 0         confess "'dir' is a required attribute" unless $self->{+DIR};
144              
145 0           $self->{+ROOT_PID} = $$;
146 0           $self->{+_CLEANUP} = delete $self->{cleanup};
147              
148 0 0         $self->{+USERNAME} = '' unless defined $self->{+USERNAME};
149 0 0         $self->{+PASSWORD} = '' unless defined $self->{+PASSWORD};
150              
151 0   0       $self->{+ENV_VARS} ||= {};
152              
153 0           return;
154             }
155              
156             sub clone_data {
157 0     0 1   my $self = shift;
158              
159             return (
160             USERNAME() => $self->{+USERNAME},
161             PASSWORD() => $self->{+PASSWORD},
162             VERBOSE() => $self->{+VERBOSE},
163             AUTOSTOP() => $self->{+AUTOSTOP},
164             AUTOSTART() => $self->{+AUTOSTART},
165              
166             cleanup => $self->{+_CLEANUP},
167              
168             FAST_DESTROY() => $self->{+FAST_DESTROY},
169              
170 0           ENV_VARS() => {%{$self->{+ENV_VARS}}},
  0            
171             );
172             }
173              
174             sub resync {
175 0     0 1   my $self = shift;
176              
177 0 0         my $from = $self->{+CLONED_FROM} or croak "No original source to sync from";
178              
179 0           my $started = $self->started;
180 0 0         $self->stop if $started;
181              
182 0 0 0       clone_dir($from, $self->{+DIR}, verbose => (($self->{+VERBOSE} // 0) > 2) ? 1 : 0, checksum => 1);
183              
184 0           $self->write_config();
185              
186 0 0         $self->start if $started;
187             }
188              
189             sub clone {
190 0     0 1   my $self = shift;
191 0           my %params = @_;
192              
193 0 0         confess "Cannot clone a started database, please stop it first."
194             if $self->started;
195              
196 0           my $orig_dir = $self->{+DIR};
197 0 0 0       my $new_dir = delete $params{dir} // tempdir('DB-QUICK-CLONE-XXXXXX', CLEANUP => 0, $ENV{QDB_TMPDIR} ? (DIR => $ENV{QDB_TMPDIR}) : (TMPDIR => 1));
198              
199 0 0 0       clone_dir($orig_dir, $new_dir, verbose => (($self->{+VERBOSE} // 0) > 2) ? 1 : 0);
200              
201 0           my $class = ref($self);
202             my %ok = (
203             cleanup => 1,
204 0           map {$_ => 1} DBIx::QuickDB::Util::HashBase::attr_list($class),
  0            
205             );
206 0           my @bad = grep { !$ok{$_} } keys %params;
  0            
207              
208 0 0         confess "Invalid options to clone(): " . join(', ' => @bad)
209             if @bad;
210              
211 0           my $clone = $class->new(
212             $self->clone_data,
213              
214             %params,
215              
216             DIR() => $new_dir,
217              
218             WATCHER() => undef,
219              
220             CLONED_FROM() => $orig_dir,
221             );
222              
223 0           $clone->write_config();
224 0 0         $clone->start if $clone->{+AUTOSTART};
225              
226 0           return $clone;
227             }
228              
229             sub gen_log {
230 0     0 0   my $self = shift;
231 0 0         return if $self->no_log(@_);
232 0           return $self->{+DIR} . "/cmd-log-$$-" . $self->{+_LOG_ID}++;
233             }
234              
235             sub no_log {
236 0     0 1   my $self = shift;
237 0           my ($params) = @_;
238 0   0       return $self->{+VERBOSE} || $params->{no_log} || $ENV{DB_VERBOSE};
239             }
240              
241             sub run_command {
242 0     0 1   my $self = shift;
243 0           my ($cmd, $params) = @_;
244              
245 0           my $no_log = $self->no_log($params);
246 0   0       my $log_file = $params->{log_file} || ($no_log ? undef : $self->gen_log);
247              
248 0           my $pid = fork();
249 0 0         croak "Could not fork" unless defined $pid;
250              
251 0 0         if ($pid) {
252 0           local $?;
253 0 0         return ($pid, $log_file) if $params->{no_wait};
254 0           my $ret = waitpid($pid, 0);
255 0           my $exit = $?;
256 0 0         die "waitpid returned $ret" unless $ret == $pid;
257              
258 0 0         return unless $exit;
259              
260 0           my $log = "";
261 0 0         unless ($no_log) {
262 0 0         open(my $fh, '<', $log_file) or warn "Failed to open log: $!";
263 0           $log = eval { join "" => <$fh> };
  0            
264             }
265 0           my $error_log = $self->read_error_log;
266 0 0         $log .= "\n=== error log ===\n$error_log" if length $error_log;
267 0           croak "Failed to run command '" . join(' ' => @$cmd) . "' ($exit)\n$log";
268             }
269              
270 0           $self->mask_env_vars;
271              
272 0 0         unless ($no_log) {
273 0 0         open(my $log, '>', $log_file) or die "Could not open log file ($log_file): $!";
274 0           close(STDOUT);
275 0           open(STDOUT, '>&', $log);
276 0           close(STDERR);
277 0           open(STDERR, '>&', $log);
278             }
279              
280 0 0         if (my $file = $params->{stdin}) {
281 0           close(STDIN);
282 0 0         open(STDIN, '<', $file) or die "Could not open new STDIN ($file): $!";
283             }
284              
285 0           exec(@$cmd);
286             }
287              
288 0     0 1   sub should_cleanup { shift->{+_CLEANUP} }
289              
290             # Flush server state durably to disk so a hard kill mid-shutdown cannot leave
291             # the data dir needing crash recovery. Drivers that benefit (e.g. PostgreSQL)
292             # override this; the default is a no-op.
293       0 0   sub checkpoint { }
294              
295             sub cleanup {
296 0     0 1   my $self = shift;
297              
298             # Ignore errors here.
299 0           my $err = [];
300 0 0         remove_tree($self->{+DIR}, {safe => 1, error => \$err}) if -d $self->{+DIR};
301 0           return;
302             }
303              
304             sub connect {
305 0     0 1   my $self = shift;
306 0           my ($db_name, %params) = @_;
307              
308 0 0         %params = (AutoCommit => 1, RaiseError => 1) unless @_ > 1;
309              
310 0           my $dbh;
311             $self->do_in_env(
312             sub {
313 0     0     my $cstring = $self->connect_string($db_name);
314 0           require DBI;
315 0           $dbh = DBI->connect($cstring, $self->username, $self->password, \%params);
316             }
317 0           );
318              
319 0           return $dbh;
320             }
321              
322             sub started {
323 0     0 0   my $self = shift;
324              
325 0           my $socket = $self->socket;
326 0 0 0       return 1 if $self->{+WATCHER} || -S $socket;
327 0           return 0;
328             }
329              
330             sub start {
331 0     0 1   my $self = shift;
332 0           my @args = @_;
333              
334 0           my $dir = $self->{+DIR};
335 0           my $socket = $self->socket;
336              
337 0 0 0       return if $self->{+WATCHER} || -S $socket;
338              
339 0           my $watcher = $self->{+WATCHER} = DBIx::QuickDB::Watcher->new(db => $self, args => \@args);
340              
341             # Defaults to 10s; tunable via QDB_START_TIMEOUT for slow hosts that need
342             # longer to bring a server up (e.g. a clone doing crash recovery).
343 0           my $timeout = env_timeout(QDB_START_TIMEOUT => 10);
344              
345 0           my $start = time;
346 0           until (-S $socket) {
347 0           my $waited = time - $start;
348              
349 0 0         if ($waited > $timeout) {
350             # Capture diagnostics BEFORE eliminate() removes the data dir (which
351             # holds both the error log and the watcher's launch log). The server
352             # process's own stdout/stderr go to the watcher's log_file, not the
353             # driver's error_log, so a server that died (or never launched)
354             # before writing error_log leaves error_log showing only inherited
355             # template history -- the real failure is in the launch log. Also
356             # report whether the server pid is still alive: "not running" points
357             # at a launch/early-exit failure, "alive" at a slow or hung startup.
358 0           my $spid = $watcher->server_pid;
359 0 0 0       my $alive = ($spid && kill(0, $spid)) ? "alive (pid $spid)" : "not running";
360 0           my $error_log = $self->read_error_log;
361 0           my $launch_log = $self->_read_file($watcher->log_file);
362              
363 0           $watcher->eliminate();
364              
365 0           my $msg = "Timed out waiting for server to start after ${timeout}s; server process is $alive.";
366 0 0         $msg .= "\n=== server launch log ===\n$launch_log" if length $launch_log;
367 0 0         $msg .= "\n=== error log ===\n$error_log" if length $error_log;
368 0           confess $msg;
369             }
370              
371 0           sleep 0.01;
372             }
373              
374 0           return;
375             }
376              
377             sub stop {
378 0     0 1   my $self = shift;
379 0           my %params = @_;
380              
381 0 0         my $watcher = delete $self->{+WATCHER} or return;
382              
383             # Flush a durable checkpoint while the server is still running. If shutdown
384             # is slow enough to be SIGKILLed, this ensures the on-disk state is already
385             # consistent so a later clone does not crash-recover and jump SERIAL
386             # sequences forward (PostgreSQL SEQ_LOG_VALS=32), corrupting the clone.
387             # No-op for drivers that do not need it.
388 0           $self->checkpoint;
389              
390             DBI->visit_handles(
391             sub {
392 0     0     my ($driver_handle) = @_;
393              
394             $driver_handle->disconnect
395             if $driver_handle->{Type} && $driver_handle->{Type} eq 'db'
396 0 0 0       && $driver_handle->{Name} && index($driver_handle->{Name}, $self->{+DIR}) >= 0;
      0        
      0        
397              
398 0           return 1;
399             }
400 0           );
401              
402 0           $watcher->stop();
403              
404 0 0         unless ($params{no_wait}) {
405             # wait() blocks until the watcher process exits, and the watcher reaps
406             # the server before it exits -- so once wait() returns the server is
407             # gone. Trust that instead of polling a stored server pid: after the
408             # watcher exits that pid may have been recycled by the OS to an
409             # unrelated process, and polling it could hang/confess on the wrong
410             # process (the same pid-reuse hazard the watcher teardown guards against).
411 0           $watcher->wait();
412              
413             # Remove a stale unix socket left behind by a hard kill so it does not
414             # confuse callers or a later run that reuses the same directory.
415 0           my $socket = $self->socket;
416 0 0 0       unlink($socket) if $socket && -S $socket;
417             }
418              
419 0           return;
420             }
421              
422             # Immediate disposable teardown for clones that are about to be deleted. Unlike
423             # stop()/DESTROY this does NOT checkpoint or attempt a graceful shutdown: it asks
424             # the watcher to SIGKILL+reap the server and remove the data dir. Only safe when
425             # the data dir is disposable (cleanup => 1). Idempotent: after the first call
426             # clears the watcher, later calls and DESTROY become no-ops apart from an
427             # idempotent cleanup().
428             sub destroy_quietly {
429 0     0 1   my $self = shift;
430 0 0 0       return unless $self->{+ROOT_PID} && $self->{+ROOT_PID} == $$;
431              
432 0 0         if (my $watcher = delete $self->{+WATCHER}) {
    0          
433             # Disconnect our DBI handles before the server dies so this process does
434             # not retain broken handles that later report "server has gone away".
435             DBI->visit_handles(
436             sub {
437 0     0     my ($driver_handle) = @_;
438              
439             $driver_handle->disconnect
440             if $driver_handle->{Type} && $driver_handle->{Type} eq 'db'
441 0 0 0       && $driver_handle->{Name} && index($driver_handle->{Name}, $self->{+DIR}) >= 0;
      0        
      0        
442              
443 0           return 1;
444             }
445 0 0         ) if $INC{'DBI.pm'};
446              
447             # The watcher is the server's parent, so it is the correct process to
448             # kill and reap it. Do NOT signal the stored server pid directly here.
449 0           $watcher->fast_eliminate();
450 0           $watcher->wait();
451              
452             # The watcher removes the data dir; this is a defensive fallback in case
453             # it exited before doing so.
454 0 0         $self->cleanup() if $self->should_cleanup;
455             }
456             elsif ($self->should_cleanup) {
457 0           $self->cleanup();
458             }
459              
460 0           return;
461             }
462              
463             sub shell {
464 0     0 1   my $self = shift;
465 0           my ($db_name) = @_;
466 0 0         $db_name = 'quickdb' unless defined $db_name;
467              
468 0           system($self->shell_command($db_name));
469             }
470              
471             sub DESTROY {
472 0     0     my $self = shift;
473 0 0 0       return unless $self->{+ROOT_PID} && $self->{+ROOT_PID} == $$;
474              
475             # Opt-in fast teardown for disposable clones. Only honored when the data dir
476             # is actually disposable (_CLEANUP); a contradictory fast_destroy + cleanup
477             # => 0 falls through to the normal graceful path below so a reusable data dir
478             # is never hard-killed.
479             return $self->destroy_quietly()
480 0 0 0       if $self->{+FAST_DESTROY} && $self->{+_CLEANUP};
481              
482 0 0         if (my $watcher = delete $self->{+WATCHER}) {
    0          
483             # eliminate() signals the watcher to stop the server and delete the data
484             # dir; destroying the watcher then blocks (via Watcher::wait) until the
485             # watcher process has exited, and the watcher reaps the server before it
486             # exits. So once $watcher is gone the server is gone too. We deliberately
487             # do NOT fall back to signalling a stored server pid here: after the
488             # watcher exits that pid may have been recycled to an unrelated process
489             # (the pid-reuse hazard), so a stray TERM/KILL could hit the wrong one.
490 0           $watcher->eliminate();
491 0           undef $watcher;
492              
493 0 0         $self->cleanup() if $self->should_cleanup;
494             }
495             elsif ($self->should_cleanup) {
496 0           $self->cleanup();
497             }
498              
499 0           return;
500             }
501              
502             1;
503              
504             __END__