Skip to content

Commit

Permalink
fix release
Browse files Browse the repository at this point in the history
  • Loading branch information
orm011 committed May 16, 2024
1 parent cfe9f53 commit 54e0fd5
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 27 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pgserver" # Required
version = "0.1.2" # Required
version = "0.1.3" # Required
description = "Self-contained postgres server for your python applications" # Required
readme = "README.md" # Optional
requires-python = ">=3.9"
Expand Down
10 changes: 7 additions & 3 deletions src/pgserver/_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,20 @@ def command(args : List[str], pgdata : Optional[Path] = None, **kwargs) -> str:
**kwargs)
stdout.seek(0)
stderr.seek(0)
output = stdout.read()
error = stderr.read()
_logger.info("Successful postgres command %s with kwargs: `%s`\nstdout:\n%s\n---\nstderr:\n%s\n---\n",
result.args, kwargs, stdout.read(), stderr.read())
result.args, kwargs, output, error)
except subprocess.CalledProcessError as err:
stdout.seek(0)
stderr.seek(0)
output = stdout.read()
error = stderr.read()
_logger.error("Failed postgres command %s with kwargs: `%s`:\nerror:\n%s\nstdout:\n%s\n---\nstderr:\n%s\n---\n",
err.args, kwargs, str(err), stdout.read(), stderr.read())
err.args, kwargs, str(err), output, error)
raise err

return result.stdout
return output

return command

Expand Down
26 changes: 19 additions & 7 deletions src/pgserver/postgres_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
import platform
import psutil
import time

from ._commands import POSTGRES_BIN_PATH, initdb, pg_ctl
from .utils import find_suitable_port, find_suitable_socket_dir, DiskList, PostmasterInfo, process_is_running
Expand Down Expand Up @@ -109,6 +110,7 @@ def ensure_pgdata_inited(self) -> None:
#
# Since we do not know PID information of the old server, we stop all servers with the same pgdata path.
# way to test this: python -c 'import pixeltable as pxt; pxt.Client()'; rm -rf ~/.pixeltable/; python -c 'import pixeltable as pxt; pxt.Client()'
_logger.info('no PG_VERSION file found, initializing pgdata')
for proc in psutil.process_iter(attrs=['name', 'cmdline']):
if proc.info['name'] == 'postgres':
if proc.info['cmdline'] is not None and str(self.pgdata) in proc.info['cmdline']:
Expand All @@ -125,13 +127,22 @@ def ensure_pgdata_inited(self) -> None:

initdb(['--auth=trust', '--auth-local=trust', '--encoding=utf8', '-U', self.postgres_user], pgdata=self.pgdata,
user=self.system_user)
else:
_logger.info('PG_VERSION file found, skipping initdb')

def ensure_postgres_running(self) -> None:
""" pre condition: pgdata is initialized
""" pre condition: pgdata is initialized, being run with lock.
post condition: self._postmaster_info is set.
"""
self._postmaster_info = PostmasterInfo.read_from_pgdata(self.pgdata)
if self._postmaster_info is None:

postmaster_info = PostmasterInfo.read_from_pgdata(self.pgdata)
if postmaster_info is not None and postmaster_info.is_running():
self._postmaster_info = postmaster_info
_logger.info(f"server already running: {self._postmaster_info=} {self._postmaster_info.process=}")

if self._postmaster_info.status != 'ready':
_logger.warning(f"server running but somehow not ready: {self._postmaster_info=}")
else:
if platform.system() != 'Windows':
# use sockets to avoid any future conflict with port numbers
socket_dir = find_suitable_socket_dir(self.pgdata, self.runtime_path)
Expand Down Expand Up @@ -159,6 +170,7 @@ def ensure_postgres_running(self) -> None:
]

try:
_logger.info(f"running pg_ctl... {pg_ctl_args=}")
pg_ctl(pg_ctl_args,pgdata=self.pgdata, user=self.system_user, timeout=10)
except subprocess.CalledProcessError as err:
_logger.error(f"Failed to start server.\nShowing contents of postgres server log ({self.log.absolute()}) below:\n{self.log.read_text()}")
Expand All @@ -167,12 +179,12 @@ def ensure_postgres_running(self) -> None:
_logger.error(f"Timeout starting server.\nShowing contents of postgres server log ({self.log.absolute()}) below:\n{self.log.read_text()}")
raise err

self._postmaster_info = PostmasterInfo.read_from_pgdata(self.pgdata)
_logger.info(f"{self._postmaster_info=}")
self._postmaster_info = PostmasterInfo.read_from_pgdata(self.pgdata)

_logger.info(f"ensuring server is running: {self._postmaster_info=}")
assert self._postmaster_info is not None
assert self._postmaster_info.is_running()
assert self._postmaster_info.status == 'ready'
assert self._postmaster_info.pid is not None


def _cleanup(self) -> None:
with self._lock:
Expand Down
26 changes: 19 additions & 7 deletions src/pgserver/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,26 @@ def __init__(self, lines : List[str]):
self.shmem_info = raw['shared_memory_info']
self.status = raw['status']

self._process = None
self.process = None # will be not None if process is running
self._init_process_meta()

@property
def process(self) -> psutil.Process:
assert self.pid is not None
if self._process is None:
self._process = psutil.Process(self.pid)
return self._process
def _init_process_meta(self) -> Optional[psutil.Process]:
if self.pid is None:
return
try:
process = psutil.Process(self.pid)
except psutil.NoSuchProcess:
return

if self.start_time is None:
return

exact_create_time = datetime.datetime.fromtimestamp(process.create_time())
if abs(self.start_time - exact_create_time) <= datetime.timedelta(seconds=1):
self.process = process

def is_running(self) -> bool:
return self.process is not None and self.process.is_running()

@classmethod
def read_from_pgdata(cls, pgdata : Path) -> Optional['PostmasterInfo']:
Expand Down
51 changes: 42 additions & 9 deletions tests/test_pgserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ def _check_postmaster_info(pgdata : Path, postmaster_info : pgserver.utils.Postm
assert postmaster_info.pgdata is not None
assert postmaster_info.pgdata == pgdata

assert postmaster_info.pid is not None
assert postmaster_info.process.is_running()
exact_create_time = datetime.datetime.fromtimestamp(postmaster_info.process.create_time())
assert postmaster_info.start_time is not None
assert abs(postmaster_info.start_time - exact_create_time) <= datetime.timedelta(seconds=1)
assert postmaster_info.is_running()

if postmaster_info.socket_dir is not None:
assert postmaster_info.socket_dir.exists()
Expand Down Expand Up @@ -175,6 +171,44 @@ def test_unix_domain_socket():
finally:
_kill_server(pid)

def test_pg_ctl():
with tempfile.TemporaryDirectory() as tmpdir:
pid = None
try:
with pgserver.get_server(tmpdir) as pg:
output = pgserver.pg_ctl(['status'], str(pg.pgdata))
assert 'server is running' in output.splitlines()[0]

finally:
_kill_server(pid)

def test_stale_postmaster():
""" To simulate a stale postmaster.pid file, we create a postmaster.pid file by starting a server,
back the file up, then restore the backup to the original location after killing the server.
( our method to kill the server is graceful to avoid running out of shmem, but this seems to also
remove the postmaster.pid file, so we need to go to these lengths to simulate a stale postmaster.pid file )
"""
with tempfile.TemporaryDirectory() as tmpdir:
pid = None
pid2 = None
try:
with pgserver.get_server(tmpdir, cleanup_mode='stop') as pg:
pid = _check_server(pg)
pgdata = pg.pgdata
postmaster_pid = pgdata / 'postmaster.pid'

## make a backup of the postmaster.pid file
shutil.copy2(str(postmaster_pid), str(postmaster_pid) + '.bak')

# restore the backup to gurantee a stale postmaster.pid file
shutil.copy2(str(postmaster_pid) + '.bak', str(postmaster_pid))
with pgserver.get_server(tmpdir) as pg:
pid2 = _check_server(pg)
finally:
_kill_server(pid)
_kill_server(pid2)


def test_cleanup_delete():
with tempfile.TemporaryDirectory() as tmpdir:
pid = None
Expand Down Expand Up @@ -243,8 +277,8 @@ def test_no_conflict():


def _reuse_deleted_datadir(prefix: str):
""" test common scenario where we repeatedly delete the datadir and start a new server on it."""
"""NB: currently this test is not reproducing the problem"""
""" test common scenario where we repeatedly delete the datadir and start a new server on it """
""" NB: currently this test is not reproducing the problem """
# one can reproduce the problem by running the following in a loop:
# python -c 'import pixeltable as pxt; pxt.Client()'; rm -rf ~/.pixeltable/; python -c 'import pixeltable as pxt; pxt.Client()'
# which creates a database with more contents etc
Expand All @@ -255,8 +289,7 @@ def _reuse_deleted_datadir(prefix: str):

num_tries = 3
try:
for iter in range(num_tries):
print(f"iteration {iter}")
for _ in range(num_tries):
assert not pgdata.exists()

queue_from_child = mp.Queue()
Expand Down

0 comments on commit 54e0fd5

Please sign in to comment.