File Coverage

blib/lib/DBIx/QuickDB/Driver/PostgreSQL.pm
Criterion Covered Total %
statement 47 158 29.7
branch 10 54 18.5
condition 4 32 12.5
subroutine 12 31 38.7
pod 13 16 81.2
total 86 291 29.5


line stmt bran cond sub pod time code
1             package DBIx::QuickDB::Driver::PostgreSQL;
2 8     8   141341 use strict;
  8         13  
  8         231  
3 8     8   26 use warnings;
  8         11  
  8         475  
4              
5             our $VERSION = '0.000049';
6              
7 8     8   3419 use IPC::Cmd qw/can_run/;
  8         295232  
  8         556  
8 8     8   3314 use DBIx::QuickDB::Util qw/strip_hash_defaults env_timeout/;
  8         27  
  8         69  
9 8     8   274 use Time::HiRes qw/sleep/;
  8         14  
  8         96  
10 8     8   497 use Scalar::Util qw/reftype/;
  8         11  
  8         346  
11              
12 8     8   32 use parent 'DBIx::QuickDB::Driver';
  8         24  
  8         76  
13              
14 8         42 use DBIx::QuickDB::Util::HashBase qw{
15             -data_dir
16              
17             -initdb -createdb -postgres -psql
18              
19             -config
20             -socket
21             -port
22 8     8   440 };
  8         11  
23              
24             my ($INITDB, $CREATEDB, $POSTGRES, $PSQL, $DBDPG);
25              
26             BEGIN {
27 8     8   18 local $@;
28              
29 8         40 $INITDB = can_run('initdb');
30 8         1864 $CREATEDB = can_run('createdb');
31 8         1361 $POSTGRES = can_run('postgres');
32 8         1353 $PSQL = can_run('psql');
33 8         1293 $DBDPG = eval { require DBD::Pg; 'DBD::Pg'};
  8         12412  
  0         0  
34             }
35              
36             sub version_string {
37 0     0 1 0 my $binary;
38              
39             # Go in reverse order assuming the last param hash provided is most important
40 0         0 for my $arg (reverse @_) {
41 0 0       0 my $type = reftype($arg) or next; # skip if not a ref
42 0 0       0 next unless $type eq 'HASH'; # We have a hashref, possibly blessed
43              
44             # If we find a launcher we are done looping, we want to use this binary.
45 0 0       0 $binary = $arg->{+POSTGRES} and last;
46             }
47              
48             # If no args provided one to use we fallback to the default from $PATH
49 0   0     0 $binary ||= $POSTGRES;
50              
51             # Call the binary with '-V', capturing and returning the output using backticks.
52 0         0 return `$binary -V`;
53             }
54              
55             sub list_env_vars {
56 0     0 1 0 my $self = shift;
57             return (
58 0         0 $self->SUPER::list_env_vars(),
59             qw{
60             PGAPPNAME PGCLIENTENCODING PGCONNECT_TIMEOUT PGDATABASE PGDATESTYLE
61             PGGEQO PGGSSLIB PGHOST PGHOSTADDR PGKRBSRVNAME PGLOCALEDIR
62             PGOPTIONS PGPASSFILE PGPASSWORD PGPORT PGREQUIREPEER PGREQUIRESSL
63             PGSERVICE PGSERVICEFILE PGSSLCERT PGSSLCOMPRESSION PGSSLCRL
64             PGSSLKEY PGSSLMODE PGSSLROOTCERT PGSYSCONFDIR PGTARGETSESSIONATTRS
65             PGTZ PGUSER
66             }
67             );
68             }
69              
70 0     0 0 0 sub error_log { "$_[0]->{+DIR}/error.log" }
71              
72             # Stop the postmaster with SIGTERM ("Smart Shutdown"): it lets in-progress
73             # transactions finish and shuts down cleanly. Smart Shutdown waits for every
74             # client to disconnect first, which previously risked hanging until the watcher
75             # escalated to SIGKILL. That risk is now handled elsewhere: stop() disconnects
76             # our own lingering handles (via DBI->visit_handles) before signalling the
77             # server, forces a CHECKPOINT so a hard kill cannot corrupt a later clone, and
78             # the watcher's grace period before SIGKILL is generous and tunable. A proper
79             # shutdown is preferred whenever it is possible.
80 0     0 1 0 sub stop_sig { 'TERM' }
81              
82             # Fast/disposable teardown uses SIGQUIT ("Immediate Shutdown") rather than the
83             # base-class SIGKILL. Immediate Shutdown aborts all backends without a
84             # checkpoint -- so it is nearly as fast as SIGKILL -- but the postmaster still
85             # runs its exit cleanup and RELEASES its SysV semaphores (and shared memory). A
86             # SIGKILLed postmaster cannot clean up, and because a disposable clone's data
87             # dir is then deleted no future postmaster will ever reuse that IPC key, so the
88             # semaphores leak permanently. On hosts where PostgreSQL uses SysV semaphores
89             # (FreeBSD, OpenBSD, macOS) a suite that hard-kills many disposable clones can
90             # exhaust the kernel SEMMNI/SEMMNS limits; SIGQUIT avoids that.
91 1     1 1 13 sub fast_stop_sig { 'QUIT' }
92              
93             sub _default_paths {
94             return (
95 9     9   64 initdb => $INITDB,
96             createdb => $CREATEDB,
97             postgres => $POSTGRES,
98             psql => $PSQL,
99             );
100             }
101              
102             sub _default_config {
103 0     0   0 my $self = shift;
104              
105             return (
106             datestyle => "'iso, mdy'",
107             default_text_search_config => "'pg_catalog.english'",
108             lc_messages => "'en_US.UTF-8'",
109             lc_monetary => "'en_US.UTF-8'",
110             lc_numeric => "'en_US.UTF-8'",
111             lc_time => "'en_US.UTF-8'",
112             listen_addresses => "''",
113             log_destination => "'stderr'",
114             logging_collector => "'on'",
115             log_directory => "'$self->{+DIR}'",
116             log_filename => "'error.log'",
117             max_connections => "100",
118             shared_buffers => "128MB",
119             unix_socket_directories => "'$self->{+DIR}'",
120 0         0 port => $self->{+PORT},
121              
122             #dynamic_shared_memory_type => "posix",
123             #log_timezone => "'US/Pacific'",
124             #timezone => "'US/Pacific'",
125             );
126             }
127              
128             sub viable {
129 9     9 1 17 my $this = shift;
130 9         16 my ($spec) = @_;
131              
132 9 50       1363 my %check = (ref($this) ? %$this : (), $this->_default_paths, %$spec);
133              
134 9         22 my @bad;
135              
136 9 50       25 push @bad => "'DBD::Pg' module could not be loaded, needed for everything" unless $DBDPG;
137              
138 9 50       27 if ($spec->{bootstrap}) {
139 9 50 33     33 push @bad => "'initdb' command is missing, needed for bootstrap" unless $check{initdb} && -x $check{initdb};
140 9 50 33     26 push @bad => "'createdb' command is missing, needed for bootstrap" unless $check{createdb} && -x $check{createdb};
141             }
142              
143 9 50       25 if ($spec->{autostart}) {
144 9 50 33     28 push @bad => "'postgres' command is missing, needed for autostart" unless $check{postgres} && -x $check{postgres};
145             }
146              
147 9 50       17 if ($spec->{load_sql}) {
148 9 50 33     26 push @bad => "'psql' command is missing, needed for load_sql" unless $check{psql} && -x $check{psql};
149             }
150              
151 9 50       19 return (1, undef) unless @bad;
152 9         49 return (0, join "\n" => @bad);
153             }
154              
155             sub init {
156 0     0 1   my $self = shift;
157 0           $self->SUPER::init();
158              
159 0   0       my $port = $self->{+PORT} ||= '5432';
160              
161 0           my $dir = $self->{+DIR};
162 0           $self->{+DATA_DIR} = "$dir/data";
163 0   0       $self->{+SOCKET} ||= "$dir/.s.PGSQL.$port";
164              
165 0   0       $self->{+ENV_VARS} ||= {};
166 0 0         $self->{+ENV_VARS}->{PGPORT} = $port unless defined $self->{+ENV_VARS}->{PGPORT};
167              
168 0           my %defaults = $self->_default_paths;
169 0   0       $self->{$_} ||= $defaults{$_} for keys %defaults;
170              
171 0           my %cfg_defs = $self->_default_config;
172 0   0       my $cfg = $self->{+CONFIG} ||= {};
173              
174 0           for my $key (keys %cfg_defs) {
175 0 0         next if defined $cfg->{$key};
176 0           $cfg->{$key} = $cfg_defs{$key};
177             }
178             }
179              
180             sub clone_data {
181 0     0 1   my $self = shift;
182              
183 0   0       my $vars = $self->env_vars || {};
184 0 0 0       delete $vars->{PGPORT} if $vars->{PGPORT} && $vars->{PGPORT} eq $self->port;
185              
186             my $config = strip_hash_defaults(
187 0           $self->{+CONFIG},
188             { $self->_default_config },
189             );
190              
191             return (
192 0           $self->SUPER::clone_data(),
193             ENV_VARS() => $vars,
194             CONFIG() => $config,
195             );
196             }
197              
198             sub write_config {
199 0     0 1   my $self = shift;
200              
201 0           my $db_dir = $self->{+DATA_DIR};
202 0 0         open(my $cf, '>', "$db_dir/postgresql.conf") or die "Could not open config file: $!";
203 0           for my $key (sort keys %{$self->{+CONFIG}}) {
  0            
204 0           my $val = $self->{+CONFIG}->{$key};
205 0 0         next unless length($val);
206              
207 0           print $cf "$key = $val\n";
208             }
209 0           close($cf);
210             }
211              
212             sub bootstrap {
213 0     0     my $self = shift;
214              
215 0           my $dir = $self->{+DIR};
216 0           my $db_dir = $self->{+DATA_DIR};
217 0 0         mkdir($db_dir) or die "Could not create data dir: $!";
218 0           $self->run_command([$self->{+INITDB}, '-E', 'UTF8', '--no-locale', '-A', 'trust', '-D', $db_dir]);
219              
220 0           $self->write_config;
221 0           $self->start;
222              
223 0           for my $try (1 .. 10) {
224 0           my ($ok, $err);
225             {
226 0           local $@;
  0            
227 0           $ok = eval {
228             $self->catch_startup(sub {
229 0     0     $self->run_command([$self->{+CREATEDB}, '-T', 'template0', '-E', 'UTF8', '-h', $dir, 'quickdb']);
230 0           });
231              
232 0           1;
233             };
234 0           $err = $@;
235             }
236              
237 0 0         last if $ok;
238              
239 0 0         die $err if $try == 5;
240              
241 0           sleep 0.5;
242             }
243              
244 0 0         $self->stop unless $self->{+AUTOSTART};
245              
246 0           return;
247             }
248              
249             sub connect {
250 0     0 1   my $self = shift;
251 0           my ($db_name, %params) = @_;
252              
253 0           my $dbh;
254             $self->catch_startup(sub {
255 0     0     $dbh = $self->SUPER::connect($db_name, %params);
256 0           });
257              
258 0           return $dbh;
259             }
260              
261             # Force a CHECKPOINT so the on-disk state is durable before shutdown. Without
262             # this, a shutdown that gets SIGKILLed (slow host blowing the watcher's grace
263             # period) leaves the cluster needing crash recovery; cloning that data dir and
264             # starting it replays WAL, which advances SERIAL sequences by SEQ_LOG_VALS (32)
265             # -- e.g. the next inserted row gets id 34 instead of 2. Best effort: any
266             # failure here must not prevent the server from stopping.
267             sub checkpoint {
268 0     0 0   my $self = shift;
269              
270 0 0         return unless $self->started;
271              
272 0           eval {
273 0           my $dbh = $self->connect('postgres', AutoCommit => 1, RaiseError => 1, PrintError => 0);
274 0           $dbh->do('CHECKPOINT');
275 0           $dbh->disconnect;
276 0           1;
277             };
278              
279 0           return;
280             }
281              
282             sub connect_string {
283 0     0 1   my $self = shift;
284 0           my ($db_name) = @_;
285 0 0         $db_name = 'quickdb' unless defined $db_name;
286              
287 0           my $dir = $self->{+DIR};
288              
289 0           require DBD::Pg;
290 0           return "dbi:Pg:dbname=$db_name;host=$dir"
291             }
292              
293             sub load_sql {
294 0     0 1   my $self = shift;
295 0           my ($dbname, $file) = @_;
296              
297 0           my $dir = $self->{+DIR};
298              
299             $self->catch_startup(sub {
300             $self->run_command([
301 0     0     $self->{+PSQL},
302             '-h' => $dir,
303             '-v' => 'ON_ERROR_STOP=1',
304             '-f' => $file,
305             $dbname,
306             ]);
307 0           });
308             }
309              
310             sub shell_command {
311 0     0 1   my $self = shift;
312 0           my ($db_name) = @_;
313              
314 0           return ($self->{+PSQL}, '-h' => $self->{+DIR}, $db_name);
315             }
316              
317             sub start_command {
318 0     0 1   my $self = shift;
319 0           return ($self->{+POSTGRES}, '-D' => $self->{+DATA_DIR}, '-p' => $self->{+PORT});
320             }
321              
322             sub catch_startup {
323 0     0 0   my $self = shift;
324 0           my ($code) = @_;
325              
326 0           my $timeout = env_timeout(QDB_START_TIMEOUT => 10);
327              
328 0           my $start = time;
329 0           while (1) {
330 0           my $waited = time - $start;
331 0 0         die "Timeout waiting for server" if $waited > $timeout;
332              
333 0           my ($ok, $err, $out);
334             {
335 0           local $@;
  0            
336 0           $ok = eval {
337 0           $out = $code->($self);
338 0           1;
339             };
340              
341 0           $err = $@;
342             }
343              
344 0 0         return $out if $ok;
345              
346 0 0         die $err unless $err =~ m/the database system is starting up/;
347              
348 0           sleep 0.01;
349             }
350             }
351              
352             1;
353              
354             __END__