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.
- {testgres-1.14.5/testgres.egg-info → testgres-1.15.0}/PKG-INFO +2 -2
- {testgres-1.14.5 → testgres-1.15.0}/pyproject.toml +1 -1
- {testgres-1.14.5 → testgres-1.15.0}/src/__init__.py +2 -3
- {testgres-1.14.5 → testgres-1.15.0}/src/consts.py +4 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/enums.py +1 -1
- {testgres-1.14.5 → testgres-1.15.0}/src/impl/platforms/internal_platform_utils.py +9 -0
- testgres-1.15.0/src/impl/platforms/linux/internal_platform_utils.py +429 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/impl/platforms/win32/internal_platform_utils.py +10 -0
- testgres-1.15.0/src/impl/port_manager__generic2.py +166 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/impl/port_manager__this_host.py +2 -1
- {testgres-1.14.5 → testgres-1.15.0}/src/node.py +66 -8
- {testgres-1.14.5 → testgres-1.15.0}/src/raise_error.py +37 -5
- {testgres-1.14.5 → testgres-1.15.0}/src/utils.py +28 -14
- {testgres-1.14.5 → testgres-1.15.0/testgres.egg-info}/PKG-INFO +2 -2
- {testgres-1.14.5 → testgres-1.15.0}/testgres.egg-info/SOURCES.txt +1 -0
- {testgres-1.14.5 → testgres-1.15.0}/testgres.egg-info/requires.txt +1 -1
- testgres-1.15.0/tests/test_os_ops_common.py +3906 -0
- {testgres-1.14.5 → testgres-1.15.0}/tests/test_os_ops_local.py +42 -6
- {testgres-1.14.5 → testgres-1.15.0}/tests/test_os_ops_remote.py +57 -13
- {testgres-1.14.5 → testgres-1.15.0}/tests/test_testgres_common.py +100 -18
- {testgres-1.14.5 → testgres-1.15.0}/tests/test_testgres_local.py +0 -35
- {testgres-1.14.5 → testgres-1.15.0}/tests/test_testgres_remote.py +31 -9
- testgres-1.14.5/src/impl/platforms/linux/internal_platform_utils.py +0 -120
- testgres-1.14.5/tests/test_os_ops_common.py +0 -1400
- {testgres-1.14.5 → testgres-1.15.0}/LICENSE +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/README.md +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/setup.cfg +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/api.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/backup.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/cache.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/config.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/connection.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/decorators.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/defaults.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/exceptions.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/impl/file_line_reader.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/impl/internal_utils.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/impl/platforms/internal_platform_utils_factory.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/impl/port_manager__generic.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/logger.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/node_app.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/port_manager.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/pubsub.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/src/standby.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/testgres.egg-info/dependency_links.txt +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/testgres.egg-info/top_level.txt +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/tests/test_api.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/tests/test_config.py +0 -0
- {testgres-1.14.5 → testgres-1.15.0}/tests/test_raise_error.py +0 -0
- {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.
|
|
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<
|
|
30
|
+
Requires-Dist: testgres.os_ops<4.0.0,>=3.1.0
|
|
31
31
|
Dynamic: license-file
|
|
32
32
|
|
|
33
33
|
[](https://github.com/postgrespro/testgres/actions/workflows/ci.yml)
|
|
@@ -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.
|
|
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", "
|
|
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
|
]
|
|
@@ -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
|