"""``TlogDerivedClock`` strategy (AZ-398) — replay-only. Advances ``monotonic_ns`` on each call to the next timestamp emitted by the wrapped tlog-timestamp source. Out-of-order timestamps raise :class:`ClockOrderingError` (AC-6) — replay determinism is hard-failed, never silently smoothed. The strategy is constructed by the replay composition root (AZ-401) with a callable that yields tlog timestamps as the parser advances. For unit tests, an iterator of pre-known timestamps suffices. """ from __future__ import annotations from collections.abc import Callable, Iterable, Iterator class ClockOrderingError(RuntimeError): """Raised when the tlog-timestamp source emits a non-monotonic value. Replay must be deterministic; a strategy that silently smooths backward jumps would mask a genuine recording corruption. The error names the offending pair so the operator can correlate against the tlog message stream. """ class TlogDerivedClock: """Replay :class:`Clock` strategy driven by the tlog timestamp stream. The source can be either a callable returning ``int`` ns (typical when wired against the live tlog parser, AZ-399) or an iterable of pre-known ``int`` ns values (typical in unit tests). Both are normalised to an internal :class:`Iterator` lazily. Semantics: - :meth:`monotonic_ns` pulls the next value from the source on every call and returns it (advance-on-call). The most-recently-returned value is cached for :meth:`time_ns` so the two methods stay aligned. - :meth:`time_ns` returns the latest cached value; if no call to :meth:`monotonic_ns` has happened yet, it returns 0 (the replay composition root must pump at least one frame before any consumer asks for wall-clock time). - :meth:`sleep_until_ns` is a no-op (``pace=ASAP``). """ __slots__ = ("_source", "_last_ns") def __init__( self, source: Callable[[], int] | Iterable[int], ) -> None: if callable(source): self._source: Iterator[int] = _iter_from_callable(source) else: self._source = iter(source) self._last_ns = 0 def monotonic_ns(self) -> int: try: next_ns = next(self._source) except StopIteration: return self._last_ns if next_ns < self._last_ns: raise ClockOrderingError( f"TlogDerivedClock: non-monotonic timestamp " f"{next_ns} ns followed {self._last_ns} ns" ) self._last_ns = next_ns return next_ns def time_ns(self) -> int: return self._last_ns def sleep_until_ns(self, target_ns: int) -> None: """No-op in ASAP pace (Invariant 6).""" return None def _iter_from_callable(source: Callable[[], int]) -> Iterator[int]: """Wrap a callable as an iterator that calls it on each ``next()``. Used when the tlog parser exposes a ``next_timestamp_ns()``-style hook; consumers should NOT pass a side-effectful callable that blocks — the source is expected to be cheap (microsecond-class). """ while True: yield source() __all__ = ["ClockOrderingError", "TlogDerivedClock"]