testgres 1.14.5__tar.gz → 1.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {testgres-1.14.5/testgres.egg-info → testgres-1.15.0}/PKG-INFO +2 -2
  2. {testgres-1.14.5 → testgres-1.15.0}/pyproject.toml +1 -1
  3. {testgres-1.14.5 → testgres-1.15.0}/src/__init__.py +2 -3
  4. {testgres-1.14.5 → testgres-1.15.0}/src/consts.py +4 -0
  5. {testgres-1.14.5 → testgres-1.15.0}/src/enums.py +1 -1
  6. {testgres-1.14.5 → testgres-1.15.0}/src/impl/platforms/internal_platform_utils.py +9 -0
  7. testgres-1.15.0/src/impl/platforms/linux/internal_platform_utils.py +429 -0
  8. {testgres-1.14.5 → testgres-1.15.0}/src/impl/platforms/win32/internal_platform_utils.py +10 -0
  9. testgres-1.15.0/src/impl/port_manager__generic2.py +166 -0
  10. {testgres-1.14.5 → testgres-1.15.0}/src/impl/port_manager__this_host.py +2 -1
  11. {testgres-1.14.5 → testgres-1.15.0}/src/node.py +66 -8
  12. {testgres-1.14.5 → testgres-1.15.0}/src/raise_error.py +37 -5
  13. {testgres-1.14.5 → testgres-1.15.0}/src/utils.py +28 -14
  14. {testgres-1.14.5 → testgres-1.15.0/testgres.egg-info}/PKG-INFO +2 -2
  15. {testgres-1.14.5 → testgres-1.15.0}/testgres.egg-info/SOURCES.txt +1 -0
  16. {testgres-1.14.5 → testgres-1.15.0}/testgres.egg-info/requires.txt +1 -1
  17. testgres-1.15.0/tests/test_os_ops_common.py +3906 -0
  18. {testgres-1.14.5 → testgres-1.15.0}/tests/test_os_ops_local.py +42 -6
  19. {testgres-1.14.5 → testgres-1.15.0}/tests/test_os_ops_remote.py +57 -13
  20. {testgres-1.14.5 → testgres-1.15.0}/tests/test_testgres_common.py +100 -18
  21. {testgres-1.14.5 → testgres-1.15.0}/tests/test_testgres_local.py +0 -35
  22. {testgres-1.14.5 → testgres-1.15.0}/tests/test_testgres_remote.py +31 -9
  23. testgres-1.14.5/src/impl/platforms/linux/internal_platform_utils.py +0 -120
  24. testgres-1.14.5/tests/test_os_ops_common.py +0 -1400
  25. {testgres-1.14.5 → testgres-1.15.0}/LICENSE +0 -0
  26. {testgres-1.14.5 → testgres-1.15.0}/README.md +0 -0
  27. {testgres-1.14.5 → testgres-1.15.0}/setup.cfg +0 -0
  28. {testgres-1.14.5 → testgres-1.15.0}/src/api.py +0 -0
  29. {testgres-1.14.5 → testgres-1.15.0}/src/backup.py +0 -0
  30. {testgres-1.14.5 → testgres-1.15.0}/src/cache.py +0 -0
  31. {testgres-1.14.5 → testgres-1.15.0}/src/config.py +0 -0
  32. {testgres-1.14.5 → testgres-1.15.0}/src/connection.py +0 -0
  33. {testgres-1.14.5 → testgres-1.15.0}/src/decorators.py +0 -0
  34. {testgres-1.14.5 → testgres-1.15.0}/src/defaults.py +0 -0
  35. {testgres-1.14.5 → testgres-1.15.0}/src/exceptions.py +0 -0
  36. {testgres-1.14.5 → testgres-1.15.0}/src/impl/file_line_reader.py +0 -0
  37. {testgres-1.14.5 → testgres-1.15.0}/src/impl/internal_utils.py +0 -0
  38. {testgres-1.14.5 → testgres-1.15.0}/src/impl/platforms/internal_platform_utils_factory.py +0 -0
  39. {testgres-1.14.5 → testgres-1.15.0}/src/impl/port_manager__generic.py +0 -0
  40. {testgres-1.14.5 → testgres-1.15.0}/src/logger.py +0 -0
  41. {testgres-1.14.5 → testgres-1.15.0}/src/node_app.py +0 -0
  42. {testgres-1.14.5 → testgres-1.15.0}/src/port_manager.py +0 -0
  43. {testgres-1.14.5 → testgres-1.15.0}/src/pubsub.py +0 -0
  44. {testgres-1.14.5 → testgres-1.15.0}/src/standby.py +0 -0
  45. {testgres-1.14.5 → testgres-1.15.0}/testgres.egg-info/dependency_links.txt +0 -0
  46. {testgres-1.14.5 → testgres-1.15.0}/testgres.egg-info/top_level.txt +0 -0
  47. {testgres-1.14.5 → testgres-1.15.0}/tests/test_api.py +0 -0
  48. {testgres-1.14.5 → testgres-1.15.0}/tests/test_config.py +0 -0
  49. {testgres-1.14.5 → testgres-1.15.0}/tests/test_raise_error.py +0 -0
  50. {testgres-1.14.5 → testgres-1.15.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testgres
3
- Version: 1.14.5
3
+ Version: 1.15.0
4
4
  Summary: Testing utility for PostgreSQL and its extensions
5
5
  Author-email: Postgres Professional <testgres@postgrespro.ru>
6
6
  License: PostgreSQL
@@ -27,7 +27,7 @@ Requires-Dist: port-for>=0.4
27
27
  Requires-Dist: six>=1.9.0
28
28
  Requires-Dist: psutil
29
29
  Requires-Dist: packaging
30
- Requires-Dist: testgres.os_ops<3.0.0,>=2.4.0
30
+ Requires-Dist: testgres.os_ops<4.0.0,>=3.1.0
31
31
  Dynamic: license-file
32
32
 
33
33
  [![CI Status](https://img.shields.io/github/actions/workflow/status/postgrespro/testgres/.github/workflows/ci.yml?label=CI)](https://github.com/postgrespro/testgres/actions/workflows/ci.yml)
@@ -65,7 +65,7 @@ dependencies = [
65
65
  "six>=1.9.0",
66
66
  "psutil",
67
67
  "packaging",
68
- "testgres.os_ops>=2.4.0,<3.0.0",
68
+ "testgres.os_ops>=3.1.0,<4.0.0",
69
69
  ]
70
70
 
71
71
  [project.urls]
@@ -41,7 +41,6 @@ from .node_app import NodeApp
41
41
  from .utils import \
42
42
  reserve_port, \
43
43
  release_port, \
44
- bound_ports, \
45
44
  get_bin_path, \
46
45
  get_bin_dir, \
47
46
  get_pg_config, \
@@ -58,7 +57,7 @@ from testgres.operations.os_ops import OsOperations, ConnectionParams
58
57
  from testgres.operations.local_ops import LocalOperations
59
58
  from testgres.operations.remote_ops import RemoteOperations
60
59
 
61
- __version__ = "1.14.5"
60
+ __version__ = "1.15.0"
62
61
 
63
62
  __all__ = [
64
63
  "get_new_node",
@@ -73,7 +72,7 @@ __all__ = [
73
72
  "NodeApp",
74
73
  "PostgresNode",
75
74
  "PortManager",
76
- "reserve_port", "release_port", "bound_ports", "get_bin_path", "get_bin_dir", "get_pg_config", "get_pg_version", "parse_pg_version",
75
+ "reserve_port", "release_port", "get_bin_path", "get_bin_dir", "get_pg_config", "get_pg_version", "parse_pg_version",
77
76
  "First", "Any",
78
77
  "OsOperations", "LocalOperations", "RemoteOperations", "ConnectionParams"
79
78
  ]
@@ -10,6 +10,10 @@ TMP_DUMP = 'tgsd_'
10
10
  TMP_CACHE = 'tgsc_'
11
11
  TMP_BACKUP = 'tgsb_'
12
12
 
13
+ TMP_TESTGRES = "testgres"
14
+
15
+ TMP_TESTGRES_PORTS = TMP_TESTGRES + "/ports"
16
+
13
17
  # path to control file
14
18
  XLOG_CONTROL_FILE = "global/pg_control"
15
19
 
@@ -29,7 +29,7 @@ class NodeStatus(IntEnum):
29
29
  Status of a PostgresNode
30
30
  """
31
31
 
32
- Running, Stopped, Uninitialized = range(3)
32
+ Running, Stopped, Uninitialized, Zombie = range(4)
33
33
 
34
34
  # for Python 3.x
35
35
  def __bool__(self):
@@ -60,3 +60,12 @@ class InternalPlatformUtils:
60
60
  assert type(bin_dir) is str
61
61
  assert type(data_dir) is str
62
62
  raise NotImplementedError("InternalPlatformUtils::FindPostmaster is not implemented.")
63
+
64
+ def ProcessIsZombi_soft_check(
65
+ self,
66
+ os_ops: OsOperations,
67
+ pid: int,
68
+ ) -> typing.Optional[bool]:
69
+ assert isinstance(os_ops, OsOperations)
70
+ assert type(pid) is int
71
+ raise NotImplementedError("InternalPlatformUtils::ProcessIsZombi_soft_ver is not implemented.")
@@ -0,0 +1,429 @@
1
+ from __future__ import annotations
2
+
3
+ from .. import internal_platform_utils as base
4
+ from ... import internal_utils
5
+ from ....raise_error import RaiseError
6
+
7
+ from testgres.operations.os_ops import OsOperations
8
+ from testgres.operations.exceptions import ExecUtilException
9
+
10
+ import re
11
+ import shlex
12
+ import typing
13
+ import time
14
+
15
+
16
+ class InternalPlatformUtils(base.InternalPlatformUtils):
17
+ C_MAX_FIND_POSTMASTER_ATTEMPTS = 5
18
+ C_BASH_EXE = "/bin/bash"
19
+
20
+ sm_exec_env = {
21
+ "LANG": "en_US.UTF-8",
22
+ "LC_ALL": "en_US.UTF-8",
23
+ }
24
+
25
+ # --------------------------------------------------------------------
26
+ def FindPostmaster(
27
+ self,
28
+ os_ops: OsOperations,
29
+ bin_dir: str,
30
+ data_dir: str
31
+ ) -> InternalPlatformUtils.FindPostmasterResult:
32
+ assert isinstance(os_ops, OsOperations)
33
+ assert type(bin_dir) is str
34
+ assert type(data_dir) is str
35
+ assert type(__class__.C_BASH_EXE) is str
36
+ assert type(__class__.sm_exec_env) is dict
37
+ assert len(__class__.C_BASH_EXE) > 0
38
+ assert len(bin_dir) > 0
39
+ assert len(data_dir) > 0
40
+
41
+ failures: typing.List[Exception] = []
42
+
43
+ postmaster_pid: typing.Optional[int] = None
44
+
45
+ nAttempts = 0
46
+
47
+ while True:
48
+ nAttempts += 1
49
+
50
+ try:
51
+ postmaster_pid = __class__._FindPostmaster(
52
+ os_ops,
53
+ bin_dir,
54
+ data_dir,
55
+ )
56
+ except Exception as e:
57
+ failures.append(e)
58
+
59
+ log_msg = "FindPostmaster (bin_dir={!r}, data_dir={!r}) detects a problem. Exception {}:\n{}".format(
60
+ bin_dir,
61
+ data_dir,
62
+ type(e).__name__,
63
+ e,
64
+ )
65
+ internal_utils.send_log_debug(log_msg)
66
+
67
+ if nAttempts < __class__.C_MAX_FIND_POSTMASTER_ATTEMPTS:
68
+ time.sleep(0.05)
69
+ continue
70
+
71
+ __class__._find_postmaster__throw_error__fail(
72
+ bin_dir=bin_dir,
73
+ data_dir=data_dir,
74
+ failures=failures,
75
+ )
76
+
77
+ break
78
+
79
+ if postmaster_pid is None:
80
+ return InternalPlatformUtils.FindPostmasterResult.create_not_found()
81
+
82
+ assert type(postmaster_pid) is int
83
+
84
+ return InternalPlatformUtils.FindPostmasterResult.create_ok(postmaster_pid)
85
+
86
+ # --------------------------------------------------------------------
87
+ @staticmethod
88
+ def _FindPostmaster(
89
+ os_ops: OsOperations,
90
+ bin_dir: str,
91
+ data_dir: str
92
+ ) -> typing.Optional[int]:
93
+ assert isinstance(os_ops, OsOperations)
94
+ assert type(bin_dir) is str
95
+ assert type(data_dir) is str
96
+ assert type(__class__.C_BASH_EXE) is str
97
+ assert type(__class__.sm_exec_env) is dict
98
+ assert len(__class__.C_BASH_EXE) > 0
99
+ assert len(bin_dir) > 0
100
+ assert len(data_dir) > 0
101
+
102
+ pg_path_e = re.escape(os_ops.build_path(bin_dir, "postgres"))
103
+ data_dir_e = re.escape(data_dir)
104
+
105
+ assert type(pg_path_e) is str
106
+ assert type(data_dir_e) is str
107
+
108
+ regexp = r"^\s*[0-9]+\s+[0-9]+\s+" + pg_path_e + r"(\s+.*)?\s+\-[D]\s+" + data_dir_e + r"(\s+.*)?"
109
+
110
+ cmd = [
111
+ __class__.C_BASH_EXE,
112
+ "-c",
113
+ "ps -ewwo \"pid=,ppid=,args=\" | grep -E " + shlex.quote(regexp),
114
+ ]
115
+
116
+ exec_r = os_ops.exec_command(
117
+ cmd=cmd,
118
+ ignore_errors=True,
119
+ verbose=True,
120
+ exec_env=__class__.sm_exec_env,
121
+ )
122
+
123
+ assert type(exec_r) is tuple
124
+ assert len(exec_r) == 3
125
+
126
+ exit_status, output_b, error_b = exec_r
127
+
128
+ assert type(exit_status) is int
129
+ assert type(output_b) is bytes
130
+ assert type(error_b) is bytes
131
+
132
+ if exit_status == 1:
133
+ return None
134
+
135
+ output = output_b.decode("utf-8")
136
+ error = error_b.decode("utf-8")
137
+
138
+ assert type(output) is str
139
+ assert type(error) is str
140
+
141
+ if exit_status != 0:
142
+ errMsg = f"test command returned an unexpected exit code: {exit_status}"
143
+ raise ExecUtilException(
144
+ message=errMsg,
145
+ command=cmd,
146
+ exit_code=exit_status,
147
+ out=output,
148
+ error=error,
149
+ )
150
+
151
+ lines = output.splitlines()
152
+ assert type(lines) is list
153
+
154
+ if len(lines) == 0:
155
+ # ACHTUNG!
156
+ raise RuntimeError("Command returns 0 error code without output.")
157
+
158
+ # parse result lines
159
+ pid_to_ppid: __class__.T_PID_TO_PPID = {}
160
+
161
+ for i_line in range(len(lines)):
162
+ assert type(lines[i_line]) is str
163
+
164
+ parts = lines[i_line].split()
165
+ assert type(parts) is list
166
+
167
+ if len(parts) < 2:
168
+ __class__._find_postmaster__throw_error__bad_line_format(
169
+ lines,
170
+ i_line,
171
+ "no usefull data",
172
+ )
173
+
174
+ if not parts[0].isdigit():
175
+ __class__._find_postmaster__throw_error__bad_line_format(
176
+ lines,
177
+ i_line,
178
+ "bad pid",
179
+ )
180
+
181
+ if not parts[1].isdigit():
182
+ __class__._find_postmaster__throw_error__bad_line_format(
183
+ lines,
184
+ i_line,
185
+ "bad ppid",
186
+ )
187
+
188
+ pid = int(parts[0])
189
+ ppid = int(parts[1])
190
+
191
+ if pid not in pid_to_ppid:
192
+ pid_to_ppid[pid] = ppid
193
+ continue
194
+
195
+ other_ppid = pid_to_ppid[pid]
196
+ assert type(other_ppid) is int
197
+
198
+ if ppid == other_ppid:
199
+ log_msg = "FindPostmaster (data_dir={!r}) get pid ({}) with ppid ({}) more than one time.".format(
200
+ data_dir,
201
+ pid,
202
+ ppid,
203
+ )
204
+ internal_utils.send_log_debug(log_msg)
205
+ continue
206
+
207
+ # ACTUNG ppid is changed --> restart
208
+ __class__._find_postmaster__throw_error__ppid_is_changed(
209
+ pid,
210
+ ppid,
211
+ other_ppid,
212
+ lines,
213
+ )
214
+
215
+ assert len(pid_to_ppid) <= len(lines)
216
+
217
+ true_postmasters = [
218
+ pid for pid, ppid in pid_to_ppid.items()
219
+ if ppid not in pid_to_ppid
220
+ ]
221
+
222
+ if len(true_postmasters) == 0:
223
+ __class__._find_postmaster__throw_error__cycle(
224
+ pid_to_ppid,
225
+ )
226
+
227
+ if len(true_postmasters) == 1:
228
+ true_pid = true_postmasters[0]
229
+
230
+ if len(pid_to_ppid) > 1:
231
+ msg = "Many processes like a postmaster for data dir [{}] are found ({}).".format(
232
+ data_dir,
233
+ len(true_postmasters),
234
+ )
235
+
236
+ msg += " List (ppid->pid): {}.".format(
237
+ __class__._make_text_from_pid_to_ppid(pid_to_ppid),
238
+ )
239
+
240
+ msg += " True postmaster PID is {}.".format(true_pid)
241
+ internal_utils.send_log_debug(msg)
242
+
243
+ return true_pid
244
+
245
+ assert len(true_postmasters) > 1
246
+
247
+ __class__._find_postmaster__throw_error__many_postmasters(
248
+ true_postmasters,
249
+ pid_to_ppid,
250
+ )
251
+
252
+ def ProcessIsZombi_soft_check(
253
+ self,
254
+ os_ops: OsOperations,
255
+ pid: int,
256
+ ) -> typing.Optional[bool]:
257
+ assert isinstance(os_ops, OsOperations)
258
+ assert type(pid) is int
259
+
260
+ proc_stat_file = os_ops.build_path("/proc", str(pid), "stat")
261
+
262
+ if not os_ops.path_exists(proc_stat_file):
263
+ return False
264
+
265
+ result: typing.Optional[bool] = None
266
+
267
+ try:
268
+ # Read one line from /proc/PID/stat
269
+ stat_content = os_ops.read_binary(proc_stat_file, 0).decode("utf-8", errors="ignore")
270
+
271
+ # We look for the closing parenthesis of the process name to ensure that
272
+ # we start from it and not depend on spaces inside the parentheses!
273
+ r_paren_idx = stat_content.rfind(")")
274
+
275
+ if r_paren_idx == -1:
276
+ pass
277
+ elif len(stat_content) <= r_paren_idx + 2:
278
+ pass
279
+ else:
280
+ # The status goes exactly one space after the closing bracket
281
+ assert (r_paren_idx + 2) < len(stat_content)
282
+ proc_status = stat_content[r_paren_idx + 2]
283
+ result = proc_status == "Z"
284
+ except Exception as e:
285
+ # If the file disappeared right during reading, it means the process is completely erased
286
+ if __class__._is_file_not_found_exception(e):
287
+ result = False
288
+
289
+ return result
290
+
291
+ @staticmethod
292
+ def _is_file_not_found_exception(e: Exception) -> bool:
293
+ if isinstance(e, FileNotFoundError):
294
+ return True
295
+
296
+ if isinstance(e, ExecUtilException):
297
+ if e.exit_code == 2:
298
+ return True
299
+
300
+ return False
301
+
302
+ T_PID_TO_PPID = typing.Dict[int, int]
303
+
304
+ @staticmethod
305
+ def _make_text_from_pid_to_ppid(pid_to_ppid: T_PID_TO_PPID) -> str:
306
+ result = ""
307
+ sep = ""
308
+ for pid, ppid in pid_to_ppid.items():
309
+ result += sep + " {}->{}".format(ppid, pid)
310
+ sep = ", "
311
+ continue
312
+ return result
313
+
314
+ @staticmethod
315
+ def _find_postmaster__throw_error__bad_line_format(
316
+ lines: typing.List[str],
317
+ i_line: int,
318
+ hint: str,
319
+ ) -> typing.NoReturn:
320
+ assert type(lines) is list
321
+ assert type(i_line) is int
322
+ assert type(hint) is int
323
+
324
+ error_lines: typing.List[str] = []
325
+ error_lines.append(
326
+ "Line {} has bad format. Hint: {}.".format(
327
+ i_line + 1,
328
+ hint,
329
+ ),
330
+ )
331
+ error_lines.append(
332
+ "Problem line is:"
333
+ )
334
+ error_lines.append(
335
+ " " + repr(lines[i_line]),
336
+ )
337
+ error_lines.append(
338
+ "All the lines is:",
339
+ )
340
+ for i in range(len(lines)):
341
+ error_lines.append(
342
+ " {}. {!r}".format(i + 1, lines[i]),
343
+ )
344
+ continue
345
+
346
+ raise RuntimeError("\n".join(error_lines))
347
+
348
+ @staticmethod
349
+ def _find_postmaster__throw_error__ppid_is_changed(
350
+ pid: int,
351
+ ppid: int,
352
+ other_ppid: int,
353
+ lines: typing.List[str],
354
+ ) -> typing.NoReturn:
355
+ assert type(pid) is int
356
+ assert type(ppid) is int
357
+ assert type(other_ppid) is int
358
+ assert type(lines) is list
359
+
360
+ error_lines: typing.List[str] = []
361
+ error_lines.append(
362
+ "Parent of process ({}) is changed from {} to {}.".format(
363
+ pid,
364
+ other_ppid,
365
+ ppid,
366
+ ),
367
+ )
368
+ error_lines.append(
369
+ "All the lines is:",
370
+ )
371
+ for i in range(len(lines)):
372
+ error_lines.append(
373
+ " {}. {!r}".format(i + 1, lines[i]),
374
+ )
375
+ continue
376
+
377
+ raise RuntimeError("\n".join(error_lines))
378
+
379
+ @staticmethod
380
+ def _find_postmaster__throw_error__cycle(
381
+ pid_to_ppid: T_PID_TO_PPID,
382
+ ) -> typing.NoReturn:
383
+ msg = "Cycle in processes postgres process tree. "
384
+
385
+ msg += " List (ppid->pid): {},".format(
386
+ __class__._make_text_from_pid_to_ppid(pid_to_ppid),
387
+ )
388
+
389
+ msg += " List size is {}.".format(
390
+ len(pid_to_ppid),
391
+ )
392
+ raise RuntimeError(msg)
393
+
394
+ @staticmethod
395
+ def _find_postmaster__throw_error__many_postmasters(
396
+ postmaster_pids: typing.List[int],
397
+ pid_to_ppid: T_PID_TO_PPID,
398
+ ) -> typing.NoReturn:
399
+ msg = "Many processes like a postmaster are found ({}): {}.".format(
400
+ len(postmaster_pids),
401
+ ", ".join(map(str, postmaster_pids)),
402
+ )
403
+
404
+ msg += " Trees (ppid->pid): {}.".format(
405
+ __class__._make_text_from_pid_to_ppid(pid_to_ppid),
406
+ )
407
+
408
+ msg += " Total process count is {}.".format(len(pid_to_ppid))
409
+ raise RuntimeError(msg)
410
+
411
+ @staticmethod
412
+ def _find_postmaster__throw_error__fail(
413
+ bin_dir: str,
414
+ data_dir: str,
415
+ failures: typing.List[Exception],
416
+ ) -> typing.NoReturn:
417
+ assert type(bin_dir) is str
418
+ assert type(data_dir) is str
419
+ assert type(failures) is list
420
+
421
+ method_name = "InternalPlatformUtils::FindPostmaster(bin_dir={!r}, data_dir={!r})".format(
422
+ bin_dir,
423
+ data_dir,
424
+ )
425
+
426
+ RaiseError.function_did_multiple_attempts_without_stable_result(
427
+ method_name,
428
+ failures,
429
+ )
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from .. import internal_platform_utils as base
4
4
  from testgres.operations.os_ops import OsOperations
5
+ import typing
5
6
 
6
7
 
7
8
  class InternalPlatformUtils(base.InternalPlatformUtils):
@@ -15,3 +16,12 @@ class InternalPlatformUtils(base.InternalPlatformUtils):
15
16
  assert type(bin_dir) is str
16
17
  assert type(data_dir) is str
17
18
  return __class__.FindPostmasterResult.create_not_implemented()
19
+
20
+ def ProcessIsZombi_soft_check(
21
+ self,
22
+ os_ops: OsOperations,
23
+ pid: int,
24
+ ) -> typing.Optional[bool]:
25
+ assert isinstance(os_ops, OsOperations)
26
+ assert type(pid) is int
27
+ return None
@@ -0,0 +1,166 @@
1
+ from testgres.operations.os_ops import OsOperations
2
+
3
+ from ..port_manager import PortManager
4
+ from ..exceptions import PortForException
5
+ from .. import consts
6
+
7
+ import threading
8
+ import random
9
+ import typing
10
+ import logging
11
+
12
+
13
+ class OsLockFsObj:
14
+ _os_ops: typing.Optional[OsOperations]
15
+ _path: typing.Optional[str]
16
+
17
+ def __init__(self, os_ops: OsOperations, path: str):
18
+ assert isinstance(os_ops, OsOperations)
19
+ assert type(path) is str
20
+
21
+ os_ops.makedir(path) # throw
22
+
23
+ self._os_ops = os_ops
24
+ self._path = path
25
+ return
26
+
27
+ def release(self) -> None:
28
+ assert type(self._path) is str
29
+ assert isinstance(self._os_ops, OsOperations)
30
+ assert self._os_ops.path_exists(self._path)
31
+
32
+ self._os_ops.rmdir(self._path) # throw
33
+
34
+ self._path = None
35
+ self._os_ops = None
36
+ return
37
+
38
+
39
+ class PortManager__Generic2(PortManager):
40
+ _C_MIN_PORT_NUMBER = 1024
41
+ _C_MAX_PORT_NUMBER = 65535
42
+
43
+ _os_ops: OsOperations
44
+ _guard: typing.Any
45
+
46
+ # TODO: is there better to use bitmap fot _available_ports?
47
+ _available_ports: typing.Set[int]
48
+ _reserved_ports: typing.Dict[int, OsLockFsObj]
49
+
50
+ _lock_dir: typing.Optional[str]
51
+
52
+ def __init__(self, os_ops: OsOperations):
53
+ assert __class__._C_MIN_PORT_NUMBER <= __class__._C_MAX_PORT_NUMBER
54
+
55
+ assert os_ops is not None
56
+ assert isinstance(os_ops, OsOperations)
57
+ self._os_ops = os_ops
58
+ self._guard = threading.Lock()
59
+
60
+ self._available_ports = set(
61
+ range(__class__._C_MIN_PORT_NUMBER, __class__._C_MAX_PORT_NUMBER + 1)
62
+ )
63
+ assert len(self._available_ports) == (
64
+ (__class__._C_MAX_PORT_NUMBER - __class__._C_MIN_PORT_NUMBER) + 1
65
+ )
66
+ self._reserved_ports = dict()
67
+ self._lock_dir = None
68
+ return
69
+
70
+ def reserve_port(self) -> int:
71
+ assert self._guard is not None
72
+ assert type(self._available_ports) is set
73
+ assert type(self._reserved_ports) is dict
74
+ assert isinstance(self._os_ops, OsOperations)
75
+
76
+ with self._guard:
77
+ if self._lock_dir is None:
78
+ temp_dir = self._os_ops.get_tempdir()
79
+ assert type(temp_dir) is str
80
+ lock_dir = self._os_ops.build_path(temp_dir, consts.TMP_TESTGRES_PORTS)
81
+ assert type(lock_dir) is str
82
+ self._os_ops.makedirs(lock_dir)
83
+ self._lock_dir = lock_dir
84
+
85
+ assert self._lock_dir is not None
86
+ assert type(self._lock_dir) is str
87
+
88
+ t = tuple(self._available_ports)
89
+ assert len(t) == len(self._available_ports)
90
+ sampled_ports = random.sample(t, min(len(t), 100))
91
+ t = None
92
+
93
+ for port in sampled_ports:
94
+ assert type(port) is int
95
+ assert port not in self._reserved_ports
96
+ assert port in self._available_ports
97
+
98
+ assert port >= __class__._C_MIN_PORT_NUMBER
99
+ assert port <= __class__._C_MAX_PORT_NUMBER
100
+
101
+ if not self._os_ops.is_port_free(port):
102
+ continue
103
+
104
+ try:
105
+ lock_path = self.helper__make_lock_path(port)
106
+ lock_obj = OsLockFsObj(self._os_ops, lock_path) # raise
107
+ except: # noqa: E722
108
+ continue
109
+
110
+ assert isinstance(lock_obj, OsLockFsObj)
111
+ assert self._os_ops.path_exists(lock_path)
112
+
113
+ try:
114
+ self._reserved_ports[port] = lock_obj
115
+ except: # noqa: E722
116
+ assert port not in self._reserved_ports
117
+ lock_obj.release()
118
+ raise
119
+
120
+ self._available_ports.discard(port)
121
+ assert port in self._reserved_ports
122
+ assert port not in self._available_ports
123
+ __class__.helper__send_debug_msg("Port {} is reserved.", port)
124
+ return port
125
+
126
+ raise PortForException("Can't select a port.")
127
+
128
+ def release_port(self, number: int) -> None:
129
+ assert type(number) is int
130
+ assert number >= __class__._C_MIN_PORT_NUMBER
131
+ assert number <= __class__._C_MAX_PORT_NUMBER
132
+
133
+ assert self._guard is not None
134
+ assert type(self._reserved_ports) is dict
135
+
136
+ with self._guard:
137
+ assert number in self._reserved_ports
138
+ assert number not in self._available_ports
139
+ self._available_ports.add(number)
140
+ lock_obj = self._reserved_ports.pop(number)
141
+ assert number not in self._reserved_ports
142
+ assert number in self._available_ports
143
+ assert isinstance(lock_obj, OsLockFsObj)
144
+ lock_obj.release()
145
+ __class__.helper__send_debug_msg("Port {} is released.", number)
146
+ return
147
+
148
+ @staticmethod
149
+ def helper__send_debug_msg(msg_template: str, *args) -> None:
150
+ assert msg_template is not None
151
+ assert args is not None
152
+ assert type(msg_template) is str
153
+ assert type(args) is tuple
154
+ assert msg_template != ""
155
+ s = "[port manager] "
156
+ s += msg_template.format(*args)
157
+ logging.debug(s)
158
+
159
+ def helper__make_lock_path(self, port_number: int) -> str:
160
+ assert type(port_number) is int
161
+ # You have to call the reserve_port at first!
162
+ assert type(self._lock_dir) is str
163
+
164
+ result = self._os_ops.build_path(self._lock_dir, str(port_number) + ".lock")
165
+ assert type(result) is str
166
+ return result