Skip to content

Commit c9a562f

Browse files
[#323] TestTestgresCommon::test_restart + PostgresNodeLogReader are updated (#417)
* [#323] TestTestgresCommon::test_restart + PostgresNodeLogReader are updated 1) test_restart does multiple (5) attempts to restart 2) PostgresNodeLogReader processes from_beginnig=False correctly Closes #323. * Tests for PostgresNodeLogReader._create_log_info are added * PostgresNodeLogReader::_create_log_info is optimized (os_ops 3.2.0) PostgresNodeLogReader::_create_log_info now requires os_ops 3.2.0 to read backward optimally.
1 parent 73a1fa8 commit c9a562f

6 files changed

Lines changed: 264 additions & 17 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ dependencies = [
6565
"six>=1.9.0",
6666
"psutil",
6767
"packaging",
68-
"testgres.os_ops>=3.1.0,<4.0.0",
68+
"testgres.os_ops>=3.2.0,<4.0.0",
6969
]
7070

7171
[project.urls]

src/node.py

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,9 +2430,14 @@ class LogInfo:
24302430
position: int
24312431
tail: bytes
24322432

2433-
def __init__(self, position: int):
2433+
def __init__(self, position: int, tail: bytes = b''):
2434+
assert type(position) is int
2435+
assert type(tail) is bytes
2436+
assert position >= 0
2437+
24342438
self.position = position
2435-
self.tail = b''
2439+
self.tail = tail
2440+
return
24362441

24372442
# --------------------------------------------------------------------
24382443
class LogDataBlock:
@@ -2487,7 +2492,7 @@ def __init__(self, node: PostgresNode, from_beginnig: bool):
24872492
if from_beginnig:
24882493
self._logs = dict()
24892494
else:
2490-
self._logs = self._collect_logs()
2495+
self._logs = self._collect_logs(find_line_start=True)
24912496

24922497
assert type(self._logs) is dict
24932498
return
@@ -2496,7 +2501,7 @@ def read(self) -> typing.List[LogDataBlock]:
24962501
assert self._node is not None
24972502
assert isinstance(self._node, PostgresNode)
24982503

2499-
cur_logs: typing.Dict[str, __class__.LogInfo] = self._collect_logs()
2504+
cur_logs = self._collect_logs(find_line_start=False)
25002505
assert cur_logs is not None
25012506
assert type(cur_logs) is dict
25022507

@@ -2570,7 +2575,8 @@ def read(self) -> typing.List[LogDataBlock]:
25702575

25712576
return result
25722577

2573-
def _collect_logs(self) -> typing.Dict[str, LogInfo]:
2578+
def _collect_logs(self, find_line_start: bool) -> typing.Dict[str, LogInfo]:
2579+
assert type(find_line_start) is bool
25742580
assert self._node is not None
25752581
assert isinstance(self._node, PostgresNode)
25762582

@@ -2587,14 +2593,92 @@ def _collect_logs(self) -> typing.Dict[str, LogInfo]:
25872593
if not self._node.os_ops.path_exists(f):
25882594
continue
25892595

2590-
file_size = self._node.os_ops.get_file_size(f)
2591-
assert type(file_size) is int
2592-
assert file_size >= 0
2593-
2594-
result[f] = __class__.LogInfo(file_size)
2596+
result[f] = self._create_log_info(
2597+
self._node.os_ops,
2598+
f,
2599+
find_line_start,
2600+
)
2601+
continue
25952602

25962603
return result
25972604

2605+
@staticmethod
2606+
def _create_log_info(
2607+
os_ops: OsOperations,
2608+
filename: str,
2609+
find_line_start: bool,
2610+
) -> LogInfo:
2611+
assert type(filename) is str
2612+
assert type(find_line_start) is bool
2613+
assert len(filename) > 0
2614+
assert os_ops is not None
2615+
assert isinstance(os_ops, OsOperations)
2616+
2617+
file_size = os_ops.get_file_size(filename)
2618+
assert type(file_size) is int
2619+
assert file_size >= 0
2620+
2621+
if not find_line_start:
2622+
return __class__.LogInfo(
2623+
position=file_size,
2624+
tail=b'',
2625+
)
2626+
2627+
read_position = file_size
2628+
tail_blocks: typing.List[bytes] = []
2629+
2630+
C_BACK_READ_BLOCK_SIZE = 4096
2631+
2632+
while read_position > 0:
2633+
if read_position < C_BACK_READ_BLOCK_SIZE:
2634+
read_offset = 0
2635+
else:
2636+
read_offset = read_position - C_BACK_READ_BLOCK_SIZE
2637+
2638+
assert read_offset >= 0
2639+
assert read_offset < file_size
2640+
assert read_offset < read_position
2641+
2642+
block_sz = read_position - read_offset
2643+
2644+
assert block_sz > 0
2645+
2646+
# read from read_offset to file end
2647+
block = os_ops.read_binary(filename, read_offset, block_sz)
2648+
2649+
assert type(block) is bytes
2650+
2651+
if len(block) != block_sz:
2652+
err_msg = "[BUG CHECK] Readed block has bad size ({}). Expected size is ({}). File name {}.".format(
2653+
len(block),
2654+
block_sz,
2655+
filename,
2656+
)
2657+
raise RuntimeError(err_msg)
2658+
2659+
assert len(block) == block_sz
2660+
2661+
x = block.rfind(b"\n", 0, block_sz)
2662+
2663+
if x == -1:
2664+
tail_blocks.append(block)
2665+
read_position = read_offset
2666+
continue
2667+
2668+
if x == block_sz - 1:
2669+
break
2670+
2671+
block = block[x + 1:]
2672+
tail_blocks.append(block)
2673+
break
2674+
2675+
tail = b''.join(reversed(tail_blocks))
2676+
2677+
return __class__.LogInfo(
2678+
position=file_size,
2679+
tail=tail,
2680+
)
2681+
25982682

25992683
class PostgresNodeUtils:
26002684
@staticmethod

tests/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ pytest-env
44
pytest-xdist
55
psycopg2
66
six
7-
testgres.os_ops>=3.1.0,<4.0.0
7+
testgres.os_ops>=3.2.0,<4.0.0
88
testgres.postgres_configuration>=0.2.2,<1.0.0

tests/test_testgres_common.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -456,12 +456,70 @@ def test_restart(self, node_svc: PostgresNodeService):
456456
assert isinstance(node_svc, PostgresNodeService)
457457

458458
with __class__.helper__get_node(node_svc) as node:
459-
node.init().start()
459+
node.init()
460+
461+
nRestartAttempt = 0
462+
463+
while True:
464+
nRestartAttempt += 1
465+
466+
logging.info("Attempt #{}".format(nRestartAttempt))
467+
468+
node.start()
469+
470+
# restart, ok
471+
res = node.execute('select 1')
472+
assert (res == [(1,)])
473+
474+
node_log_reader = PostgresNodeLogReader(
475+
node,
476+
from_beginnig=False,
477+
)
478+
479+
try:
480+
node.restart()
481+
except StartNodeException as e:
482+
logging.info("Exception ({}): {}".format(
483+
type(e).__name__,
484+
e,
485+
))
486+
487+
if nRestartAttempt == 5:
488+
raise
489+
490+
if not PostgresNodeUtils.detect_port_conflict(node_log_reader):
491+
raise
492+
493+
logging.info("Node port {} conflicted with another PostgreSQL instance.".format(
494+
node.port
495+
))
496+
497+
logging.info("Wait for node stop")
498+
499+
nStopAttemtp = 0
500+
501+
while True:
502+
if nStopAttemtp == 5:
503+
raise RuntimeError("Node is not stopped!")
504+
505+
nStopAttemtp += 1
506+
507+
time.sleep(1)
508+
509+
node_status = node.status()
510+
511+
logging.info("Node status is {}".format(node_status))
512+
513+
if node_status == NodeStatus.Stopped:
514+
break
515+
continue
516+
517+
# node is stopped. try again
518+
continue
519+
520+
assert node.status() == NodeStatus.Running
521+
break
460522

461-
# restart, ok
462-
res = node.execute('select 1')
463-
assert (res == [(1,)])
464-
node.restart()
465523
res = node.execute('select 2')
466524
assert (res == [(2,)])
467525

tests/units/node/PostgresNodeLogReader/__init__.py

Whitespace-only changes.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from __future__ import annotations
2+
3+
from ....helpers.global_data import OsOpsDescrs
4+
from ....helpers.global_data import OsOpsDescr
5+
from ....helpers.global_data import OsOperations
6+
7+
from src.node import PostgresNodeLogReader
8+
9+
import pytest
10+
import typing
11+
import random
12+
13+
14+
class TestSetM001__helper_create_log_info:
15+
sm_os_ops_descrs: typing.List[OsOpsDescr] = [
16+
OsOpsDescrs.sm_local_os_ops_descr,
17+
OsOpsDescrs.sm_remote_os_ops_descr
18+
]
19+
20+
@pytest.fixture(
21+
params=[
22+
pytest.param(
23+
descr,
24+
id=descr.sign,
25+
)
26+
for descr in sm_os_ops_descrs
27+
],
28+
)
29+
def os_ops_descr(self, request: pytest.FixtureRequest) -> OsOpsDescr:
30+
assert isinstance(request, pytest.FixtureRequest)
31+
assert isinstance(request.param, OsOpsDescr)
32+
return request.param
33+
34+
def test_001__common(self, os_ops_descr: OsOpsDescr):
35+
assert type(os_ops_descr) is OsOpsDescr
36+
os_ops = os_ops_descr.os_ops
37+
assert isinstance(os_ops, OsOperations)
38+
39+
filename = os_ops.mkstemp("data_for_create_log_info")
40+
41+
# Scenario 0: The log file ends with a normal line feed
42+
C_DATA0 = b""
43+
os_ops.write(filename, C_DATA0, binary=True, truncate=True)
44+
45+
log_info1 = PostgresNodeLogReader._create_log_info(os_ops, filename, find_line_start=True)
46+
assert log_info1.tail == b""
47+
assert log_info1.position == len(C_DATA0)
48+
49+
# Scenario 1: The log file ends with a normal line feed
50+
C_DATA1 = b"Line 1\nLine 2\n"
51+
os_ops.write(filename, C_DATA1, binary=True, truncate=True)
52+
53+
log_info1 = PostgresNodeLogReader._create_log_info(os_ops, filename, find_line_start=True)
54+
# Since the file ends with \n, the tail must be empty!
55+
assert log_info1.tail == b""
56+
assert log_info1.position == len(C_DATA1)
57+
58+
# Scenario 2: The log file contains an unterminated line (our UTF-8 trap)
59+
C_DATA2 = b"Line 1\nLine 2\nIncomplete UTF8 \xd0"
60+
os_ops.write(filename, C_DATA2, binary=True, truncate=True)
61+
62+
log_info2 = PostgresNodeLogReader._create_log_info(os_ops, filename, find_line_start=True)
63+
# The tail must contain exactly the piece after the last \n
64+
assert log_info2.tail == b"Incomplete UTF8 \xd0"
65+
assert log_info2.position == len(C_DATA2)
66+
67+
# Scenario 3: The file has no line breaks at all (one long line)
68+
C_DATA3 = b"Just one long line without newlines"
69+
os_ops.write(filename, C_DATA3, binary=True, truncate=True)
70+
71+
log_info3 = PostgresNodeLogReader._create_log_info(os_ops, filename, find_line_start=True)
72+
# Should take the entire file in tail, and set the position to the file size
73+
assert log_info3.tail == C_DATA3
74+
assert log_info3.position == len(C_DATA3)
75+
76+
# 4. Large data (two segments)
77+
allowed_bytes = bytes([b for b in range(256) if b != 10])
78+
C_DATA4 = bytes(random.choices(allowed_bytes, k=5000))
79+
os_ops.write(filename, C_DATA4, binary=True, truncate=True)
80+
81+
log_info = PostgresNodeLogReader._create_log_info(os_ops, filename, find_line_start=True)
82+
assert log_info.tail == C_DATA4
83+
assert log_info.position == len(C_DATA4)
84+
85+
# 5. Large data (many segments)
86+
allowed_bytes = bytes([b for b in range(256) if b != 10])
87+
C_DATA5 = bytes(random.choices(allowed_bytes, k=999983))
88+
os_ops.write(filename, C_DATA5, binary=True, truncate=True)
89+
90+
log_info = PostgresNodeLogReader._create_log_info(os_ops, filename, find_line_start=True)
91+
assert log_info.tail == C_DATA5
92+
assert log_info.position == len(C_DATA5)
93+
94+
# 6. Large data (first_line + many segments)
95+
allowed_bytes = bytes([b for b in range(256) if b != 10])
96+
os_ops.write(filename, b'abcd\n', binary=True, truncate=True)
97+
os_ops.write(filename, C_DATA5, binary=True, truncate=False)
98+
99+
log_info = PostgresNodeLogReader._create_log_info(os_ops, filename, find_line_start=True)
100+
assert log_info.tail == C_DATA5
101+
assert log_info.position == 5 + len(C_DATA5)
102+
103+
# Cleanup
104+
os_ops.remove_file(filename)
105+
return

0 commit comments

Comments
 (0)