File Coverage

blib/lib/DBIx/QuickDB/Driver/PostgreSQL.pm
Criterion Covered Total %
statement 46 156 29.4
branch 10 54 18.5
condition 4 32 12.5
subroutine 11 30 36.6
pod 12 15 80.0
total 83 287 28.9


line stmt bran cond sub pod time code
1             package DBIx::QuickDB::Driver::PostgreSQL;
2 5     5   142431 use strict;
  5         7  
  5         177  
3 5     5   19 use warnings;
  5         8  
  5         297  
4              
5             our $VERSION = '0.000045';
6              
7 5     5   1720 use IPC::Cmd qw/can_run/;
  5         134255  
  5         297  
8 5     5   1334 use DBIx::QuickDB::Util qw/strip_hash_defaults/;
  5         15  
  5         44  
9 5     5   154 use Time::HiRes qw/sleep/;
  5         8  
  5         37  
10 5     5   292 use Scalar::Util qw/reftype/;
  5         6  
  5         212  
11              
12 5     5   18 use parent 'DBIx::QuickDB::Driver';
  5         10  
  5         30  
13              
14 5         22 use DBIx::QuickDB::Util::HashBase qw{
15             -data_dir
16              
17             -initdb -createdb -postgres -psql
18              
19             -config
20             -socket
21             -port
22 5     5   307 };
  5         8  
23              
24             my ($INITDB, $CREATEDB, $POSTGRES, $PSQL, $DBDPG);
25              
26             BEGIN {
27 5     5   11 local $@;
28              
29 5         23 $INITDB = can_run('initdb');
30 5         1202 $CREATEDB = can_run('createdb');
31 5         828 $POSTGRES = can_run('postgres');
32 5         804 $PSQL = can_run('psql');
33 5         873 $DBDPG = eval { require DBD::Pg; 'DBD::Pg'};
  5         7901  
  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             sub _default_paths {
83             return (
84 7     7   52 initdb => $INITDB,
85             createdb => $CREATEDB,
86             postgres => $POSTGRES,
87             psql => $PSQL,
88             );
89             }
90              
91             sub _default_config {
92 0     0   0 my $self = shift;
93              
94             return (
95             datestyle => "'iso, mdy'",
96             default_text_search_config => "'pg_catalog.english'",
97             lc_messages => "'en_US.UTF-8'",
98             lc_monetary => "'en_US.UTF-8'",
99             lc_numeric => "'en_US.UTF-8'",
100             lc_time => "'en_US.UTF-8'",
101             listen_addresses => "''",
102             log_destination => "'stderr'",
103             logging_collector => "'on'",
104             log_directory => "'$self->{+DIR}'",
105             log_filename => "'error.log'",
106             max_connections => "100",
107             shared_buffers => "128MB",
108             unix_socket_directories => "'$self->{+DIR}'",
109 0         0 port => $self->{+PORT},
110              
111             #dynamic_shared_memory_type => "posix",
112             #log_timezone => "'US/Pacific'",
113             #timezone => "'US/Pacific'",
114             );
115             }
116              
117             sub viable {
118 7     7 1 11 my $this = shift;
119 7         18 my ($spec) = @_;
120              
121 7 50       30 my %check = (ref($this) ? %$this : (), $this->_default_paths, %$spec);
122              
123 7         15 my @bad;
124              
125 7 50       21 push @bad => "'DBD::Pg' module could not be loaded, needed for everything" unless $DBDPG;
126              
127 7 50       26 if ($spec->{bootstrap}) {
128 7 50 33     24 push @bad => "'initdb' command is missing, needed for bootstrap" unless $check{initdb} && -x $check{initdb};
129 7 50 33     36 push @bad => "'createdb' command is missing, needed for bootstrap" unless $check{createdb} && -x $check{createdb};
130             }
131              
132 7 50       17 if ($spec->{autostart}) {
133 7 50 33     1462 push @bad => "'postgres' command is missing, needed for autostart" unless $check{postgres} && -x $check{postgres};
134             }
135              
136 7 50       21 if ($spec->{load_sql}) {
137 7 50 33     36 push @bad => "'psql' command is missing, needed for load_sql" unless $check{psql} && -x $check{psql};
138             }
139              
140 7 50       17 return (1, undef) unless @bad;
141 7         54 return (0, join "\n" => @bad);
142             }
143              
144             sub init {
145 0     0 1   my $self = shift;
146 0           $self->SUPER::init();
147              
148 0   0       my $port = $self->{+PORT} ||= '5432';
149              
150 0           my $dir = $self->{+DIR};
151 0           $self->{+DATA_DIR} = "$dir/data";
152 0   0       $self->{+SOCKET} ||= "$dir/.s.PGSQL.$port";
153              
154 0   0       $self->{+ENV_VARS} ||= {};
155 0 0         $self->{+ENV_VARS}->{PGPORT} = $port unless defined $self->{+ENV_VARS}->{PGPORT};
156              
157 0           my %defaults = $self->_default_paths;
158 0   0       $self->{$_} ||= $defaults{$_} for keys %defaults;
159              
160 0           my %cfg_defs = $self->_default_config;
161 0   0       my $cfg = $self->{+CONFIG} ||= {};
162              
163 0           for my $key (keys %cfg_defs) {
164 0 0         next if defined $cfg->{$key};
165 0           $cfg->{$key} = $cfg_defs{$key};
166             }
167             }
168              
169             sub clone_data {
170 0     0 1   my $self = shift;
171              
172 0   0       my $vars = $self->env_vars || {};
173 0 0 0       delete $vars->{PGPORT} if $vars->{PGPORT} && $vars->{PGPORT} eq $self->port;
174              
175             my $config = strip_hash_defaults(
176 0           $self->{+CONFIG},
177             { $self->_default_config },
178             );
179              
180             return (
181 0           $self->SUPER::clone_data(),
182             ENV_VARS() => $vars,
183             CONFIG() => $config,
184             );
185             }
186              
187             sub write_config {
188 0     0 1   my $self = shift;
189              
190 0           my $db_dir = $self->{+DATA_DIR};
191 0 0         open(my $cf, '>', "$db_dir/postgresql.conf") or die "Could not open config file: $!";
192 0           for my $key (sort keys %{$self->{+CONFIG}}) {
  0            
193 0           my $val = $self->{+CONFIG}->{$key};
194 0 0         next unless length($val);
195              
196 0           print $cf "$key = $val\n";
197             }
198 0           close($cf);
199             }
200              
201             sub bootstrap {
202 0     0     my $self = shift;
203              
204 0           my $dir = $self->{+DIR};
205 0           my $db_dir = $self->{+DATA_DIR};
206 0 0         mkdir($db_dir) or die "Could not create data dir: $!";
207 0           $self->run_command([$self->{+INITDB}, '-E', 'UTF8', '--no-locale', '-A', 'trust', '-D', $db_dir]);
208              
209 0           $self->write_config;
210 0           $self->start;
211              
212 0           for my $try (1 .. 10) {
213 0           my ($ok, $err);
214             {
215 0           local $@;
  0            
216 0           $ok = eval {
217             $self->catch_startup(sub {
218 0     0     $self->run_command([$self->{+CREATEDB}, '-T', 'template0', '-E', 'UTF8', '-h', $dir, 'quickdb']);
219 0           });
220              
221 0           1;
222             };
223 0           $err = $@;
224             }
225              
226 0 0         last if $ok;
227              
228 0 0         die $err if $try == 5;
229              
230 0           sleep 0.5;
231             }
232              
233 0 0         $self->stop unless $self->{+AUTOSTART};
234              
235 0           return;
236             }
237              
238             sub connect {
239 0     0 1   my $self = shift;
240 0           my ($db_name, %params) = @_;
241              
242 0           my $dbh;
243             $self->catch_startup(sub {
244 0     0     $dbh = $self->SUPER::connect($db_name, %params);
245 0           });
246              
247 0           return $dbh;
248             }
249              
250             # Force a CHECKPOINT so the on-disk state is durable before shutdown. Without
251             # this, a shutdown that gets SIGKILLed (slow host blowing the watcher's grace
252             # period) leaves the cluster needing crash recovery; cloning that data dir and
253             # starting it replays WAL, which advances SERIAL sequences by SEQ_LOG_VALS (32)
254             # -- e.g. the next inserted row gets id 34 instead of 2. Best effort: any
255             # failure here must not prevent the server from stopping.
256             sub checkpoint {
257 0     0 0   my $self = shift;
258              
259 0 0         return unless $self->started;
260              
261 0           eval {
262 0           my $dbh = $self->connect('postgres', AutoCommit => 1, RaiseError => 1, PrintError => 0);
263 0           $dbh->do('CHECKPOINT');
264 0           $dbh->disconnect;
265 0           1;
266             };
267              
268 0           return;
269             }
270              
271             sub connect_string {
272 0     0 1   my $self = shift;
273 0           my ($db_name) = @_;
274 0 0         $db_name = 'quickdb' unless defined $db_name;
275              
276 0           my $dir = $self->{+DIR};
277              
278 0           require DBD::Pg;
279 0           return "dbi:Pg:dbname=$db_name;host=$dir"
280             }
281              
282             sub load_sql {
283 0     0 1   my $self = shift;
284 0           my ($dbname, $file) = @_;
285              
286 0           my $dir = $self->{+DIR};
287              
288             $self->catch_startup(sub {
289             $self->run_command([
290 0     0     $self->{+PSQL},
291             '-h' => $dir,
292             '-v' => 'ON_ERROR_STOP=1',
293             '-f' => $file,
294             $dbname,
295             ]);
296 0           });
297             }
298              
299             sub shell_command {
300 0     0 1   my $self = shift;
301 0           my ($db_name) = @_;
302              
303 0           return ($self->{+PSQL}, '-h' => $self->{+DIR}, $db_name);
304             }
305              
306             sub start_command {
307 0     0 1   my $self = shift;
308 0           return ($self->{+POSTGRES}, '-D' => $self->{+DATA_DIR}, '-p' => $self->{+PORT});
309             }
310              
311             sub catch_startup {
312 0     0 0   my $self = shift;
313 0           my ($code) = @_;
314              
315 0           my $start = time;
316 0           while (1) {
317 0           my $waited = time - $start;
318 0 0         die "Timeout waiting for server" if $waited > 10;
319              
320 0           my ($ok, $err, $out);
321             {
322 0           local $@;
  0            
323 0           $ok = eval {
324 0           $out = $code->($self);
325 0           1;
326             };
327              
328 0           $err = $@;
329             }
330              
331 0 0         return $out if $ok;
332              
333 0 0         die $err unless $err =~ m/the database system is starting up/;
334              
335 0           sleep 0.01;
336             }
337             }
338              
339             1;
340              
341             __END__