Skip to content

Commit f278bff

Browse files
utils: public API surface is restored (#443)
1 parent 705f6df commit f278bff

5 files changed

Lines changed: 341 additions & 294 deletions

File tree

‎src/impl/internal_node_utils.py‎

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
from __future__ import annotations
2+
3+
from . import internal_utils
4+
from . platforms.internal_platform_utils_provider import InternalPlaformUtilsProvider
5+
6+
from .. import consts
7+
from ..node_state import PostgresNodeState
8+
from ..enums import NodeStatus
9+
from ..raise_error import RaiseError
10+
from ..exceptions import ExecUtilException
11+
12+
from testgres.operations.os_ops import OsOperations
13+
14+
import typing
15+
import time
16+
17+
18+
class InternalNodeUtils:
19+
T_PLATFORM_UTILS = InternalPlaformUtilsProvider.T_PLATFORM_UTILS
20+
21+
@staticmethod
22+
def get_pg_node_state(
23+
os_ops: OsOperations,
24+
bin_dir: str,
25+
data_dir: str,
26+
utils_log_file: typing.Optional[str],
27+
) -> PostgresNodeState:
28+
assert isinstance(os_ops, OsOperations)
29+
assert type(bin_dir) is str
30+
assert type(data_dir) is str
31+
assert utils_log_file is None or type(utils_log_file) is str
32+
33+
C_MAX_ATTEMPTS = 3
34+
C_SLEEP_TIME1 = 1
35+
C_SLEEP_TIME_MULT = 2
36+
37+
pg_ctl_params = [
38+
os_ops.build_path(bin_dir, consts.BINARY_NAME__PG_CTL),
39+
"-D",
40+
data_dir,
41+
"status",
42+
]
43+
44+
attempt = 0
45+
sleep_time = C_SLEEP_TIME1
46+
47+
platform_utils_provider = InternalPlaformUtilsProvider(
48+
os_ops,
49+
)
50+
51+
while True:
52+
assert type(attempt) is int
53+
assert attempt >= 0
54+
assert attempt < C_MAX_ATTEMPTS
55+
56+
attempt += 1
57+
58+
if attempt > 1:
59+
internal_utils.send_log_debug("Sleep {} second(s) before an attempt #{}".format(
60+
sleep_time,
61+
attempt,
62+
))
63+
time.sleep(sleep_time)
64+
sleep_time = sleep_time * C_SLEEP_TIME_MULT
65+
66+
exec_r = internal_utils.execute_utility3(
67+
os_ops,
68+
pg_ctl_params,
69+
utils_log_file,
70+
check=False,
71+
)
72+
73+
status_code = exec_r.returncode
74+
out = exec_r.stdout
75+
error = exec_r.stderr
76+
77+
assert type(status_code) is int
78+
assert type(out) is str
79+
assert type(error) is str
80+
81+
# -----------------
82+
if status_code == consts.PG_CTL__STATUS__NODE_IS_STOPPED:
83+
return PostgresNodeState(NodeStatus.Stopped, None)
84+
85+
# -----------------
86+
if status_code == consts.PG_CTL__STATUS__BAD_DATADIR:
87+
return PostgresNodeState(NodeStatus.Uninitialized, None)
88+
89+
# -----------------
90+
if status_code == consts.PG_CTL__STATUS__OK:
91+
pid = __class__._parse_pid(
92+
out,
93+
pg_ctl_params,
94+
)
95+
assert type(pid) is int
96+
assert pid != 0
97+
98+
# ----------------- detect zombie
99+
if platform_utils_provider.get().ProcessIsZombie_soft_check(os_ops, pid) is True:
100+
internal_utils.send_log_debug("Postmaster process {} is a zombie.".format(
101+
pid,
102+
))
103+
return PostgresNodeState(NodeStatus.Zombie, pid)
104+
105+
# -----------------
106+
return PostgresNodeState(NodeStatus.Running, pid)
107+
108+
assert status_code != consts.PG_CTL__STATUS__OK
109+
110+
errMsg = "Getting of a node status [data_dir is {0}] failed.".format(
111+
data_dir,
112+
)
113+
114+
e1 = ExecUtilException(
115+
message=errMsg,
116+
command=pg_ctl_params,
117+
exit_code=status_code,
118+
out=out,
119+
error=error,
120+
)
121+
122+
if status_code == consts.PG_CTL__STATUS__FAILED:
123+
internal_utils.send_log_debug(
124+
"pg_ctl fails with an error: {}".format(
125+
exec_r.stderr,
126+
))
127+
128+
try:
129+
find_postmaster_r = platform_utils_provider.get().FindPostmaster(
130+
os_ops,
131+
bin_dir,
132+
data_dir,
133+
)
134+
except Exception as e2:
135+
raise e2 from e1
136+
137+
assert type(find_postmaster_r) is __class__.T_PLATFORM_UTILS.FindPostmasterResult
138+
139+
if find_postmaster_r.code == __class__.T_PLATFORM_UTILS.FindPostmasterResultCode.ok:
140+
# Postmaster is alive. Let's wait a few seconds and check its status again.
141+
internal_utils.send_log_debug(
142+
"Postmaster is found and has PID {}.".format(
143+
find_postmaster_r.pid,
144+
))
145+
146+
if attempt < C_MAX_ATTEMPTS:
147+
continue
148+
149+
raise e1
150+
151+
@staticmethod
152+
def _parse_pid(
153+
out: str,
154+
pg_ctl_params,
155+
) -> int:
156+
assert type(out) is str
157+
158+
if out == "":
159+
RaiseError.pg_ctl_returns_an_empty_string(
160+
pg_ctl_params,
161+
)
162+
163+
C_PID_PREFIX = "(PID: "
164+
165+
i = out.find(C_PID_PREFIX)
166+
167+
if i == -1:
168+
RaiseError.pg_ctl_returns_an_unexpected_string(
169+
out,
170+
pg_ctl_params,
171+
)
172+
173+
assert i > 0
174+
assert i < len(out)
175+
assert len(C_PID_PREFIX) <= len(out)
176+
assert i <= len(out) - len(C_PID_PREFIX)
177+
178+
i += len(C_PID_PREFIX)
179+
start_pid_s = i
180+
181+
while True:
182+
if i == len(out):
183+
RaiseError.pg_ctl_returns_an_unexpected_string(
184+
out,
185+
pg_ctl_params,
186+
)
187+
188+
ch = out[i]
189+
190+
if ch == ")":
191+
break
192+
193+
if ch.isdigit():
194+
i += 1
195+
continue
196+
197+
RaiseError.pg_ctl_returns_an_unexpected_string(
198+
out,
199+
pg_ctl_params,
200+
)
201+
assert False
202+
203+
if i == start_pid_s:
204+
RaiseError.pg_ctl_returns_an_unexpected_string(
205+
out,
206+
pg_ctl_params,
207+
)
208+
209+
# TODO: Let's verify a length of pid string.
210+
211+
pid = int(out[start_pid_s:i])
212+
213+
if pid == 0:
214+
RaiseError.pg_ctl_returns_a_zero_pid(
215+
out,
216+
pg_ctl_params,
217+
)
218+
219+
assert pid != 0
220+
221+
return pid

‎src/impl/internal_utils.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
from testgres.operations.os_ops import OsOperations
2+
from testgres.operations.os_ops import OsCommandResult
3+
from testgres.operations.types import T_OS_CMD
4+
from testgres.operations.types import T_OS_EXEC_ENV
5+
from testgres.operations.helpers import Helpers as OsHelpers
6+
7+
from ..exceptions import ExecUtilException
28

39
import logging
410
import typing
@@ -90,3 +96,55 @@ def read_line_to_pos__bin(
9096
assert type(result) is bytes
9197
assert len(result) <= (position - read_position)
9298
return result
99+
100+
101+
def execute_utility3(
102+
os_ops: OsOperations,
103+
args: T_OS_CMD,
104+
logfile: typing.Optional[str] = None,
105+
check: bool = True,
106+
exec_env: typing.Optional[T_OS_EXEC_ENV] = None,
107+
) -> OsCommandResult:
108+
assert os_ops is not None
109+
assert isinstance(os_ops, OsOperations)
110+
assert type(check) is bool
111+
assert exec_env is None or type(exec_env) is dict
112+
113+
exec_r = os_ops.run(
114+
args,
115+
check=check,
116+
encoding=OsHelpers.GetDefaultEncoding(),
117+
exec_env=exec_env,
118+
)
119+
120+
assert type(exec_r) is OsCommandResult
121+
122+
# write new log entry if possible
123+
if logfile:
124+
try:
125+
log_lines = [
126+
os_ops.join_command_arguments(args),
127+
]
128+
129+
if exec_r.stdout is None:
130+
log_lines.append("# #NONE#")
131+
else:
132+
# comment-out lines
133+
assert type(exec_r.stdout) is str
134+
log_lines += ['# ' + line for line in exec_r.stdout.splitlines()]
135+
136+
log_lines.append("")
137+
138+
os_ops.write(
139+
filename=logfile,
140+
data="\n".join(log_lines),
141+
truncate=False,
142+
)
143+
except IOError:
144+
raise ExecUtilException(
145+
"Problem with writing to logfile `{}` during run command `{}`".format(
146+
logfile,
147+
args,
148+
))
149+
150+
return exec_r
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from .internal_platform_utils_factory import create_internal_platform_utils
2+
from .internal_platform_utils_factory import InternalPlatformUtils
3+
4+
from testgres.operations.os_ops import OsOperations
5+
6+
import typing
7+
8+
9+
class InternalPlaformUtilsProvider:
10+
T_PLATFORM_UTILS = InternalPlatformUtils
11+
12+
_os_ops: OsOperations
13+
_platform_utils: typing.Optional[T_PLATFORM_UTILS] = None
14+
15+
def __init__(
16+
self,
17+
os_ops: OsOperations,
18+
):
19+
assert isinstance(os_ops, OsOperations)
20+
self._os_ops = os_ops
21+
self._platform_utils = None
22+
return
23+
24+
def get(self) -> T_PLATFORM_UTILS:
25+
if self._platform_utils is None:
26+
self._platform_utils = create_internal_platform_utils(
27+
self._os_ops,
28+
)
29+
assert isinstance(self._platform_utils, __class__.T_PLATFORM_UTILS)
30+
31+
assert isinstance(self._platform_utils, __class__.T_PLATFORM_UTILS)
32+
return self._platform_utils

‎src/node_state.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from .enums import NodeStatus
2+
3+
import typing
4+
5+
6+
class PostgresNodeState:
7+
node_status: NodeStatus
8+
pid: typing.Optional[int]
9+
10+
def __init__(
11+
self,
12+
node_status: NodeStatus,
13+
pid: typing.Optional[int],
14+
):
15+
assert type(node_status) is NodeStatus
16+
assert pid is None or type(pid) is int
17+
18+
self.node_status = node_status
19+
self.pid = pid
20+
return

0 commit comments

Comments
 (0)