Skip to content

Commit bfe3456

Browse files
PostgresNode::is_started is read-only and is not used internally (#305)
PostgresNode::is_started provides information about a manually started node. It is "set" in start method and is "reset" in stop, kill methods. These methods do not verify this flag and do not use for other aims - for example to ignore the second start or stop of a node. So we cannot stop and start our node twice. An updated implementation of kill method raises InvalidOperationException when our node is not running. This patch allows to use testgres in code where a node started or stopped explicitly, not via start/stop/kill methods.
1 parent 44d305e commit bfe3456

5 files changed

Lines changed: 336 additions & 32 deletions

File tree

src/node.py

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,14 @@ class PostgresNode(object):
160160
# a max number of node start attempts
161161
_C_MAX_START_ATEMPTS = 5
162162

163+
_C_PM_PID__IS_NOT_DETECTED = -1
164+
163165
_name: typing.Optional[str]
164166
_port: typing.Optional[int]
165167
_should_free_port: bool
166168
_os_ops: OsOperations
167169
_port_manager: typing.Optional[PortManager]
170+
_manually_started_pm_pid: typing.Optional[int]
168171

169172
def __init__(self,
170173
name=None,
@@ -251,7 +254,7 @@ def __init__(self,
251254
self.pg_log_name = self.pg_log_file
252255

253256
# Node state
254-
self.is_started = False
257+
self._manually_started_pm_pid = None
255258

256259
def __enter__(self):
257260
return self
@@ -380,6 +383,14 @@ def pid(self) -> int:
380383
assert type(x.pid) == int # noqa: E721
381384
return x.pid
382385

386+
@property
387+
def is_started(self) -> bool:
388+
if self._manually_started_pm_pid is None:
389+
return False
390+
391+
assert type(self._manually_started_pm_pid) == int # noqa: E721
392+
return True
393+
383394
@property
384395
def auxiliary_pids(self) -> typing.Dict[ProcessType, typing.List[int]]:
385396
"""
@@ -995,9 +1006,6 @@ def start(self, params=[], wait=True, exec_env=None):
9951006
assert exec_env is None or type(exec_env) == dict # noqa: E721
9961007
assert __class__._C_MAX_START_ATEMPTS > 1
9971008

998-
if self.is_started:
999-
return self
1000-
10011009
if self._port is None:
10021010
raise InvalidOperationException("Can't start PostgresNode. Port is not defined.")
10031011

@@ -1016,8 +1024,11 @@ def LOCAL__start_node():
10161024
if error and 'does not exist' in error:
10171025
raise Exception(error)
10181026

1019-
def LOCAL__raise_cannot_start_node(from_exception, msg):
1020-
assert isinstance(from_exception, Exception)
1027+
def LOCAL__raise_cannot_start_node(
1028+
from_exception: typing.Optional[Exception],
1029+
msg: str
1030+
):
1031+
assert from_exception is None or isinstance(from_exception, Exception)
10211032
assert type(msg) == str # noqa: E721
10221033
files = self._collect_special_files()
10231034
raise_from(StartNodeException(msg, files), from_exception)
@@ -1076,7 +1087,17 @@ def LOCAL__raise_cannot_start_node__std(from_exception):
10761087
continue
10771088
break
10781089
self._maybe_start_logger()
1079-
self.is_started = True
1090+
1091+
if not wait:
1092+
# Postmaster process is starting in background
1093+
self._manually_started_pm_pid = __class__._C_PM_PID__IS_NOT_DETECTED
1094+
else:
1095+
self._manually_started_pm_pid = self._get_node_state().pid
1096+
if self._manually_started_pm_pid is None:
1097+
LOCAL__raise_cannot_start_node(None, "Cannot detect postmaster pid.")
1098+
1099+
assert type(self._manually_started_pm_pid) == int # noqa: E721
1100+
10801101
return self
10811102

10821103
def stop(self, params=[], wait=True):
@@ -1090,9 +1111,6 @@ def stop(self, params=[], wait=True):
10901111
Returns:
10911112
This instance of :class:`.PostgresNode`.
10921113
"""
1093-
if not self.is_started:
1094-
return self
1095-
10961114
_params = [
10971115
self._get_bin_path("pg_ctl"),
10981116
"-D", self.data_dir,
@@ -1102,8 +1120,9 @@ def stop(self, params=[], wait=True):
11021120

11031121
execute_utility2(self.os_ops, _params, self.utils_log_file)
11041122

1123+
self._manually_started_pm_pid = None
1124+
11051125
self._maybe_stop_logger()
1106-
self.is_started = False
11071126
return self
11081127

11091128
def kill(self, someone=None):
@@ -1114,14 +1133,27 @@ def kill(self, someone=None):
11141133
someone: A key to the auxiliary process in the auxiliary_pids dictionary.
11151134
If None, the main PostgreSQL node process will be killed. Defaults to None.
11161135
"""
1117-
if self.is_started:
1118-
assert isinstance(self._os_ops, OsOperations)
1119-
sig = signal.SIGKILL if os.name != 'nt' else signal.SIGBREAK
1120-
if someone is None:
1121-
self._os_ops.kill(self.pid, sig)
1122-
else:
1123-
self._os_ops.kill(self.auxiliary_pids[someone][0], sig)
1124-
self.is_started = False
1136+
x = self._get_node_state()
1137+
assert type(x) == utils.PostgresNodeState # noqa: E721
1138+
1139+
if x.node_status != NodeStatus.Running:
1140+
RaiseError.node_err__cant_kill(x.node_status)
1141+
assert False
1142+
1143+
assert x.node_status == NodeStatus.Running
1144+
assert type(x.pid) == int # noqa: E721
1145+
sig = signal.SIGKILL if os.name != 'nt' else signal.SIGBREAK
1146+
if someone is None:
1147+
self._os_ops.kill(x.pid, sig)
1148+
self._manually_started_pm_pid = None
1149+
else:
1150+
childs = self._get_child_processes(x.pid)
1151+
for c in childs:
1152+
assert type(c) == ProcessProxy # noqa: E721
1153+
if c.ptype == someone:
1154+
self._os_ops.kill(c.process.pid, sig)
1155+
continue
1156+
return
11251157

11261158
def restart(self, params=[]):
11271159
"""

src/raise_error.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ def node_err__cant_enumerate_child_processes(
4545

4646
raise InvalidOperationException(msg)
4747

48+
@staticmethod
49+
def node_err__cant_kill(
50+
node_status: NodeStatus
51+
):
52+
assert type(node_status) == NodeStatus # noqa: E721
53+
54+
msg = "Can't kill server process. {}.".format(
55+
__class__._map_node_status_to_reason(
56+
node_status,
57+
None,
58+
)
59+
)
60+
61+
raise InvalidOperationException(msg)
62+
4863
@staticmethod
4964
def _map_node_status_to_reason(
5065
node_status: NodeStatus,

tests/test_raise_error.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

99
class TestRaiseError:
10-
class tagData001__NodeErr_CantEnumerateChildProcesses:
10+
class tagTestData001:
1111
node_status: NodeStatus
1212
expected_msg: str
1313

@@ -29,12 +29,12 @@ def sign(self) -> str:
2929
msg = "status: {}".format(self.node_status)
3030
return msg
3131

32-
sm_Data001: typing.List[tagData001__NodeErr_CantEnumerateChildProcesses] = [
33-
tagData001__NodeErr_CantEnumerateChildProcesses(
32+
sm_Data001: typing.List[tagTestData001] = [
33+
tagTestData001(
3434
NodeStatus.Uninitialized,
3535
"Can't enumerate node child processes. Node is not initialized.",
3636
),
37-
tagData001__NodeErr_CantEnumerateChildProcesses(
37+
tagTestData001(
3838
NodeStatus.Stopped,
3939
"Can't enumerate node child processes. Node is not running.",
4040
),
@@ -44,16 +44,16 @@ def sign(self) -> str:
4444
params=sm_Data001,
4545
ids=[x.sign for x in sm_Data001],
4646
)
47-
def data001(self, request: pytest.FixtureRequest) -> tagData001__NodeErr_CantEnumerateChildProcesses:
47+
def data001(self, request: pytest.FixtureRequest) -> tagTestData001:
4848
assert isinstance(request, pytest.FixtureRequest)
49-
assert type(request.param).__name__ == "tagData001__NodeErr_CantEnumerateChildProcesses"
49+
assert type(request.param).__name__ == "tagTestData001"
5050
return request.param
5151

5252
def test_001__node_err__cant_enumerate_child_processes(
5353
self,
54-
data001: tagData001__NodeErr_CantEnumerateChildProcesses,
54+
data001: tagTestData001,
5555
):
56-
assert type(data001) == __class__.tagData001__NodeErr_CantEnumerateChildProcesses # noqa: E721
56+
assert type(data001) == __class__.tagTestData001 # noqa: E721
5757

5858
with pytest.raises(expected_exception=InvalidOperationException) as x:
5959
RaiseError.node_err__cant_enumerate_child_processes(
@@ -63,3 +63,38 @@ def test_001__node_err__cant_enumerate_child_processes(
6363
assert x is not None
6464
assert str(x.value) == data001.expected_msg
6565
return
66+
67+
sm_Data002: typing.List[tagTestData001] = [
68+
tagTestData001(
69+
NodeStatus.Uninitialized,
70+
"Can't kill server process. Node is not initialized.",
71+
),
72+
tagTestData001(
73+
NodeStatus.Stopped,
74+
"Can't kill server process. Node is not running.",
75+
),
76+
]
77+
78+
@pytest.fixture(
79+
params=sm_Data002,
80+
ids=[x.sign for x in sm_Data002],
81+
)
82+
def data002(self, request: pytest.FixtureRequest) -> tagTestData001:
83+
assert isinstance(request, pytest.FixtureRequest)
84+
assert type(request.param).__name__ == "tagTestData001"
85+
return request.param
86+
87+
def test_002__node_err__cant_kill(
88+
self,
89+
data002: tagTestData001,
90+
):
91+
assert type(data002) == __class__.tagTestData001 # noqa: E721
92+
93+
with pytest.raises(expected_exception=InvalidOperationException) as x:
94+
RaiseError.node_err__cant_kill(
95+
data002.node_status
96+
)
97+
98+
assert x is not None
99+
assert str(x.value) == data002.expected_msg
100+
return

0 commit comments

Comments
 (0)