Source code for ralph.display.raw_overflow
"""Per-unit raw NDJSON overflow log writer."""
from __future__ import annotations
import contextlib
import re
import threading
import time
from typing import TYPE_CHECKING, BinaryIO, cast
if TYPE_CHECKING:
from collections.abc import Callable
from pathlib import Path
_SAFE_CHARS = re.compile(r"[^A-Za-z0-9._-]")
DEFAULT_MAX_OVERFLOW_FILE_BYTES = 50 * 1024 * 1024
#: Userspace buffer for the persistent handle. Amortizes write syscalls
#: (and the fsevents they generate) across many appended lines.
_BUFFER_BYTES = 64 * 1024
#: Default seconds between forced flushes. MUST stay well below
#: ralph.timeout_defaults.LOG_GROWTH_SECONDS (30.0): operators tail this
#: file and the on-disk copy must never look wedged while the unit is live.
DEFAULT_FLUSH_INTERVAL_SECONDS = 5.0
def _sanitize_unit_id(unit_id: str) -> str:
return _SAFE_CHARS.sub("_", unit_id)
[docs]
class RawOverflowLog:
"""Append-mode raw log for a single work unit.
Thread-safe. Holds one buffered file handle open for the unit's
lifetime instead of opening/closing per line (the per-line pattern
generated an fsevent storm on long runs). Silently no-ops on
filesystem errors so the display path never crashes due to a
read-only workspace.
"""
def __init__(
self,
workspace_root: Path,
unit_id: str,
*,
max_bytes: int = DEFAULT_MAX_OVERFLOW_FILE_BYTES,
flush_interval_seconds: float = DEFAULT_FLUSH_INTERVAL_SECONDS,
now: Callable[[], float] = time.monotonic,
) -> None:
safe_id = _sanitize_unit_id(unit_id)
self.path = workspace_root / ".agent" / "raw" / f"{safe_id}.log"
self._lock = threading.Lock()
self._first_write = True
self._disabled = False
self._max_bytes = max(max_bytes, 0)
self._bytes_written = 0
self._flush_interval = max(flush_interval_seconds, 0.0)
self._now = now
self._fh: BinaryIO | None = None
self._last_flush = now()
[docs]
def disable(self) -> None:
"""Permanently disable this log so future appends are no-ops."""
with self._lock:
self._close_locked()
self._disabled = True
[docs]
def append(self, line: str) -> bool:
"""Write *line* to the overflow log.
Returns True when the line was written. Returns False when the log is
disabled, the byte cap has been reached, or an I/O error occurs.
"""
with self._lock:
if self._disabled:
return False
try:
text = line.rstrip("\n") + "\n"
encoded = text.encode("utf-8")
if self._bytes_written + len(encoded) > self._max_bytes:
self._close_locked()
self._disabled = True
return False
if self._fh is None:
self.path.parent.mkdir(parents=True, exist_ok=True)
mode = "wb" if self._first_write else "ab"
handle_obj: object = self.path.open(mode, buffering=_BUFFER_BYTES)
self._fh = cast("BinaryIO", handle_obj)
self._first_write = False
fh: BinaryIO | None = self._fh
if fh is None:
return False
fh.write(encoded)
self._bytes_written += len(encoded)
if self._now() - self._last_flush >= self._flush_interval:
fh.flush()
self._last_flush = self._now()
return True
except (OSError, PermissionError):
self._close_locked()
self._disabled = True
return False
[docs]
def flush(self) -> None:
"""Force buffered bytes to disk. Never raises."""
with self._lock:
if self._fh is not None:
try:
self._fh.flush()
self._last_flush = self._now()
except (OSError, PermissionError):
self._close_locked()
self._disabled = True
[docs]
def close(self) -> None:
"""Flush and release the file handle. Idempotent; appends may reopen."""
with self._lock:
self._close_locked()
def _close_locked(self) -> None:
if self._fh is not None:
with contextlib.suppress(OSError, PermissionError):
self._fh.close()
self._fh = None
[docs]
def relative_reference(self, workspace_root: Path) -> str:
"""Return POSIX path relative to *workspace_root*, or absolute on error."""
try:
return self.path.relative_to(workspace_root).as_posix()
except ValueError:
return self.path.as_posix()
@property
def size_bytes(self) -> int:
"""Bytes appended so far (buffered bytes included).
The idle watchdog's log-growth corroborator reads this to prove the
unit is alive; it must advance on every append, not only on flush.
Returns 0 before the first write. Never raises.
The in-memory ``_bytes_written`` counter is the authoritative
liveness signal — an on-disk ``stat()`` probe is intentionally
avoided because a missing or unfetchable file (operator unlink,
watcher quarantine, transient I/O error) must NOT silence the
watchdog while the unit itself is still appending.
"""
return self._bytes_written
@property
def is_disabled(self) -> bool:
"""True when the log has been permanently disabled (byte cap reached or I/O error)."""
return self._disabled
__all__ = [
"DEFAULT_FLUSH_INTERVAL_SECONDS",
"DEFAULT_MAX_OVERFLOW_FILE_BYTES",
"RawOverflowLog",
]