Skip to content

Commit 28ac425

Browse files
Port numbers management is improved (#164)
- We don't release a port number that was defined by client - We only check log files to detect port number conflicts - We use slightly smarter log file checking A test is added.
1 parent 4fe1894 commit 28ac425

File tree

2 files changed

+189
-17
lines changed

2 files changed

+189
-17
lines changed

testgres/node.py

+72-17
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@
8383

8484
from .standby import First
8585

86+
from . import utils
87+
8688
from .utils import \
8789
PgVer, \
8890
eprint, \
8991
get_bin_path, \
9092
get_pg_version, \
91-
reserve_port, \
92-
release_port, \
9393
execute_utility, \
9494
options_string, \
9595
clean_on_error
@@ -158,7 +158,7 @@ def __init__(self, name=None, base_dir=None, port=None, conn_params: ConnectionP
158158
self.os_ops = LocalOperations(conn_params)
159159

160160
self.host = self.os_ops.host
161-
self.port = port or reserve_port()
161+
self.port = port or utils.reserve_port()
162162

163163
self.ssh_key = self.os_ops.ssh_key
164164

@@ -471,6 +471,28 @@ def _collect_special_files(self):
471471

472472
return result
473473

474+
def _collect_log_files(self):
475+
# dictionary of log files + size in bytes
476+
477+
files = [
478+
self.pg_log_file
479+
] # yapf: disable
480+
481+
result = {}
482+
483+
for f in files:
484+
# skip missing files
485+
if not self.os_ops.path_exists(f):
486+
continue
487+
488+
file_size = self.os_ops.get_file_size(f)
489+
assert type(file_size) == int # noqa: E721
490+
assert file_size >= 0
491+
492+
result[f] = file_size
493+
494+
return result
495+
474496
def init(self, initdb_params=None, cached=True, **kwargs):
475497
"""
476498
Perform initdb for this node.
@@ -722,6 +744,22 @@ def slow_start(self, replica=False, dbname='template1', username=None, max_attem
722744
OperationalError},
723745
max_attempts=max_attempts)
724746

747+
def _detect_port_conflict(self, log_files0, log_files1):
748+
assert type(log_files0) == dict # noqa: E721
749+
assert type(log_files1) == dict # noqa: E721
750+
751+
for file in log_files1.keys():
752+
read_pos = 0
753+
754+
if file in log_files0.keys():
755+
read_pos = log_files0[file] # the previous size
756+
757+
file_content = self.os_ops.read_binary(file, read_pos)
758+
file_content_s = file_content.decode()
759+
if 'Is another postmaster already running on port' in file_content_s:
760+
return True
761+
return False
762+
725763
def start(self, params=[], wait=True):
726764
"""
727765
Starts the PostgreSQL node using pg_ctl if node has not been started.
@@ -745,27 +783,42 @@ def start(self, params=[], wait=True):
745783
"-w" if wait else '-W', # --wait or --no-wait
746784
"start"] + params # yapf: disable
747785

748-
startup_retries = 5
786+
log_files0 = self._collect_log_files()
787+
assert type(log_files0) == dict # noqa: E721
788+
789+
nAttempt = 0
790+
timeout = 1
749791
while True:
792+
nAttempt += 1
750793
try:
751794
exit_status, out, error = execute_utility(_params, self.utils_log_file, verbose=True)
752795
if error and 'does not exist' in error:
753796
raise Exception
754797
except Exception as e:
755-
files = self._collect_special_files()
756-
if any(len(file) > 1 and 'Is another postmaster already '
757-
'running on port' in file[1].decode() for
758-
file in files):
759-
logging.warning("Detected an issue with connecting to port {0}. "
760-
"Trying another port after a 5-second sleep...".format(self.port))
761-
self.port = reserve_port()
762-
options = {'port': str(self.port)}
763-
self.set_auto_conf(options)
764-
startup_retries -= 1
765-
time.sleep(5)
766-
continue
798+
if self._should_free_port and nAttempt < 5:
799+
log_files1 = self._collect_log_files()
800+
if self._detect_port_conflict(log_files0, log_files1):
801+
log_files0 = log_files1
802+
logging.warning(
803+
"Detected an issue with connecting to port {0}. "
804+
"Trying another port after a {1}-second sleep...".format(self.port, timeout)
805+
)
806+
time.sleep(timeout)
807+
timeout = min(2 * timeout, 5)
808+
cur_port = self.port
809+
new_port = utils.reserve_port() # throw
810+
try:
811+
options = {'port': str(new_port)}
812+
self.set_auto_conf(options)
813+
except: # noqa: E722
814+
utils.release_port(new_port)
815+
raise
816+
self.port = new_port
817+
utils.release_port(cur_port)
818+
continue
767819

768820
msg = 'Cannot start node'
821+
files = self._collect_special_files()
769822
raise_from(StartNodeException(msg, files), e)
770823
break
771824
self._maybe_start_logger()
@@ -930,8 +983,10 @@ def free_port(self):
930983
"""
931984

932985
if self._should_free_port:
986+
port = self.port
933987
self._should_free_port = False
934-
release_port(self.port)
988+
self.port = None
989+
utils.release_port(port)
935990

936991
def cleanup(self, max_attempts=3, full=False):
937992
"""

tests/test_simple.py

+117
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,123 @@ def test_the_same_port(self):
10641064

10651065
self.assertIn("Cannot start node", str(ctx.exception))
10661066

1067+
class tagPortManagerProxy:
1068+
sm_prev_testgres_reserve_port = None
1069+
sm_prev_testgres_release_port = None
1070+
1071+
sm_DummyPortNumber = None
1072+
sm_DummyPortMaxUsage = None
1073+
1074+
sm_DummyPortCurrentUsage = None
1075+
sm_DummyPortTotalUsage = None
1076+
1077+
def __init__(self, dummyPortNumber, dummyPortMaxUsage):
1078+
assert type(dummyPortNumber) == int # noqa: E721
1079+
assert type(dummyPortMaxUsage) == int # noqa: E721
1080+
assert dummyPortNumber >= 0
1081+
assert dummyPortMaxUsage >= 0
1082+
1083+
assert __class__.sm_prev_testgres_reserve_port is None
1084+
assert __class__.sm_prev_testgres_release_port is None
1085+
assert testgres.utils.reserve_port == testgres.utils.internal__reserve_port
1086+
assert testgres.utils.release_port == testgres.utils.internal__release_port
1087+
1088+
__class__.sm_prev_testgres_reserve_port = testgres.utils.reserve_port
1089+
__class__.sm_prev_testgres_release_port = testgres.utils.release_port
1090+
1091+
testgres.utils.reserve_port = __class__._proxy__reserve_port
1092+
testgres.utils.release_port = __class__._proxy__release_port
1093+
1094+
assert testgres.utils.reserve_port == __class__._proxy__reserve_port
1095+
assert testgres.utils.release_port == __class__._proxy__release_port
1096+
1097+
__class__.sm_DummyPortNumber = dummyPortNumber
1098+
__class__.sm_DummyPortMaxUsage = dummyPortMaxUsage
1099+
1100+
__class__.sm_DummyPortCurrentUsage = 0
1101+
__class__.sm_DummyPortTotalUsage = 0
1102+
1103+
def __enter__(self):
1104+
return self
1105+
1106+
def __exit__(self, type, value, traceback):
1107+
assert __class__.sm_DummyPortCurrentUsage == 0
1108+
1109+
assert __class__.sm_prev_testgres_reserve_port is not None
1110+
assert __class__.sm_prev_testgres_release_port is not None
1111+
1112+
assert testgres.utils.reserve_port == __class__._proxy__reserve_port
1113+
assert testgres.utils.release_port == __class__._proxy__release_port
1114+
1115+
testgres.utils.reserve_port = __class__.sm_prev_testgres_reserve_port
1116+
testgres.utils.release_port = __class__.sm_prev_testgres_release_port
1117+
1118+
__class__.sm_prev_testgres_reserve_port = None
1119+
__class__.sm_prev_testgres_release_port = None
1120+
1121+
def _proxy__reserve_port():
1122+
assert type(__class__.sm_DummyPortMaxUsage) == int # noqa: E721
1123+
assert type(__class__.sm_DummyPortTotalUsage) == int # noqa: E721
1124+
assert type(__class__.sm_DummyPortCurrentUsage) == int # noqa: E721
1125+
assert __class__.sm_DummyPortTotalUsage >= 0
1126+
assert __class__.sm_DummyPortCurrentUsage >= 0
1127+
1128+
assert __class__.sm_DummyPortTotalUsage <= __class__.sm_DummyPortMaxUsage
1129+
assert __class__.sm_DummyPortCurrentUsage <= __class__.sm_DummyPortTotalUsage
1130+
1131+
assert __class__.sm_prev_testgres_reserve_port is not None
1132+
1133+
if __class__.sm_DummyPortTotalUsage == __class__.sm_DummyPortMaxUsage:
1134+
return __class__.sm_prev_testgres_reserve_port()
1135+
1136+
__class__.sm_DummyPortTotalUsage += 1
1137+
__class__.sm_DummyPortCurrentUsage += 1
1138+
return __class__.sm_DummyPortNumber
1139+
1140+
def _proxy__release_port(dummyPortNumber):
1141+
assert type(dummyPortNumber) == int # noqa: E721
1142+
1143+
assert type(__class__.sm_DummyPortMaxUsage) == int # noqa: E721
1144+
assert type(__class__.sm_DummyPortTotalUsage) == int # noqa: E721
1145+
assert type(__class__.sm_DummyPortCurrentUsage) == int # noqa: E721
1146+
assert __class__.sm_DummyPortTotalUsage >= 0
1147+
assert __class__.sm_DummyPortCurrentUsage >= 0
1148+
1149+
assert __class__.sm_DummyPortTotalUsage <= __class__.sm_DummyPortMaxUsage
1150+
assert __class__.sm_DummyPortCurrentUsage <= __class__.sm_DummyPortTotalUsage
1151+
1152+
assert __class__.sm_prev_testgres_release_port is not None
1153+
1154+
if __class__.sm_DummyPortCurrentUsage > 0 and dummyPortNumber == __class__.sm_DummyPortNumber:
1155+
assert __class__.sm_DummyPortTotalUsage > 0
1156+
__class__.sm_DummyPortCurrentUsage -= 1
1157+
return
1158+
1159+
return __class__.sm_prev_testgres_release_port(dummyPortNumber)
1160+
1161+
def test_port_rereserve_during_node_start(self):
1162+
C_COUNT_OF_BAD_PORT_USAGE = 3
1163+
1164+
with get_new_node() as node1:
1165+
node1.init().start()
1166+
self.assertTrue(node1._should_free_port)
1167+
self.assertEqual(type(node1.port), int) # noqa: E721
1168+
node1.safe_psql("SELECT 1;")
1169+
1170+
with __class__.tagPortManagerProxy(node1.port, C_COUNT_OF_BAD_PORT_USAGE):
1171+
assert __class__.tagPortManagerProxy.sm_DummyPortNumber == node1.port
1172+
with get_new_node() as node2:
1173+
self.assertTrue(node2._should_free_port)
1174+
self.assertEqual(node2.port, node1.port)
1175+
1176+
node2.init().start()
1177+
1178+
self.assertNotEqual(node2.port, node1.port)
1179+
self.assertEqual(__class__.tagPortManagerProxy.sm_DummyPortCurrentUsage, 0)
1180+
self.assertEqual(__class__.tagPortManagerProxy.sm_DummyPortTotalUsage, C_COUNT_OF_BAD_PORT_USAGE)
1181+
1182+
node2.safe_psql("SELECT 1;")
1183+
10671184
def test_simple_with_bin_dir(self):
10681185
with get_new_node() as node:
10691186
node.init().start()

0 commit comments

Comments
 (0)