feat(sessions): add durable observation archive and replay API
This commit is contained in:
@@ -10,6 +10,7 @@ import pytest
|
||||
from paho.mqtt.packettypes import PacketTypes
|
||||
from paho.mqtt.reasoncodes import ReasonCode
|
||||
|
||||
import k1link.mqtt.capture as capture_module
|
||||
from k1link.mqtt.capture import (
|
||||
FRAME_HEADER,
|
||||
RAW_MAGIC,
|
||||
@@ -256,3 +257,85 @@ def test_capture_reader_rejects_invalid_or_unbounded_frames(
|
||||
|
||||
with pytest.raises(CaptureFormatError, match=message):
|
||||
list(iter_capture_frames(path, max_payload_bytes=4))
|
||||
|
||||
|
||||
def _mqtt_message(topic: str, payload: bytes) -> mqtt.MQTTMessage:
|
||||
message = mqtt.MQTTMessage(topic=topic.encode("utf-8"))
|
||||
message.payload = payload
|
||||
message.qos = 0
|
||||
message.retain = False
|
||||
message.dup = False
|
||||
return message
|
||||
|
||||
|
||||
def test_group_commit_fsyncs_raw_before_publishing_metadata(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
writer = capture_module._CaptureWriter(tmp_path / "capture", 1024) # noqa: SLF001
|
||||
writer.open()
|
||||
assert writer._raw is not None # noqa: SLF001
|
||||
assert writer._metadata is not None # noqa: SLF001
|
||||
raw_fd = writer._raw.fileno() # noqa: SLF001
|
||||
metadata_fd = writer._metadata.fileno() # noqa: SLF001
|
||||
fsync_calls: list[int] = []
|
||||
real_fsync = capture_module.os.fsync
|
||||
|
||||
def observe_fsync(descriptor: int) -> None:
|
||||
fsync_calls.append(descriptor)
|
||||
real_fsync(descriptor)
|
||||
|
||||
monkeypatch.setattr(capture_module.os, "fsync", observe_fsync)
|
||||
monkeypatch.setattr(capture_module, "GROUP_COMMIT_MAX_MESSAGES", 2)
|
||||
|
||||
writer.record(_mqtt_message("RealtimePath", b"one"))
|
||||
assert writer.metadata_path.read_bytes() == b""
|
||||
writer.record(_mqtt_message("RealtimePath", b"two"))
|
||||
|
||||
assert fsync_calls[:2] == [raw_fd, metadata_fd]
|
||||
assert len(writer.metadata_path.read_text(encoding="utf-8").splitlines()) == 2
|
||||
writer.close()
|
||||
|
||||
|
||||
def test_group_commit_timer_bounds_quiet_stream_rpo(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
writer = capture_module._CaptureWriter(tmp_path / "capture", 1024) # noqa: SLF001
|
||||
writer.open()
|
||||
writer.record(_mqtt_message("RealtimePath", b"one"))
|
||||
assert writer.metadata_path.read_bytes() == b""
|
||||
|
||||
writer.maybe_commit(
|
||||
writer._last_commit_monotonic # noqa: SLF001
|
||||
+ capture_module.GROUP_COMMIT_INTERVAL_SECONDS
|
||||
)
|
||||
|
||||
assert len(writer.metadata_path.read_text(encoding="utf-8").splitlines()) == 1
|
||||
writer.close()
|
||||
|
||||
|
||||
def test_raw_fsync_failure_never_publishes_metadata_ahead_of_raw(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
writer = capture_module._CaptureWriter(tmp_path / "capture", 1024) # noqa: SLF001
|
||||
writer.open()
|
||||
writer.record(_mqtt_message("RealtimePath", b"one"))
|
||||
assert writer._raw is not None # noqa: SLF001
|
||||
raw_fd = writer._raw.fileno() # noqa: SLF001
|
||||
real_fsync = capture_module.os.fsync
|
||||
|
||||
def fail_raw_fsync(descriptor: int) -> None:
|
||||
if descriptor == raw_fd:
|
||||
raise OSError("synthetic raw fsync failure")
|
||||
real_fsync(descriptor)
|
||||
|
||||
monkeypatch.setattr(capture_module.os, "fsync", fail_raw_fsync)
|
||||
|
||||
with pytest.raises(OSError, match="synthetic raw fsync failure"):
|
||||
writer._commit_pending() # noqa: SLF001
|
||||
|
||||
assert writer.metadata_path.read_bytes() == b""
|
||||
# Restore durability primitive so the fixture can close normally.
|
||||
monkeypatch.setattr(capture_module.os, "fsync", real_fsync)
|
||||
writer.close()
|
||||
|
||||
Reference in New Issue
Block a user