oc_chat
Loading...
Searching...
No Matches
clock.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/clock.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_storage_foundation.py"
6"""
7Injectable clock service for registry code.
8
9Purpose
10-------
11Centralize all wall-clock and monotonic reads used by the registry so
12tests can pin time and production code never reads time directly.
13
14Responsibilities
15----------------
16- Define the two-read :class:`Clock` contract.
17- Provide :class:`RealClock` backed by Python timers.
18- Provide :class:`FakeClock` for deterministic tests.
19- Provide wall-time arithmetic helpers so other registry modules do not
20 import time or datetime directly.
21
22Diagnostics
23-----------
24Domain: OC_CHAT.MAIN
25Levels:
26 L2 - lifecycle events
27 L3 - semantic details
28 L4 - deep tracing
29
30Contracts
31---------
32- This is the only module under ``oc_chat.registry`` allowed to import
33 ``time`` or ``datetime``.
34- ``now_wall`` returns explicit RFC3339 UTC ending in ``Z``.
35- ``state_version`` ordering is an integer counter; monotonic timing is
36 available for timeout logic and wall time is only stored as text.
37
38Dependencies
39------------
40- oc_diagnostics (structured debug logging via _dbg)
41- time (wall-clock and monotonic nanosecond reads for RealClock)
42- datetime (RFC3339 UTC formatting, parsing and wall-time arithmetic)
43"""
44from __future__ import annotations
45
46from abc import ABC, abstractmethod
47from dataclasses import dataclass
48from datetime import datetime, timedelta, timezone
49import time
50
51from oc_diagnostics import _dbg
52
53
54_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
55
56
57class Clock(ABC):
58 """Two-read clock contract required by the registry spec."""
59
60 @abstractmethod
61 def now_wall(self) -> str:
62 """Return the current wall time as RFC3339 UTC."""
63
64 @abstractmethod
65 def now_monotonic_ns(self) -> int:
66 """Return a monotonic nanosecond counter."""
67
68
69def _rfc3339_from_time_ns(value: int) -> str:
70 seconds, nanoseconds = divmod(value, 1_000_000_000)
71 dt = datetime.fromtimestamp(seconds, tz=timezone.utc).replace(
72 microsecond=nanoseconds // 1_000
73 )
74 return dt.isoformat(timespec="microseconds").replace("+00:00", "Z")
75
76
77def wall_plus_seconds(wall: str, seconds: int) -> str:
78 """Return ``wall`` plus ``seconds`` as RFC3339 UTC."""
79
80 if seconds < 0:
81 raise ValueError("seconds must be non-negative")
82 normalized = wall[:-1] + "+00:00" if wall.endswith("Z") else wall
83 dt = datetime.fromisoformat(normalized)
84 if dt.tzinfo is None:
85 dt = dt.replace(tzinfo=timezone.utc)
86 else:
87 dt = dt.astimezone(timezone.utc)
88 return (
89 (dt + timedelta(seconds=seconds))
90 .isoformat(timespec="microseconds")
91 .replace("+00:00", "Z")
92 )
93
94
96 """Clock backed by Python's wall and monotonic timers."""
97
98 def now_wall(self) -> str:
99 return _rfc3339_from_time_ns(time.time_ns())
100
101 def now_monotonic_ns(self) -> int:
102 return time.monotonic_ns()
103
104
105@dataclass
107 """Pinned clock for deterministic registry tests."""
108
109 wall: str = "2026-05-20T18:30:41.123456Z"
110 monotonic_ns: int = 0
111
112 def now_wall(self) -> str:
113 return self.wall
114
115 def now_monotonic_ns(self) -> int:
116 return self.monotonic_ns
117
118 def set_wall(self, value: str) -> None:
119 self.wall = value
120
121 def set_monotonic_ns(self, value: int) -> None:
122 self.monotonic_ns = value
None set_wall(self, str value)
Definition clock.py:118
None set_monotonic_ns(self, int value)
Definition clock.py:121
str _rfc3339_from_time_ns(int value)
Definition clock.py:69
str wall_plus_seconds(str wall, int seconds)
Definition clock.py:77