10
10
from queue import Queue
11
11
12
12
import time
13
+ import typing
13
14
14
15
try :
15
16
from collections .abc import Iterable
@@ -131,12 +132,47 @@ def __repr__(self):
131
132
repr (self .process ))
132
133
133
134
135
+ class PostgresNodePortManager :
136
+ def reserve_port (self )-> int :
137
+ raise NotImplementedError ("PostManager::reserve_port is not implemented." )
138
+
139
+ def release_port (self ,number :int )-> None :
140
+ assert type (number )== int # noqa: E721
141
+ raise NotImplementedError ("PostManager::release_port is not implemented." )
142
+
143
+
144
+ class PostgresNodePortManager__Global (PostgresNodePortManager ):
145
+ def __init__ (self ):
146
+ pass
147
+
148
+ def reserve_port (self )-> int :
149
+ return utils .reserve_port ()
150
+
151
+ def release_port (self ,number :int )-> None :
152
+ assert type (number )== int # noqa: E721
153
+ return utils .release_port (number )
154
+
155
+
134
156
class PostgresNode (object ):
135
157
# a max number of node start attempts
136
158
_C_MAX_START_ATEMPTS = 5
137
159
138
- def __init__ (self ,name = None ,base_dir = None ,port = None ,conn_params :ConnectionParams = ConnectionParams (),
139
- bin_dir = None ,prefix = None ,os_ops = None ):
160
+ _name :typing .Optional [str ]
161
+ _host :typing .Optional [str ]
162
+ _port :typing .Optional [int ]
163
+ _should_free_port :bool
164
+ _os_ops :OsOperations
165
+ _port_manager :PostgresNodePortManager
166
+
167
+ def __init__ (self ,
168
+ name = None ,
169
+ base_dir = None ,
170
+ port :typing .Optional [int ]= None ,
171
+ conn_params :ConnectionParams = ConnectionParams (),
172
+ bin_dir = None ,
173
+ prefix = None ,
174
+ os_ops :typing .Optional [OsOperations ]= None ,
175
+ port_manager :typing .Optional [PostgresNodePortManager ]= None ):
140
176
"""
141
177
PostgresNode constructor.
142
178
@@ -145,34 +181,57 @@ def __init__(self, name=None, base_dir=None, port=None, conn_params: ConnectionP
145
181
port: port to accept connections.
146
182
base_dir: path to node's data directory.
147
183
bin_dir: path to node's binary directory.
184
+ os_ops: None or correct OS operation object.
185
+ port_manager: None or correct port manager object.
148
186
"""
187
+ assert port is None or type (port )== int # noqa: E721
188
+ assert os_ops is None or isinstance (os_ops ,OsOperations )
189
+ assert port_manager is None or isinstance (port_manager ,PostgresNodePortManager )
149
190
150
191
# private
151
192
if os_ops is None :
152
- os_ops = __class__ ._get_os_ops (conn_params )
193
+ self . _os_ops = __class__ ._get_os_ops (conn_params )
153
194
else :
154
195
assert conn_params is None
196
+ assert isinstance (os_ops ,OsOperations )
197
+ self ._os_ops = os_ops
155
198
pass
156
199
157
- assert os_ops is not None
158
- assert isinstance (os_ops ,OsOperations )
159
- self ._os_ops = os_ops
200
+ assert self ._os_ops is not None
201
+ assert isinstance (self ._os_ops ,OsOperations )
160
202
161
- self ._pg_version = PgVer (get_pg_version2 (os_ops ,bin_dir ))
162
- self ._should_free_port = port is None
203
+ self ._pg_version = PgVer (get_pg_version2 (self ._os_ops ,bin_dir ))
163
204
self ._base_dir = base_dir
164
205
self ._bin_dir = bin_dir
165
206
self ._prefix = prefix
166
207
self ._logger = None
167
208
self ._master = None
168
209
169
210
# basic
170
- self .name = name or generate_app_name ()
211
+ self ._name = name or generate_app_name ()
212
+ self ._host = self ._os_ops .host
171
213
172
- self .host = os_ops .host
173
- self .port = port or utils .reserve_port ()
214
+ if port is not None :
215
+ assert type (port )== int # noqa: E721
216
+ assert port_manager is None
217
+ self ._port = port
218
+ self ._should_free_port = False
219
+ self ._port_manager = None
220
+ else :
221
+ if port_manager is not None :
222
+ assert isinstance (port_manager ,PostgresNodePortManager )
223
+ self ._port_manager = port_manager
224
+ else :
225
+ self ._port_manager = __class__ ._get_port_manager (self ._os_ops )
226
+
227
+ assert self ._port_manager is not None
228
+ assert isinstance (self ._port_manager ,PostgresNodePortManager )
229
+
230
+ self ._port = self ._port_manager .reserve_port ()# raises
231
+ assert type (self ._port )== int # noqa: E721
232
+ self ._should_free_port = True
174
233
175
- self .ssh_key = os_ops . ssh_key
234
+ assert type ( self ._port ) == int # noqa: E721
176
235
177
236
# defaults for __exit__()
178
237
self .cleanup_on_good_exit = testgres_config .node_cleanup_on_good_exit
@@ -207,7 +266,11 @@ def __exit__(self, type, value, traceback):
207
266
208
267
def __repr__ (self ):
209
268
return "{}(name='{}', port={}, base_dir='{}')" .format (
210
- self .__class__ .__name__ ,self .name ,self .port ,self .base_dir )
269
+ self .__class__ .__name__ ,
270
+ self .name ,
271
+ str (self ._port )if self ._port is not None else "None" ,
272
+ self .base_dir
273
+ )
211
274
212
275
@staticmethod
213
276
def _get_os_ops (conn_params :ConnectionParams )-> OsOperations :
@@ -221,12 +284,28 @@ def _get_os_ops(conn_params: ConnectionParams) -> OsOperations:
221
284
222
285
return LocalOperations (conn_params )
223
286
287
+ @staticmethod
288
+ def _get_port_manager (os_ops :OsOperations )-> PostgresNodePortManager :
289
+ assert os_ops is not None
290
+ assert isinstance (os_ops ,OsOperations )
291
+
292
+ # [2025-04-03] It is our old, incorrected behaviour
293
+ return PostgresNodePortManager__Global ()
294
+
224
295
def clone_with_new_name_and_base_dir (self ,name :str ,base_dir :str ):
225
296
assert name is None or type (name )== str # noqa: E721
226
297
assert base_dir is None or type (base_dir )== str # noqa: E721
227
298
228
299
assert __class__ == PostgresNode
229
300
301
+ if self ._port_manager is None :
302
+ raise InvalidOperationException ("PostgresNode without PortManager can't be cloned." )
303
+
304
+ assert self ._port_manager is not None
305
+ assert isinstance (self ._port_manager ,PostgresNodePortManager )
306
+ assert self ._os_ops is not None
307
+ assert isinstance (self ._os_ops ,OsOperations )
308
+
230
309
node = PostgresNode (
231
310
name = name ,
232
311
base_dir = base_dir ,
@@ -243,6 +322,34 @@ def os_ops(self) -> OsOperations:
243
322
assert isinstance (self ._os_ops ,OsOperations )
244
323
return self ._os_ops
245
324
325
+ @property
326
+ def name (self )-> str :
327
+ if self ._name is None :
328
+ raise InvalidOperationException ("PostgresNode name is not defined." )
329
+ assert type (self ._name )== str # noqa: E721
330
+ return self ._name
331
+
332
+ @property
333
+ def host (self )-> str :
334
+ if self ._host is None :
335
+ raise InvalidOperationException ("PostgresNode host is not defined." )
336
+ assert type (self ._host )== str # noqa: E721
337
+ return self ._host
338
+
339
+ @property
340
+ def port (self )-> int :
341
+ if self ._port is None :
342
+ raise InvalidOperationException ("PostgresNode port is not defined." )
343
+
344
+ assert type (self ._port )== int # noqa: E721
345
+ return self ._port
346
+
347
+ @property
348
+ def ssh_key (self )-> typing .Optional [str ]:
349
+ assert self ._os_ops is not None
350
+ assert isinstance (self ._os_ops ,OsOperations )
351
+ return self ._os_ops .ssh_key
352
+
246
353
@property
247
354
def pid (self ):
248
355
"""
@@ -993,6 +1100,11 @@ def start(self, params=[], wait=True):
993
1100
if self .is_started :
994
1101
return self
995
1102
1103
+ if self ._port is None :
1104
+ raise InvalidOperationException ("Can't start PostgresNode. Port is node defined." )
1105
+
1106
+ assert type (self ._port )== int # noqa: E721
1107
+
996
1108
_params = [self ._get_bin_path ("pg_ctl" ),
997
1109
"-D" ,self .data_dir ,
998
1110
"-l" ,self .pg_log_file ,
@@ -1023,6 +1135,8 @@ def LOCAL__raise_cannot_start_node__std(from_exception):
1023
1135
LOCAL__raise_cannot_start_node__std (e )
1024
1136
else :
1025
1137
assert self ._should_free_port
1138
+ assert self ._port_manager is not None
1139
+ assert isinstance (self ._port_manager ,PostgresNodePortManager )
1026
1140
assert __class__ ._C_MAX_START_ATEMPTS > 1
1027
1141
1028
1142
log_files0 = self ._collect_log_files ()
@@ -1048,20 +1162,20 @@ def LOCAL__raise_cannot_start_node__std(from_exception):
1048
1162
1049
1163
log_files0 = log_files1
1050
1164
logging .warning (
1051
- "Detected a conflict with using the port {0}. Trying another port after a {1}-second sleep..." .format (self .port ,timeout )
1165
+ "Detected a conflict with using the port {0}. Trying another port after a {1}-second sleep..." .format (self ._port ,timeout )
1052
1166
)
1053
1167
time .sleep (timeout )
1054
1168
timeout = min (2 * timeout ,5 )
1055
- cur_port = self .port
1056
- new_port = utils .reserve_port ()# can raise
1169
+ cur_port = self ._port
1170
+ new_port = self . _port_manager .reserve_port ()# can raise
1057
1171
try :
1058
1172
options = {'port' :new_port }
1059
1173
self .set_auto_conf (options )
1060
1174
except :# noqa: E722
1061
- utils .release_port (new_port )
1175
+ self . _port_manager .release_port (new_port )
1062
1176
raise
1063
- self .port = new_port
1064
- utils .release_port (cur_port )
1177
+ self ._port = new_port
1178
+ self . _port_manager .release_port (cur_port )
1065
1179
continue
1066
1180
break
1067
1181
self ._maybe_start_logger ()
@@ -1226,10 +1340,15 @@ def free_port(self):
1226
1340
"""
1227
1341
1228
1342
if self ._should_free_port :
1229
- port = self .port
1343
+ assert type (self ._port )== int # noqa: E721
1344
+
1345
+ assert self ._port_manager is not None
1346
+ assert isinstance (self ._port_manager ,PostgresNodePortManager )
1347
+
1348
+ port = self ._port
1230
1349
self ._should_free_port = False
1231
- self .port = None
1232
- utils .release_port (port )
1350
+ self ._port = None
1351
+ self . _port_manager .release_port (port )
1233
1352
1234
1353
def cleanup (self ,max_attempts = 3 ,full = False ):
1235
1354
"""