Skip to content

Commit 1ad0a56

Browse files
dmitry-lipetskMark G.
andauthored
Detection of zombi postmaster is added (NodeStatus.Zombie) (#394)
* Detection of zombi postmaster is added (NodeStatus.Zombie) When we kill vanila-postmaster (10-18) on remote machine it becomes 'zombie'. pg_ctl says that node is started, by by fact it is not so. Now PostgresNode::status() check a postmaster process status (via /proc/<pid>/stat) and if 'Z' (zombie) is detected, this method returns the a status: NodeStatus.Zombie. Just for info: PG ENT does not have a such problem. Co-authored-by: Mark G. <mark@google.com> * impl__test_pg_ctl_wait_option is updated to process zombies. We may catch a zombie during a node stop: "FAILED tests/test_testgres_common.py::TestTestgresCommon::test_pg_ctl_wait_option[local] - Exception: Unexpected node status: 3." It was on astralinux_1_7. --------- Co-authored-by: Mark G. <mark@google.com>
1 parent 8b8c014 commit 1ad0a56

6 files changed

Lines changed: 120 additions & 11 deletions

File tree

src/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class NodeStatus(IntEnum):
2929
Status of a PostgresNode
3030
"""
3131

32-
Running, Stopped, Uninitialized = range(3)
32+
Running, Stopped, Uninitialized, Zombie = range(4)
3333

3434
# for Python 3.x
3535
def __bool__(self):

src/impl/platforms/internal_platform_utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,12 @@ def FindPostmaster(
6060
assert type(bin_dir) is str
6161
assert type(data_dir) is str
6262
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.")

src/impl/platforms/linux/internal_platform_utils.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import re
1010
import shlex
11+
import typing
1112

1213

1314
class InternalPlatformUtils(base.InternalPlatformUtils):
@@ -118,3 +119,53 @@ def is_space_or_tab(ch) -> bool:
118119
assert type(pid) is int
119120

120121
return __class__.FindPostmasterResult.create_ok(pid)
122+
123+
def ProcessIsZombi_soft_check(
124+
self,
125+
os_ops: OsOperations,
126+
pid: int,
127+
) -> typing.Optional[bool]:
128+
assert isinstance(os_ops, OsOperations)
129+
assert type(pid) is int
130+
131+
proc_stat_file = os_ops.build_path("/proc", str(pid), "stat")
132+
133+
if not os_ops.path_exists(proc_stat_file):
134+
return False
135+
136+
result: typing.Optional[bool] = None
137+
138+
try:
139+
# Read one line from /proc/PID/stat
140+
stat_content = os_ops.read_binary(proc_stat_file, 0).decode("utf-8", errors="ignore")
141+
142+
# We look for the closing parenthesis of the process name to ensure that
143+
# we start from it and not depend on spaces inside the parentheses!
144+
r_paren_idx = stat_content.rfind(")")
145+
146+
if r_paren_idx == -1:
147+
pass
148+
elif len(stat_content) <= r_paren_idx + 2:
149+
pass
150+
else:
151+
# The status goes exactly one space after the closing bracket
152+
assert (r_paren_idx + 2) < len(stat_content)
153+
proc_status = stat_content[r_paren_idx + 2]
154+
result = proc_status == "Z"
155+
except Exception as e:
156+
# If the file disappeared right during reading, it means the process is completely erased
157+
if __class__._is_file_not_found_exception(e):
158+
result = False
159+
160+
return result
161+
162+
@staticmethod
163+
def _is_file_not_found_exception(e: Exception) -> bool:
164+
if isinstance(e, FileNotFoundError):
165+
return True
166+
167+
if isinstance(e, ExecUtilException):
168+
if e.exit_code == 2:
169+
return True
170+
171+
return False

src/impl/platforms/win32/internal_platform_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .. import internal_platform_utils as base
44
from testgres.operations.os_ops import OsOperations
5+
import typing
56

67

78
class InternalPlatformUtils(base.InternalPlatformUtils):
@@ -15,3 +16,12 @@ def FindPostmaster(
1516
assert type(bin_dir) is str
1617
assert type(data_dir) is str
1718
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

src/utils.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,23 @@ def get_pg_node_state(
411411
attempt = 0
412412
sleep_time = C_SLEEP_TIME1
413413

414-
platform_utils: typing.Optional[internal_platform_utils_factory.InternalPlatformUtils] = None
414+
class tagPlaformUtilsProvider:
415+
T_PLATFORM_UTILS = internal_platform_utils_factory.InternalPlatformUtils
416+
417+
_platform_utils: typing.Optional[T_PLATFORM_UTILS] = None
418+
419+
def __init__(self):
420+
self._platform_utils = None
421+
422+
def get(self) -> T_PLATFORM_UTILS:
423+
if self._platform_utils is None:
424+
self._platform_utils = internal_platform_utils_factory.create_internal_platform_utils(os_ops)
425+
assert isinstance(self._platform_utils, __class__.T_PLATFORM_UTILS)
426+
427+
assert isinstance(self._platform_utils, __class__.T_PLATFORM_UTILS)
428+
return self._platform_utils
429+
430+
platform_utils_provider = tagPlaformUtilsProvider()
415431

416432
while True:
417433
assert type(attempt) is int
@@ -513,6 +529,13 @@ def get_pg_node_state(
513529

514530
assert pid != 0
515531

532+
# ----------------- detect zombie
533+
if platform_utils_provider.get().ProcessIsZombi_soft_check(os_ops, pid) is True:
534+
internal_utils.send_log_debug("Postmaster process {} is a zombie.".format(
535+
pid,
536+
))
537+
return PostgresNodeState(NodeStatus.Zombie, pid)
538+
516539
# -----------------
517540
return PostgresNodeState(NodeStatus.Running, pid)
518541

@@ -543,14 +566,8 @@ def get_pg_node_state(
543566
bin_dir,
544567
))
545568

546-
if platform_utils is None:
547-
platform_utils = internal_platform_utils_factory.create_internal_platform_utils(os_ops)
548-
assert isinstance(platform_utils, internal_platform_utils_factory.InternalPlatformUtils)
549-
550-
assert isinstance(platform_utils, internal_platform_utils_factory.InternalPlatformUtils)
551-
552569
try:
553-
find_postmaster_r = platform_utils.FindPostmaster(
570+
find_postmaster_r = platform_utils_provider.get().FindPostmaster(
554571
os_ops,
555572
bin_dir,
556573
data_dir,

tests/test_testgres_common.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,9 @@ def test_kill__ok(
687687
):
688688
assert isinstance(node_svc, PostgresNodeService)
689689

690-
with __class__.helper__get_node(node_svc) as node:
690+
node = __class__.helper__get_node(node_svc)
691+
692+
try:
691693
assert isinstance(node, PostgresNode)
692694
assert (node.pid == 0)
693695
assert (node.status() == NodeStatus.Uninitialized)
@@ -696,6 +698,9 @@ def test_kill__ok(
696698
assert not node.is_started
697699
node.slow_start()
698700
assert node.is_started
701+
702+
assert node.status() == NodeStatus.Running
703+
699704
node.kill()
700705
assert not node.is_started
701706

@@ -717,8 +722,19 @@ def test_kill__ok(
717722
if s == NodeStatus.Running:
718723
continue
719724

720-
assert s == NodeStatus.Stopped
725+
if s == NodeStatus.Stopped:
726+
logging.info("Node stopped")
727+
break
728+
729+
if s == NodeStatus.Zombie:
730+
logging.info("Node is zombie")
731+
break
732+
733+
logging.error("Node has unknown status: {}.".format(s.name))
721734
break
735+
finally:
736+
if node.is_started:
737+
node.stop()
722738
return
723739

724740
def test_kill_backgroud_writer__ok(
@@ -1614,9 +1630,15 @@ def impl__test_pg_ctl_wait_option(
16141630
logging.info("Attempt #{0}.".format(nAttempt))
16151631
s1 = node.status()
16161632

1633+
logging.info("Node status is {}.".format(s1.name))
1634+
16171635
if s1 == NodeStatus.Running:
16181636
continue
16191637

1638+
if s1 == NodeStatus.Zombie:
1639+
# [2026-07-12] We will wait for final stop (stabilization). OK?
1640+
continue
1641+
16201642
if s1 == NodeStatus.Stopped:
16211643
break
16221644

0 commit comments

Comments
 (0)