Skip to content

Commit 56f79aa

Browse files
authored
Merge pull request #35 from funbringer/unique_system_ids_for_cached_initdb
Unique system ids for cached initdb
2 parents 5bd8d48 + 636d0f9 commit 56f79aa

File tree

5 files changed

+87
-9
lines changed

5 files changed

+87
-9
lines changed

testgres/cache.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
# coding: utf-8
22

3+
import io
34
import os
45
import shutil
56

67
from six import raise_from
78

89
from .config import testgres_config
910

11+
from .consts import XLOG_CONTROL_FILE
12+
1013
from .exceptions import \
1114
InitNodeException, \
1215
ExecUtilException
1316

1417
from .utils import \
1518
get_bin_path, \
16-
execute_utility
19+
execute_utility, \
20+
generate_system_id
1721

1822

1923
def cached_initdb(data_dir, logfile=None, params=None):
@@ -42,5 +46,23 @@ def call_initdb(initdb_dir, log=None):
4246
try:
4347
# Copy cached initdb to current data dir
4448
shutil.copytree(cached_data_dir, data_dir)
49+
50+
# Assign this node a unique system id if asked to
51+
if testgres_config.cached_initdb_unique:
52+
# XXX: write new unique system id to control file
53+
# Some users might rely upon unique system ids, but
54+
# our initdb caching mechanism breaks this contract.
55+
pg_control = os.path.join(data_dir, XLOG_CONTROL_FILE)
56+
with io.open(pg_control, "r+b") as f:
57+
f.write(generate_system_id()) # overwrite id
58+
59+
# XXX: build new WAL segment with our system id
60+
_params = [get_bin_path("pg_resetwal"), "-D", data_dir, "-f"]
61+
execute_utility(_params, logfile)
62+
63+
except ExecUtilException as e:
64+
msg = "Failed to reset WAL for system id"
65+
raise_from(InitNodeException(msg), e)
66+
4567
except Exception as e:
4668
raise_from(InitNodeException("Failed to spawn a node"), e)

testgres/config.py

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class GlobalConfig(object):
1717
Attributes:
1818
cache_initdb: shall we use cached initdb instance?
1919
cached_initdb_dir: shall we create a temp dir for cached initdb?
20+
cached_initdb_unique: shall we assign new node a unique system id?
2021
2122
cache_pg_config: shall we cache pg_config results?
2223
@@ -32,6 +33,7 @@ class GlobalConfig(object):
3233

3334
cache_initdb = True
3435
_cached_initdb_dir = None
36+
cached_initdb_unique = False
3537

3638
cache_pg_config = True
3739

testgres/consts.py

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
TMP_CACHE = 'tgsc_'
1111
TMP_BACKUP = 'tgsb_'
1212

13+
# path to control file
14+
XLOG_CONTROL_FILE = "global/pg_control"
15+
1316
# names for config files
1417
RECOVERY_CONF_FILE = "recovery.conf"
1518
PG_AUTO_CONF_FILE = "postgresql.auto.conf"

testgres/utils.py

+24
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,30 @@ def generate_app_name():
6868
return 'testgres-{}'.format(str(uuid.uuid4()))
6969

7070

71+
def generate_system_id():
72+
"""
73+
Generate a new 64-bit unique system identifier for node.
74+
"""
75+
76+
import datetime
77+
import struct
78+
79+
date1 = datetime.datetime.utcfromtimestamp(0)
80+
date2 = datetime.datetime.utcnow()
81+
82+
secs = int((date2 - date1).total_seconds())
83+
usecs = date2.microsecond
84+
85+
# see pg_resetwal.c : GuessControlValues()
86+
system_id = 0
87+
system_id |= (secs << 32)
88+
system_id |= (usecs << 12)
89+
system_id |= (os.getpid() & 0xFFF)
90+
91+
# pack ULL in native byte order
92+
return struct.pack('=Q', system_id)
93+
94+
7195
def execute_utility(args, logfile=None):
7296
"""
7397
Execute utility (pg_ctl, pg_dump etc).

tests/test_simple.py

+35-8
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@
3838
get_pg_config
3939

4040
from testgres import bound_ports
41+
from testgres.utils import pg_version_ge
4142

4243

43-
def util_is_executable(util):
44+
def util_exists(util):
4445
def good_properties(f):
45-
return (
46-
os.path.exists(f) and
47-
os.path.isfile(f) and
48-
os.access(f, os.X_OK)
49-
)
46+
# yapf: disable
47+
return (os.path.exists(f) and
48+
os.path.isfile(f) and
49+
os.access(f, os.X_OK))
5050

5151
# try to resolve it
5252
if good_properties(get_bin_path(util)):
@@ -91,6 +91,34 @@ def test_init_after_cleanup(self):
9191
node.cleanup()
9292
node.init().start().execute('select 1')
9393

94+
@unittest.skipUnless(util_exists('pg_resetwal'), 'might be missing')
95+
@unittest.skipUnless(pg_version_ge('9.6'), 'query works on 9.6+')
96+
def test_init_unique_system_id(self):
97+
# this function exists in PostgreSQL 9.6+
98+
query = 'select system_identifier from pg_control_system()'
99+
100+
with scoped_config(cache_initdb=False):
101+
with get_new_node().init().start() as node0:
102+
id0 = node0.execute(query)[0]
103+
104+
# yapf: disable
105+
with scoped_config(cache_initdb=True,
106+
cached_initdb_unique=True) as config:
107+
108+
self.assertTrue(config.cache_initdb)
109+
self.assertTrue(config.cached_initdb_unique)
110+
111+
# spawn two nodes; ids must be different
112+
with get_new_node().init().start() as node1, \
113+
get_new_node().init().start() as node2:
114+
115+
id1 = node1.execute(query)[0]
116+
id2 = node2.execute(query)[0]
117+
118+
# ids must increase
119+
self.assertGreater(id1, id0)
120+
self.assertGreater(id2, id1)
121+
94122
def test_node_exit(self):
95123
base_dir = None
96124

@@ -486,8 +514,7 @@ def test_logging(self):
486514
master.restart()
487515
self.assertTrue(master._logger.is_alive())
488516

489-
@unittest.skipUnless(
490-
util_is_executable("pgbench"), "pgbench may be missing")
517+
@unittest.skipUnless(util_exists('pgbench'), 'might be missing')
491518
def test_pgbench(self):
492519
with get_new_node().init().start() as node:
493520

0 commit comments

Comments
 (0)