Skip to content

Unique system ids for cached initdb #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion testgres/cache.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# coding: utf-8

import io
import os
import shutil

from six import raise_from

from .config import testgres_config

from .consts import XLOG_CONTROL_FILE

from .exceptions import \
InitNodeException, \
ExecUtilException

from .utils import \
get_bin_path, \
execute_utility
execute_utility, \
generate_system_id


def cached_initdb(data_dir, logfile=None, params=None):
Expand Down Expand Up @@ -42,5 +46,23 @@ def call_initdb(initdb_dir, log=None):
try:
# Copy cached initdb to current data dir
shutil.copytree(cached_data_dir, data_dir)

# Assign this node a unique system id if asked to
if testgres_config.cached_initdb_unique:
# XXX: write new unique system id to control file
# Some users might rely upon unique system ids, but
# our initdb caching mechanism breaks this contract.
pg_control = os.path.join(data_dir, XLOG_CONTROL_FILE)
with io.open(pg_control, "r+b") as f:
f.write(generate_system_id()) # overwrite id

# XXX: build new WAL segment with our system id
_params = [get_bin_path("pg_resetwal"), "-D", data_dir, "-f"]
execute_utility(_params, logfile)

except ExecUtilException as e:
msg = "Failed to reset WAL for system id"
raise_from(InitNodeException(msg), e)

except Exception as e:
raise_from(InitNodeException("Failed to spawn a node"), e)
2 changes: 2 additions & 0 deletions testgres/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class GlobalConfig(object):
Attributes:
cache_initdb: shall we use cached initdb instance?
cached_initdb_dir: shall we create a temp dir for cached initdb?
cached_initdb_unique: shall we assign new node a unique system id?

cache_pg_config: shall we cache pg_config results?

Expand All @@ -30,6 +31,7 @@ class GlobalConfig(object):

cache_initdb = True
_cached_initdb_dir = None
cached_initdb_unique = False

cache_pg_config = True

Expand Down
3 changes: 3 additions & 0 deletions testgres/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
DATA_DIR = "data"
LOGS_DIR = "logs"

# path to control file
XLOG_CONTROL_FILE = "global/pg_control"

# names for config files
RECOVERY_CONF_FILE = "recovery.conf"
PG_AUTO_CONF_FILE = "postgresql.auto.conf"
Expand Down
23 changes: 23 additions & 0 deletions testgres/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,29 @@ def generate_app_name():
return 'testgres-{}'.format(str(uuid.uuid4()))


def generate_system_id():
"""
Generate a new 64-bit unique system identifier for node.
"""

import datetime
import struct

date1 = datetime.datetime.utcfromtimestamp(0)
date2 = datetime.datetime.utcnow()

secs = int((date2 - date1).total_seconds())
usecs = date2.microsecond

system_id = 0
system_id |= (secs << 32)
system_id |= (usecs << 12)
system_id |= (os.getpid() & 0xFFF)

# pack ULL in native byte order
return struct.pack('=Q', system_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably bail out with struct.error: argument out of range after 19 January 2038, but for now it is fine :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arssher Why? We have 32 bits for seconds, and that's ~136 years.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, =Q is unsigned. Yes, then we have 68 years more.



def execute_utility(args, logfile=None):
"""
Execute utility (pg_ctl, pg_dump etc).
Expand Down
37 changes: 29 additions & 8 deletions tests/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@
get_pg_config

from testgres import bound_ports
from testgres.utils import pg_version_ge


def util_is_executable(util):
def util_exists(util):
def good_properties(f):
return (
os.path.exists(f) and
os.path.isfile(f) and
os.access(f, os.X_OK)
)
# yapf: disable
return (os.path.exists(f) and
os.path.isfile(f) and
os.access(f, os.X_OK))

# try to resolve it
if good_properties(get_bin_path(util)):
Expand Down Expand Up @@ -91,6 +91,28 @@ def test_init_after_cleanup(self):
node.cleanup()
node.init().start().execute('select 1')

@unittest.skipUnless(util_exists('pg_resetwal'), 'might be missing')
@unittest.skipUnless(pg_version_ge('9.6'), 'query works on 9.6+')
def test_init_unique_system_id(self):
with scoped_config(
cache_initdb=True, cached_initdb_unique=True) as config:

self.assertTrue(config.cache_initdb)
self.assertTrue(config.cached_initdb_unique)

# spawn two nodes; ids must be different
with get_new_node().init().start() as node1, \
get_new_node().init().start() as node2:

# this function exists in PostgreSQL 9.6+
query = 'select system_identifier from pg_control_system()'

id1 = node1.execute(query)[0]
id2 = node2.execute(query)[0]

# ids must increase
self.assertGreater(id2, id1)

def test_node_exit(self):
base_dir = None

Expand Down Expand Up @@ -486,8 +508,7 @@ def test_logging(self):
master.restart()
self.assertTrue(master._logger.is_alive())

@unittest.skipUnless(
util_is_executable("pgbench"), "pgbench may be missing")
@unittest.skipUnless(util_exists('pgbench'), 'might be missing')
def test_pgbench(self):
with get_new_node().init().start() as node:

Expand Down