Skip to content

Commit 5df36cf

Browse files
committed
(puppetlabsGH-198) Fix race condition on postgresql startup
This patch is a fix for the race condition that keeps occuring during postgresql setup. Its very rare on its own, but when you are using this module in a CI environment it happens quite frequently. Basically what happens is that sometimes the service will announce the database has started, but really it is still working in the background. Sometimes the unix socket may not be listening, and sometimes the system is still loading and you get a weird client error. The fix itself is a modification to postgresql::validate_db_connection so that it is able to connect on the local unix socket, plus retry until the database is available. This new and improved validate_db_connection can then be put into the build pipeline (in the service class in particular) to ensure the database is started before continuing on with the remaining steps. This in effect blocks the puppet module from continuing until the postgresql database is fully started and able to receive connections which is perfect. Tests and documentation provided. Signed-off-by: Ken Barber <[email protected]>
1 parent 173f599 commit 5df36cf

9 files changed

+168
-16
lines changed

README.md

+16-4
Original file line numberDiff line numberDiff line change
@@ -749,20 +749,32 @@ Example usage:
749749
Uniquely identify this resource, but functionally does nothing.
750750

751751
####`database_host`
752-
The hostname of the database you wish to test.
752+
The hostname of the database you wish to test. Defaults to 'undef' which generally uses the designated local unix socket.
753753

754754
####`database_port`
755-
Port to use when connecting.
755+
Port to use when connecting. Default to 'undef' which generally defaults to 5432 depending on your PostgreSQL packaging.
756756

757757
####`database_name`
758-
The name of the database you wish to test.
758+
The name of the database you wish to test. Defaults to 'postgres'.
759759

760760
####`database_username`
761-
Username to connect with.
761+
Username to connect with. Defaults to 'undef', which when using a unix socket and ident auth will be the user you are running as. If the host is remote you must provide a username.
762762

763763
####`database_password`
764764
Password to connect with. Can be left blank, but that is not recommended.
765765

766+
####`run_as`
767+
The user to run the `psql` command with for authenticiation. This is important when trying to connect to a database locally using Unix sockets and `ident` authentication. It is not needed for remote testing.
768+
769+
####`sleep`
770+
Upon failure, sets the number of seconds to sleep for before trying again.
771+
772+
####`tries`
773+
Upon failure, sets the number of attempts before giving up and failing the resource.
774+
775+
####`create_db_first`
776+
This will ensure the database is created before running the test. This only really works if your test is local. Defaults to `true`.
777+
766778

767779
###Function: postgresql\_password
768780
If you need to generate a postgres encrypted password, use `postgresql_password`. You can call it from your production manifests if you don't mind them containing the clear text versions of your passwords, or you can call it from the command line and then copy and paste the encrypted password into your manifest:
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
3+
# usage is: validate_db_connection 2 50 psql
4+
5+
SLEEP=$1
6+
TRIES=$2
7+
PSQL=$3
8+
9+
STATE=1
10+
11+
for (( c=1; c<=$TRIES; c++ ))
12+
do
13+
echo $c
14+
if [ $c -gt 1 ]
15+
then
16+
echo 'sleeping'
17+
sleep $SLEEP
18+
fi
19+
20+
/bin/echo "SELECT 1" | $PSQL
21+
STATE=$?
22+
23+
if [ $STATE -eq 0 ]
24+
then
25+
exit 0
26+
fi
27+
done
28+
29+
echo 'Unable to connect to puppetdb'
30+
31+
exit 1

manifests/client.pp

+15
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,19 @@
1111
tag => 'postgresql',
1212
}
1313

14+
$file_ensure = $package_ensure ? {
15+
'present' => 'file',
16+
true => 'file',
17+
'absent' => 'absent',
18+
false => 'absent',
19+
default => 'file',
20+
}
21+
file { "/usr/local/bin/validate_postgresql_connection.sh":
22+
ensure => $file_ensure,
23+
source => "puppet:///modules/postgresql/validate_postgresql_connection.sh",
24+
owner => 0,
25+
group => 0,
26+
mode => 0755,
27+
}
28+
1429
}

manifests/server/database.pp

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
postgresql_psql { "Check for existence of db '${dbname}'":
4949
command => 'SELECT 1',
5050
unless => "SELECT datname FROM pg_database WHERE datname='${dbname}'",
51-
require => Class['postgresql::server']
51+
require => Class['postgresql::server::service']
5252
}~>
5353
exec { $createdb_command :
5454
refreshonly => true,

manifests/server/service.pp

+16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
$service_name = $postgresql::server::service_name
55
$service_provider = $postgresql::server::service_provider
66
$service_status = $postgresql::server::service_status
7+
$user = $postgresql::server::user
78

89
$service_ensure = $ensure ? {
910
present => true,
@@ -19,4 +20,19 @@
1920
hasstatus => true,
2021
status => $service_status,
2122
}
23+
24+
if($service_ensure) {
25+
# This blocks the class before continuing if chained correctly, making
26+
# sure the service really is 'up' before continuing.
27+
#
28+
# Without it, we may continue doing more work before the database is
29+
# prepared leading to a nasty race condition.
30+
postgresql::validate_db_connection { 'validate_service_is_running':
31+
run_as => $user,
32+
sleep => 1,
33+
tries => 30,
34+
create_db_first => false,
35+
require => Service['postgresqld'],
36+
}
37+
}
2238
}

manifests/validate_db_connection.pp

+45-11
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,58 @@
44
#
55
# See README.md for more details.
66
define postgresql::validate_db_connection(
7-
$database_host,
8-
$database_name,
9-
$database_password,
10-
$database_username,
11-
$database_port = 5432
7+
$database_host = undef,
8+
$database_name = 'postgres',
9+
$database_password = undef,
10+
$database_username = undef,
11+
$database_port = undef,
12+
$run_as = undef,
13+
$sleep = 2,
14+
$tries = 10,
15+
$create_db_first = true
1216
) {
1317
require postgresql::client
1418

1519
$psql_path = $postgresql::params::psql_path
1620

17-
# TODO: port to ruby
18-
$psql = "${psql_path} --tuples-only --quiet -h ${database_host} -U ${database_username} -p ${database_port} --dbname ${database_name}"
21+
$cmd_init = "${psql_path} --tuples-only --quiet "
22+
$cmd_host = $database_host ? {
23+
default => "-h ${database_host} ",
24+
undef => ""
25+
}
26+
$cmd_user = $database_username ? {
27+
default => "-U ${database_username} ",
28+
undef => ""
29+
}
30+
$cmd_port = $database_port ? {
31+
default => "-p ${database_port} ",
32+
undef => ""
33+
}
34+
$cmd_dbname = $database_name ? {
35+
default => "--dbname ${database_name} ",
36+
undef => ""
37+
}
38+
$env = $database_password ? {
39+
default => "PGPASSWORD=${database_password}",
40+
undef => undef,
41+
}
42+
$cmd = join([$cmd_init, $cmd_host, $cmd_user, $cmd_port, $cmd_dbname])
43+
$validate_cmd = "/usr/local/bin/validate_postgresql_connection.sh ${sleep} ${tries} '${cmd}'"
44+
45+
# This is more of a safety valve, we add a little extra to compensate for the
46+
# time it takes to run each psql command.
47+
$timeout = (($sleep + 2) * $tries)
1948

2049
$exec_name = "validate postgres connection for ${database_host}/${database_name}"
2150
exec { $exec_name:
22-
command => '/bin/false',
23-
unless => "/bin/echo \"SELECT 1\" | ${psql}",
51+
command => "echo 'Unable to connect to defined database using: ${cmd}' && false",
52+
unless => $validate_cmd,
2453
cwd => '/tmp',
25-
environment => "PGPASSWORD=${database_password}",
54+
environment => $env,
2655
logoutput => 'on_failure',
56+
user => $run_as,
57+
path => '/bin',
58+
timeout => $timeout,
2759
require => Package['postgresql-client'],
2860
}
2961

@@ -36,5 +68,7 @@
3668
# We accomplish this by using Puppet's resource collection syntax to search
3769
# for the Database resource in our current catalog; if it exists, the
3870
# appropriate relationship is created here.
39-
Postgresql::Server::Database<|title == $database_name|> -> Exec[$exec_name]
71+
if($create_db_first) {
72+
Postgresql::Server::Database<|title == $database_name|> -> Exec[$exec_name]
73+
}
4074
}

spec/system/validate_db_connection_spec.rb

+34
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,40 @@ class { 'postgresql::server': }
2525
end
2626
end
2727

28+
it 'should run puppet with no changes declared if socket connectivity works' do
29+
pp = <<-EOS.unindent
30+
postgresql::validate_db_connection { 'foo':
31+
database_name => 'foo',
32+
run_as => 'postgres',
33+
}
34+
EOS
35+
36+
puppet_apply(pp) do |r|
37+
r.exit_code.should == 0
38+
end
39+
end
40+
41+
it 'should keep retrying if database is down' do
42+
# So first we shut the db down, then background a startup routine with a
43+
# sleep 10 in front of it. That way rspec-system should continue while
44+
# the pause and db startup happens in the background.
45+
shell("/etc/init.d/postgresql* stop")
46+
shell('nohup bash -c "sleep 10; /etc/init.d/postgresql* start" > /dev/null 2>&1 &')
47+
48+
pp = <<-EOS.unindent
49+
postgresql::validate_db_connection { 'foo':
50+
database_name => 'foo',
51+
tries => 30,
52+
sleep => 1,
53+
run_as => 'postgres',
54+
}
55+
EOS
56+
57+
puppet_apply(pp) do |r|
58+
r.exit_code.should == 0
59+
end
60+
end
61+
2862
it 'should run puppet with no changes declared if db ip connectivity works' do
2963
pp = <<-EOS.unindent
3064
postgresql::validate_db_connection { 'foo':

spec/unit/classes/server_spec.rb

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
describe 'with no parameters' do
1515
it { should include_class("postgresql::params") }
1616
it { should include_class("postgresql::server") }
17+
it 'should validate connection' do
18+
should contain_postgresql__validate_db_connection('validate_service_is_running')
19+
end
1720
end
1821

1922
describe 'manage_firewall => true' do

spec/unit/defines/validate_db_connection_spec.rb

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
'test'
1414
end
1515

16+
describe 'should work with only default parameters' do
17+
it { should contain_postgresql__validate_db_connection('test') }
18+
end
19+
1620
describe 'should work with all parameters' do
1721
let :params do
1822
{
@@ -21,6 +25,9 @@
2125
:database_password => 'test',
2226
:database_username => 'test',
2327
:database_port => 5432,
28+
:run_as => 'postgresq',
29+
:sleep => 4,
30+
:tries => 30,
2431
}
2532
end
2633
it { should contain_postgresql__validate_db_connection('test') }

0 commit comments

Comments
 (0)