Skip to content

Commit 3bb59c6

Browse files
[#235] test_pg_ctl_wait_option detects a port conflict (#257)
This patch must fix a problem in test_pg_ctl_wait_option when his PostgreSQL instance conflicts with another one. For this, we added two new things: - PostgresNodeLogReader - PostgresNodeUtils PostgresNodeLogReader reads server logs. PostgresNodeUtils provides an utility to detect a port conflict. PostgresNode::start also uses these new classes.
1 parent edd64db commit 3bb59c6

File tree

2 files changed

+200
-45
lines changed

2 files changed

+200
-45
lines changed

testgres/node.py

Lines changed: 165 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -784,28 +784,6 @@ def _collect_special_files(self):
784784

785785
return result
786786

787-
def _collect_log_files(self):
788-
# dictionary of log files + size in bytes
789-
790-
files = [
791-
self.pg_log_file
792-
] # yapf: disable
793-
794-
result = {}
795-
796-
for f in files:
797-
# skip missing files
798-
if not self.os_ops.path_exists(f):
799-
continue
800-
801-
file_size = self.os_ops.get_file_size(f)
802-
assert type(file_size) == int # noqa: E721
803-
assert file_size >= 0
804-
805-
result[f] = file_size
806-
807-
return result
808-
809787
def init(self, initdb_params=None, cached=True, **kwargs):
810788
"""
811789
Perform initdb for this node.
@@ -1062,22 +1040,6 @@ def slow_start(self, replica=False, dbname='template1', username=None, max_attem
10621040
OperationalError},
10631041
max_attempts=max_attempts)
10641042

1065-
def _detect_port_conflict(self, log_files0, log_files1):
1066-
assert type(log_files0) == dict # noqa: E721
1067-
assert type(log_files1) == dict # noqa: E721
1068-
1069-
for file in log_files1.keys():
1070-
read_pos = 0
1071-
1072-
if file in log_files0.keys():
1073-
read_pos = log_files0[file] # the previous size
1074-
1075-
file_content = self.os_ops.read_binary(file, read_pos)
1076-
file_content_s = file_content.decode()
1077-
if 'Is another postmaster already running on port' in file_content_s:
1078-
return True
1079-
return False
1080-
10811043
def start(self, params=[], wait=True, exec_env=None):
10821044
"""
10831045
Starts the PostgreSQL node using pg_ctl if node has not been started.
@@ -1137,8 +1099,7 @@ def LOCAL__raise_cannot_start_node__std(from_exception):
11371099
assert isinstance(self._port_manager, PortManager)
11381100
assert __class__._C_MAX_START_ATEMPTS > 1
11391101

1140-
log_files0 = self._collect_log_files()
1141-
assert type(log_files0) == dict # noqa: E721
1102+
log_reader = PostgresNodeLogReader(self, from_beginnig=False)
11421103

11431104
nAttempt = 0
11441105
timeout = 1
@@ -1154,11 +1115,11 @@ def LOCAL__raise_cannot_start_node__std(from_exception):
11541115
if nAttempt == __class__._C_MAX_START_ATEMPTS:
11551116
LOCAL__raise_cannot_start_node(e, "Cannot start node after multiple attempts.")
11561117

1157-
log_files1 = self._collect_log_files()
1158-
if not self._detect_port_conflict(log_files0, log_files1):
1118+
is_it_port_conflict = PostgresNodeUtils.delect_port_conflict(log_reader)
1119+
1120+
if not is_it_port_conflict:
11591121
LOCAL__raise_cannot_start_node__std(e)
11601122

1161-
log_files0 = log_files1
11621123
logging.warning(
11631124
"Detected a conflict with using the port {0}. Trying another port after a {1}-second sleep...".format(self._port, timeout)
11641125
)
@@ -2192,6 +2153,167 @@ def _escape_config_value(value):
21922153
return result
21932154

21942155

2156+
class PostgresNodeLogReader:
2157+
class LogInfo:
2158+
position: int
2159+
2160+
def __init__(self, position: int):
2161+
self.position = position
2162+
2163+
# --------------------------------------------------------------------
2164+
class LogDataBlock:
2165+
_file_name: str
2166+
_position: int
2167+
_data: str
2168+
2169+
def __init__(
2170+
self,
2171+
file_name: str,
2172+
position: int,
2173+
data: str
2174+
):
2175+
assert type(file_name) == str # noqa: E721
2176+
assert type(position) == int # noqa: E721
2177+
assert type(data) == str # noqa: E721
2178+
assert file_name != ""
2179+
assert position >= 0
2180+
self._file_name = file_name
2181+
self._position = position
2182+
self._data = data
2183+
2184+
@property
2185+
def file_name(self) -> str:
2186+
assert type(self._file_name) == str # noqa: E721
2187+
assert self._file_name != ""
2188+
return self._file_name
2189+
2190+
@property
2191+
def position(self) -> int:
2192+
assert type(self._position) == int # noqa: E721
2193+
assert self._position >= 0
2194+
return self._position
2195+
2196+
@property
2197+
def data(self) -> str:
2198+
assert type(self._data) == str # noqa: E721
2199+
return self._data
2200+
2201+
# --------------------------------------------------------------------
2202+
_node: PostgresNode
2203+
_logs: typing.Dict[str, LogInfo]
2204+
2205+
# --------------------------------------------------------------------
2206+
def __init__(self, node: PostgresNode, from_beginnig: bool):
2207+
assert node is not None
2208+
assert isinstance(node, PostgresNode)
2209+
assert type(from_beginnig) == bool # noqa: E721
2210+
2211+
self._node = node
2212+
2213+
if from_beginnig:
2214+
self._logs = dict()
2215+
else:
2216+
self._logs = self._collect_logs()
2217+
2218+
assert type(self._logs) == dict # noqa: E721
2219+
return
2220+
2221+
def read(self) -> typing.List[LogDataBlock]:
2222+
assert self._node is not None
2223+
assert isinstance(self._node, PostgresNode)
2224+
2225+
cur_logs: typing.Dict[__class__.LogInfo] = self._collect_logs()
2226+
assert cur_logs is not None
2227+
assert type(cur_logs) == dict # noqa: E721
2228+
2229+
assert type(self._logs) == dict # noqa: E721
2230+
2231+
result = list()
2232+
2233+
for file_name, cur_log_info in cur_logs.items():
2234+
assert type(file_name) == str # noqa: E721
2235+
assert type(cur_log_info) == __class__.LogInfo # noqa: E721
2236+
2237+
read_pos = 0
2238+
2239+
if file_name in self._logs.keys():
2240+
prev_log_info = self._logs[file_name]
2241+
assert type(prev_log_info) == __class__.LogInfo # noqa: E721
2242+
read_pos = prev_log_info.position # the previous size
2243+
2244+
file_content_b = self._node.os_ops.read_binary(file_name, read_pos)
2245+
assert type(file_content_b) == bytes # noqa: E721
2246+
2247+
#
2248+
# A POTENTIAL PROBLEM: file_content_b may contain an incompleted UTF-8 symbol.
2249+
#
2250+
file_content_s = file_content_b.decode()
2251+
assert type(file_content_s) == str # noqa: E721
2252+
2253+
next_read_pos = read_pos + len(file_content_b)
2254+
2255+
# It is a research/paranoja check.
2256+
# When we will process partial UTF-8 symbol, it must be adjusted.
2257+
assert cur_log_info.position <= next_read_pos
2258+
2259+
cur_log_info.position = next_read_pos
2260+
2261+
block = __class__.LogDataBlock(
2262+
file_name,
2263+
read_pos,
2264+
file_content_s
2265+
)
2266+
2267+
result.append(block)
2268+
2269+
# A new check point
2270+
self._logs = cur_logs
2271+
2272+
return result
2273+
2274+
def _collect_logs(self) -> typing.Dict[LogInfo]:
2275+
assert self._node is not None
2276+
assert isinstance(self._node, PostgresNode)
2277+
2278+
files = [
2279+
self._node.pg_log_file
2280+
] # yapf: disable
2281+
2282+
result = dict()
2283+
2284+
for f in files:
2285+
assert type(f) == str # noqa: E721
2286+
2287+
# skip missing files
2288+
if not self._node.os_ops.path_exists(f):
2289+
continue
2290+
2291+
file_size = self._node.os_ops.get_file_size(f)
2292+
assert type(file_size) == int # noqa: E721
2293+
assert file_size >= 0
2294+
2295+
result[f] = __class__.LogInfo(file_size)
2296+
2297+
return result
2298+
2299+
2300+
class PostgresNodeUtils:
2301+
@staticmethod
2302+
def delect_port_conflict(log_reader: PostgresNodeLogReader) -> bool:
2303+
assert type(log_reader) == PostgresNodeLogReader # noqa: E721
2304+
2305+
blocks = log_reader.read()
2306+
assert type(blocks) == list # noqa: E721
2307+
2308+
for block in blocks:
2309+
assert type(block) == PostgresNodeLogReader.LogDataBlock # noqa: E721
2310+
2311+
if 'Is another postmaster already running on port' in block.data:
2312+
return True
2313+
2314+
return False
2315+
2316+
21952317
class NodeApp:
21962318

21972319
def __init__(self, test_path=None, nodes_to_cleanup=None, os_ops=None):

tests/test_testgres_common.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from testgres.node import PgVer
77
from testgres.node import PostgresNode
8+
from testgres.node import PostgresNodeLogReader
9+
from testgres.node import PostgresNodeUtils
810
from testgres.utils import get_pg_version2
911
from testgres.utils import file_tail
1012
from testgres.utils import get_bin_path2
@@ -883,8 +885,29 @@ def test_backup_wrong_xlog_method(self, node_svc: PostgresNodeService):
883885

884886
def test_pg_ctl_wait_option(self, node_svc: PostgresNodeService):
885887
assert isinstance(node_svc, PostgresNodeService)
886-
with __class__.helper__get_node(node_svc) as node:
887-
self.impl__test_pg_ctl_wait_option(node_svc, node)
888+
889+
C_MAX_ATTEMPT = 5
890+
891+
nAttempt = 0
892+
893+
while True:
894+
if nAttempt == C_MAX_ATTEMPT:
895+
raise Exception("PostgresSQL did not start.")
896+
897+
nAttempt += 1
898+
logging.info("------------------------ NODE #{}".format(
899+
nAttempt
900+
))
901+
902+
with __class__.helper__get_node(node_svc, port=12345) as node:
903+
if self.impl__test_pg_ctl_wait_option(node_svc, node):
904+
break
905+
continue
906+
907+
logging.info("OK. Test is passed. Number of attempts is {}".format(
908+
nAttempt
909+
))
910+
return
888911

889912
def impl__test_pg_ctl_wait_option(
890913
self,
@@ -899,9 +922,18 @@ def impl__test_pg_ctl_wait_option(
899922

900923
node.init()
901924
assert node.status() == NodeStatus.Stopped
925+
926+
node_log_reader = PostgresNodeLogReader(node, from_beginnig=True)
927+
902928
node.start(wait=False)
903929
nAttempt = 0
904930
while True:
931+
if PostgresNodeUtils.delect_port_conflict(node_log_reader):
932+
logging.info("Node port {} conflicted with another PostgreSQL instance.".format(
933+
node.port
934+
))
935+
return False
936+
905937
if nAttempt == C_MAX_ATTEMPTS:
906938
#
907939
# [2025-03-11]
@@ -960,6 +992,7 @@ def impl__test_pg_ctl_wait_option(
960992
raise Exception("Unexpected node status: {0}.".format(s1))
961993

962994
logging.info("OK. Node is stopped.")
995+
return True
963996

964997
def test_replicate(self, node_svc: PostgresNodeService):
965998
assert isinstance(node_svc, PostgresNodeService)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy