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

Lines changed: 72 additions & 17 deletions
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

Lines changed: 117 additions & 0 deletions
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)
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