Skip to content

Commit f42e43b

Browse files
PostgresNode::start and start2 are updated (#326)
- parameter params can be None or list - typing - start2 documentation is corrected - asserts are added - new unit tests for start and start2 methods - start and start2 are using new internal method _start that does not have default parameters
1 parent 098b688 commit f42e43b

File tree

8 files changed

+733
-9
lines changed

8 files changed

+733
-9
lines changed

src/node.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,12 @@ def slow_start(self, replica=False, dbname='template1', username=None, max_attem
10021002
raise
10031003
return
10041004

1005-
def start(self, params=[], wait=True, exec_env=None) -> PostgresNode:
1005+
def start(
1006+
self,
1007+
params: typing.Optional[typing.List[str]] = None,
1008+
wait: bool = True,
1009+
exec_env: typing.Optional[typing.Dict] = None,
1010+
) -> PostgresNode:
10061011
"""
10071012
Starts the PostgreSQL node using pg_ctl and set flag 'is_started'.
10081013
By default, it waits for the operation to complete before returning.
@@ -1016,7 +1021,11 @@ def start(self, params=[], wait=True, exec_env=None) -> PostgresNode:
10161021
Returns:
10171022
This instance of :class:`.PostgresNode`.
10181023
"""
1019-
self.start2(params, wait, exec_env)
1024+
assert params is None or type(params) == list # noqa: E721
1025+
assert type(wait) == bool # noqa: E721
1026+
assert exec_env is None or type(exec_env) == dict # noqa: E721
1027+
1028+
self._start(params, wait, exec_env)
10201029

10211030
if not wait:
10221031
# Postmaster process is starting in background
@@ -1029,7 +1038,12 @@ def start(self, params=[], wait=True, exec_env=None) -> PostgresNode:
10291038
assert type(self._manually_started_pm_pid) == int # noqa: E721
10301039
return self
10311040

1032-
def start2(self, params=[], wait=True, exec_env=None) -> None:
1041+
def start2(
1042+
self,
1043+
params: typing.Optional[typing.List[str]] = None,
1044+
wait: bool = True,
1045+
exec_env: typing.Optional[typing.Dict] = None,
1046+
) -> None:
10331047
"""
10341048
Starts the PostgreSQL node using pg_ctl.
10351049
By default, it waits for the operation to complete before returning.
@@ -1041,21 +1055,43 @@ def start2(self, params=[], wait=True, exec_env=None) -> None:
10411055
wait: wait until operation completes.
10421056
10431057
Returns:
1044-
This instance of :class:`.PostgresNode`.
1058+
None.
10451059
"""
1060+
assert params is None or type(params) == list # noqa: E721
1061+
assert type(wait) == bool # noqa: E721
1062+
assert exec_env is None or type(exec_env) == dict # noqa: E721
1063+
1064+
self._start(params, wait, exec_env)
1065+
return
1066+
1067+
def _start(
1068+
self,
1069+
params: typing.Optional[typing.List[str]] = None,
1070+
wait: bool = True,
1071+
exec_env: typing.Optional[typing.Dict] = None,
1072+
) -> None:
1073+
assert params is None or type(params) == list # noqa: E721
1074+
assert type(wait) == bool # noqa: E721
10461075
assert exec_env is None or type(exec_env) == dict # noqa: E721
1076+
10471077
assert __class__._C_MAX_START_ATEMPTS > 1
10481078

10491079
if self._port is None:
10501080
raise InvalidOperationException("Can't start PostgresNode. Port is not defined.")
10511081

10521082
assert type(self._port) == int # noqa: E721
10531083

1054-
_params = [self._get_bin_path("pg_ctl"),
1055-
"-D", self.data_dir,
1056-
"-l", self.pg_log_file,
1057-
"-w" if wait else '-W', # --wait or --no-wait
1058-
"start"] + params # yapf: disable
1084+
_params = [
1085+
self._get_bin_path("pg_ctl"),
1086+
"start",
1087+
"-D", self.data_dir,
1088+
"-l", self.pg_log_file,
1089+
"-w" if wait else '-W', # --wait or --no-wait
1090+
]
1091+
1092+
if params is not None:
1093+
assert type(params) == list # noqa: E721
1094+
_params += params
10591095

10601096
def LOCAL__start_node():
10611097
# 'error' will be None on Windows

tests/helpers/global_data.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,9 @@ class PostgresNodeServices:
7676
OsOpsDescrs.sm_local_os_ops,
7777
PortManagers.sm_local2_port_manager
7878
)
79+
80+
sm_locals_and_remotes = [
81+
sm_local,
82+
sm_local2,
83+
sm_remote,
84+
]

tests/helpers/pg_node_utils.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
from src import PostgresNode
2+
from src import PortManager
3+
from src import OsOperations
4+
from src import NodeStatus
5+
from src.node import PostgresNodeLogReader
6+
7+
from tests.helpers.utils import Utils as HelperUtils
8+
from tests.helpers.utils import T_WAIT_TIME
9+
10+
from tests.helpers.global_data import PostgresNodeService
11+
12+
import typing
13+
14+
15+
class PostgresNodeUtils:
16+
class PostgresNodeUtilsException(Exception):
17+
pass
18+
19+
class PortConflictNodeException(PostgresNodeUtilsException):
20+
_data_dir: str
21+
_port: int
22+
23+
def __init__(self, data_dir: str, port: int):
24+
assert type(data_dir) == str # noqa: E721
25+
assert type(port) == int # noqa: E721
26+
27+
super().__init__()
28+
29+
self._data_dir = data_dir
30+
self._port = port
31+
return
32+
33+
@property
34+
def data_dir(self) -> str:
35+
assert type(self._data_dir) == str # noqa: E721
36+
return self._data_dir
37+
38+
@property
39+
def port(self) -> int:
40+
assert type(self._port) == int # noqa: E721
41+
return self._port
42+
43+
@property
44+
def message(self) -> str:
45+
assert type(self._data_dir) == str # noqa: E721
46+
assert type(self._port) == int # noqa: E721
47+
48+
r = "PostgresNode [data:{}][port: {}] conflicts with port of another instance.".format(
49+
self._data_dir,
50+
self._port,
51+
)
52+
assert type(r) == str # noqa: E721
53+
return r
54+
55+
def __str__(self) -> str:
56+
r = self.message
57+
assert type(r) == str # noqa: E721
58+
return r
59+
60+
def __repr__(self) -> str:
61+
# It must be overrided!
62+
assert type(self) == __class__ # noqa: E721
63+
r = "{}({}, {})".format(
64+
__class__.__name__,
65+
repr(self._data_dir),
66+
repr(self._port),
67+
)
68+
assert type(r) == str # noqa: E721
69+
return r
70+
71+
# --------------------------------------------------------------------
72+
class StartNodeException(PostgresNodeUtilsException):
73+
_data_dir: str
74+
_files: typing.Optional[typing.Iterable]
75+
76+
def __init__(
77+
self,
78+
data_dir: str,
79+
files: typing.Optional[typing.Iterable] = None
80+
):
81+
assert type(data_dir) == str # noqa: E721
82+
assert files is None or isinstance(files, typing.Iterable)
83+
84+
super().__init__()
85+
86+
self._data_dir = data_dir
87+
self._files = files
88+
return
89+
90+
@property
91+
def message(self) -> str:
92+
assert self._data_dir is None or type(self._data_dir) == str # noqa: E721
93+
assert self._files is None or isinstance(self._files, typing.Iterable)
94+
95+
msg_parts = []
96+
97+
msg_parts.append("PostgresNode [data_dir: {}] is not started.".format(
98+
self._data_dir
99+
))
100+
101+
for f, lines in self._files or []:
102+
assert type(f) == str # noqa: E721
103+
assert type(lines) in [str, bytes] # noqa: E721
104+
msg_parts.append(u'{}\n----\n{}\n'.format(f, lines))
105+
106+
return "\n".join(msg_parts)
107+
108+
@property
109+
def data_dir(self) -> typing.Optional[str]:
110+
assert type(self._data_dir) == str # noqa: E721
111+
return self._data_dir
112+
113+
@property
114+
def files(self) -> typing.Optional[typing.Iterable]:
115+
assert self._files is None or isinstance(self._files, typing.Iterable)
116+
return self._files
117+
118+
def __repr__(self) -> str:
119+
assert type(self._data_dir) == str # noqa: E721
120+
assert self._files is None or isinstance(self._files, typing.Iterable)
121+
122+
r = "{}({}, {})".format(
123+
__class__.__name__,
124+
repr(self._data_dir),
125+
repr(self._files),
126+
)
127+
assert type(r) == str # noqa: E721
128+
return r
129+
130+
# --------------------------------------------------------------------
131+
@staticmethod
132+
def get_node(
133+
node_svc: PostgresNodeService,
134+
name: typing.Optional[str] = None,
135+
port: typing.Optional[int] = None,
136+
port_manager: typing.Optional[PortManager] = None
137+
) -> PostgresNode:
138+
assert isinstance(node_svc, PostgresNodeService)
139+
assert isinstance(node_svc.os_ops, OsOperations)
140+
assert isinstance(node_svc.port_manager, PortManager)
141+
142+
if port_manager is None:
143+
port_manager = node_svc.port_manager
144+
145+
return PostgresNode(
146+
name,
147+
port=port,
148+
os_ops=node_svc.os_ops,
149+
port_manager=port_manager if port is None else None
150+
)
151+
152+
# --------------------------------------------------------------------
153+
@staticmethod
154+
def wait_for_running_state(
155+
node: PostgresNode,
156+
node_log_reader: PostgresNodeLogReader,
157+
timeout: T_WAIT_TIME,
158+
):
159+
assert type(node) == PostgresNode # noqa: E721
160+
assert type(node_log_reader) == PostgresNodeLogReader # noqa: E721
161+
assert type(timeout) in [int, float]
162+
assert node_log_reader._node is node
163+
assert timeout > 0
164+
165+
for _ in HelperUtils.WaitUntil(
166+
timeout=timeout
167+
):
168+
s = node.status()
169+
170+
if s == NodeStatus.Running:
171+
return
172+
173+
assert s == NodeStatus.Stopped
174+
175+
blocks = node_log_reader.read()
176+
assert type(blocks) == list # noqa: E721
177+
178+
for block in blocks:
179+
assert type(block) == PostgresNodeLogReader.LogDataBlock # noqa: E721
180+
181+
if 'Is another postmaster already running on port' in block.data:
182+
raise __class__.PortConflictNodeException(node.data_dir, node.port)
183+
184+
if 'database system is shut down' in block.data:
185+
raise __class__.StartNodeException(
186+
node.data_dir,
187+
node._collect_special_files(),
188+
)
189+
continue

tests/helpers/utils.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import typing
2+
import time
3+
import logging
4+
5+
6+
T_WAIT_TIME = typing.Union[int, float]
7+
8+
9+
class Utils:
10+
@staticmethod
11+
def PrintAndSleep(wait: T_WAIT_TIME):
12+
assert type(wait) in [int, float]
13+
logging.info("Wait for {} second(s)".format(wait))
14+
time.sleep(wait)
15+
return
16+
17+
@staticmethod
18+
def WaitUntil(
19+
error_message: str = "Did not complete",
20+
timeout: T_WAIT_TIME = 30,
21+
interval: T_WAIT_TIME = 1,
22+
notification_interval: T_WAIT_TIME = 5,
23+
):
24+
"""
25+
Loop until the timeout is reached. If the timeout is reached, raise an
26+
exception with the given error message.
27+
28+
Source of idea: pgbouncer
29+
"""
30+
assert type(timeout) in [int, float]
31+
assert type(interval) in [int, float]
32+
assert type(notification_interval) in [int, float]
33+
assert timeout >= 0
34+
assert interval >= 0
35+
assert notification_interval >= 0
36+
37+
start_ts = time.monotonic()
38+
end_ts = start_ts + timeout
39+
last_printed_progress = start_ts
40+
last_iteration_ts = start_ts
41+
42+
yield
43+
attempt = 1
44+
45+
while end_ts > time.monotonic():
46+
if (timeout > 5 and time.monotonic() - last_printed_progress) > notification_interval:
47+
last_printed_progress = time.monotonic()
48+
49+
m = "{} in {} seconds and {} attempts - will retry".format(
50+
error_message,
51+
time.monotonic() - start_ts,
52+
attempt,
53+
)
54+
logging.info(m)
55+
56+
interval_remaining = last_iteration_ts + interval - time.monotonic()
57+
if interval_remaining > 0:
58+
time.sleep(interval_remaining)
59+
60+
last_iteration_ts = time.monotonic()
61+
yield
62+
attempt += 1
63+
continue
64+
65+
raise TimeoutError(error_message + " in time")

tests/units/node/PostgresNode/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)