**Phase 187 — final verification pass: all criteria met, repo 100% green.**
Verified (all four tasks already in `complete/`)
- `app/core/memory.py`: `rss_mb` / `rss_peak_mb` / `cgroup_peak_mb` / `fmt_mb`, pure stdlib, never-raise, **100%** coverage, 24 branch tests.
- Lifecycle lines append-only: `sync: started … rss_mb=`, all three terminals `… rss_mb= rss_peak_mb=` (failed = sanitized one-liner beside the surviving `logger.exception`), `import: summary … rss_mb=`; prefixes byte-identical.
- `GET /api/sync/status`: additive `rss_mb`/`rss_peak_mb` (`int|null`), keyed off the ONE `_sample_rss` (run start, phase transitions, forced terminals, 5 s-throttled hook), `peak >= current` invariant, anonymous 403.
- `sources.js` `fmtSyncPeakClause`: ` · peak <N> MB` trailing the counts line / "Sync cancelled", nothing when null, no new control/live region.
Gates (exact commands)
- `uv run pytest --cov=app --cov-report=term-missing` → **4570 passed**, TOTAL **99%**, `app/api/sync.py` **100%**, `app/core/memory.py` **100%**
- `uv run ruff check .` → clean · `uv run pyright` → **0 errors**
- `uv run pytest tests/e2e/test_sync_peak_memory_visible.py -v --no-cov` → **5 passed in isolation** (ran twice)
- All 27 touched E2E families re-run **in isolation**, all green (progress_box 8, cancel 3, status_freshness 4, summary_visibility 3, sync_button 3, session_reset 4, git_freeze 2, git_log_stream_dates 5, PDF/image suites, …)
Completion criteria: every checkbox ✅ except the commit + phase-dir move, which belong to the harness.
Documentation
- README reviewed in full (pages, commands, config tables cross-checked against `app/config.py` — 85 env rows, all defaults match; phase-187 ops section, greps, process-vs-cgroup caveat): consistent, no changes needed.
- `AGENTS.md`: **fixed one pre-existing inconsistency** — rule 6 claimed the no-CDN test covers "the index page"; it actually parametrizes 10 pages (all but `/doc-edit.html`).
Notable
- Two failures early in the pass were self-inflicted by running the E2E module concurrently with the full suite on the shared Postgres (TRUNCATE deadlock + one order-dependent `test_overview` flake); both vanish under the documented serial isolation — no code defect, no change made.
- **PLAN §9 still needs the owner's reconciliation** (phase 187 adds the sync memory fields + two status keys; PLAN is never agent-edited — AGENTS rule 10 carries the spec). `TODO.md` holds an unrelated owner note, left untouched.
Next pending phase: `188_chunks_hnsw_index`.
156 lines
7.3 KiB
Python
156 lines
7.3 KiB
Python
"""Process memory as a number — current RSS, peak RSS, and a log formatter
|
||
(phase 187, task 01, D1, LOCKED A1).
|
||
|
||
**Pure stdlib:** this module imports the standard library only — no FastAPI,
|
||
no ``app.api``, no third-party package — mirroring ``app/core/response_budget.py``.
|
||
|
||
**The incident (owner, 2026-10-15).** Reconstructing "how much RAM did the
|
||
sync actually use?" meant reading ``systemd`` unit-teardown lines from three
|
||
boots (``brain-of-code-app.service: … 3.3G memory peak``), and boot −1 — the
|
||
run that killed the box — had no teardown record at all because the VM never
|
||
shut down cleanly. The app itself had no opinion about its own memory: no
|
||
log line, no status field, nothing to ``grep``. These helpers give the sync's
|
||
lifecycle lines (task 02) a number so the next incident starts from data
|
||
instead of archaeology.
|
||
|
||
**The helpers.**
|
||
|
||
* :func:`rss_mb` — the process's CURRENT resident set size in MB. Linux:
|
||
parse ``VmRSS`` from ``/proc/self/status`` (kB, floored to whole MB). On a
|
||
platform without ``/proc`` — or on any failure reading it — fall back to
|
||
``resource.getrusage(RUSAGE_SELF).ru_maxrss // 1024``, which is the
|
||
process HIGH-WATER mark, not a current value: the best available proxy.
|
||
(``ru_maxrss`` is kilobytes on Linux, the deploy target.)
|
||
* :func:`rss_peak_mb` — ``ru_maxrss // 1024``, the process high-water mark.
|
||
``None`` only where the ``resource`` module is unavailable (Windows); the
|
||
import is guarded so this module still loads there.
|
||
* :func:`cgroup_peak_mb` — ``/sys/fs/cgroup/memory.peak`` (cgroup v2, bytes,
|
||
floored to MB) when that file exists and parses, else ``None``. Read it
|
||
for context, never as the app's number — see the caveat below.
|
||
* :func:`fmt_mb` — ``"?"`` for ``None``, else ``str(v)``, so a log line
|
||
renders ``rss_mb=?`` and never ``rss_mb=None`` (task 02 single-sources
|
||
the rendering through this one function).
|
||
|
||
**The never-raise contract.** Every helper returns ``int`` or ``None``;
|
||
none may propagate an exception to a caller on the sync or request path.
|
||
Memory is diagnostics — a failed ``/proc`` read must never fail the turn,
|
||
the sync, or a status poll that asked for a number, so every read is
|
||
wrapped and every failure mode degrades to ``None`` (or to the proxy).
|
||
|
||
**The container/cgroup caveat (written ONCE, here, so no future reader
|
||
conflates the numbers).** Inside a container ``/sys/fs/cgroup/memory.peak``
|
||
is usually absent (the HOST reads it), and the host's number counts PAGE
|
||
CACHE too: the incident box's app cgroup showed ~2.16 GB of ``file``
|
||
against ~211 MB of ``anon`` while idle, and ``systemd`` reported
|
||
``3.3G memory peak`` for the unit while the uvicorn process was a fraction
|
||
of that. This module therefore reports PROCESS numbers (LOCKED A1); an
|
||
operator dashboard may legitimately show a larger cgroup number — neither
|
||
is wrong, and the log says which one it is.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import contextlib
|
||
from types import ModuleType
|
||
|
||
#: Procfs file carrying ``VmRSS`` (current RSS in kB) among the other
|
||
#: ``/proc/self`` identity fields. Module-level so tests can point it at a
|
||
#: stub file (or a nonexistent path) without patching ``open`` globally.
|
||
PROC_STATUS_PATH = "/proc/self/status"
|
||
|
||
#: cgroup v2 high-water mark, in BYTES. Usually absent inside a container —
|
||
#: see the module docstring's caveat; ``None`` is the normal answer there.
|
||
CGROUP_PEAK_PATH = "/sys/fs/cgroup/memory.peak"
|
||
|
||
#: ``resource`` is Unix-only. Guarded so the module loads (and every helper
|
||
#: answers ``None`` where it cannot know) on platforms without it.
|
||
_resource: ModuleType | None
|
||
try:
|
||
import resource as _resource
|
||
except ImportError:
|
||
_resource = None
|
||
|
||
#: The ``/proc/self/status`` label holding current RSS, in kB.
|
||
_VM_RSS_LABEL = "VmRSS:"
|
||
|
||
|
||
def rss_mb() -> int | None:
|
||
"""Current process RSS in whole MB, or ``None`` if unknowable.
|
||
|
||
Linux path: the ``VmRSS:`` line of ``/proc/self/status`` ("kB" is
|
||
implied by the kernel; the value is floored with integer division).
|
||
Any failure — no ``/proc`` (non-Linux), unreadable file, a file
|
||
without the line, a line whose value is not an integer — falls back
|
||
to :func:`rss_peak_mb`, the high-water proxy. ``KeyError`` is caught
|
||
defensively per the phase contract; the line-scanning form below
|
||
itself cannot raise it (the absent-line case is a fall-through).
|
||
"""
|
||
with contextlib.suppress(OSError, KeyError, ValueError, IndexError), open(
|
||
PROC_STATUS_PATH, encoding="utf-8"
|
||
) as fh:
|
||
for line in fh:
|
||
if line.startswith(_VM_RSS_LABEL):
|
||
return int(line.split()[1]) // 1024
|
||
return rss_peak_mb()
|
||
|
||
|
||
def rss_peak_mb() -> int | None:
|
||
"""Process high-water RSS in whole MB, or ``None`` without ``resource``.
|
||
|
||
``ru_maxrss`` is the high-water mark, never a current value (kilobytes
|
||
on Linux, the deploy target), floored to whole MB. The ``getrusage``
|
||
call itself is wrapped to honor the never-raise contract even though
|
||
``RUSAGE_SELF`` cannot realistically fail.
|
||
|
||
**One honest edge, measured here (so nobody "fixes" this helper):** the
|
||
kernel refreshes its high-water LAZILY and the value FLOORs to whole
|
||
MB, so a growing process can report a CURRENT ``VmRSS`` ABOVE its
|
||
reported ``ru_maxrss`` — observed on this repo's own suite at
|
||
``VmRSS`` 217,288 kB against ``ru_maxrss`` 216,832 kB, and again as
|
||
``rss_mb=164`` against ``rss_peak_mb=162``. This helper stays the RAW
|
||
kernel reading (it is a diagnostic, and a diagnostic that quietly
|
||
edits the OS is worse than the imprecision).
|
||
|
||
The CALLER that publishes the pair is what normalizes it:
|
||
:func:`app.api.sync._sample_rss` — the single sampling point behind
|
||
both sync lifecycle lines, ``GET /api/sync/status`` and the Sources
|
||
page's clause — lifts a peak that sits below the current reading up to
|
||
that reading, because a high-water BELOW the process's own current set
|
||
is not a smaller number but a false one. So the RUN's numbers carry
|
||
``peak >= current``, while a raw ``rss_peak_mb()`` call may still sit
|
||
under a raw ``rss_mb()``.
|
||
"""
|
||
if _resource is None:
|
||
return None
|
||
try:
|
||
return int(_resource.getrusage(_resource.RUSAGE_SELF).ru_maxrss) // 1024
|
||
except (OSError, AttributeError, TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def cgroup_peak_mb() -> int | None:
|
||
"""The cgroup v2 ``memory.peak`` in whole MB, or ``None``.
|
||
|
||
Context for operators, NOT the app's number: inside a container this
|
||
file is usually absent (the host reads it), and the host's number
|
||
counts PAGE CACHE too (~2.16 GB ``file`` against ~211 MB ``anon`` on
|
||
the idle incident box; ``systemd`` said ``3.3G memory peak`` for the
|
||
unit while the uvicorn process was a fraction of that). PROCESS
|
||
numbers are what the sync logs — module docstring has the full
|
||
caveat, LOCKED A1.
|
||
"""
|
||
try:
|
||
with open(CGROUP_PEAK_PATH, encoding="utf-8") as fh:
|
||
return int(fh.read().strip()) // (1024 * 1024)
|
||
except (OSError, ValueError):
|
||
return None
|
||
|
||
|
||
def fmt_mb(value: int | None) -> str:
|
||
"""Render an MB reading for a log line: ``?`` when unknown, digits when not.
|
||
|
||
``0`` is a VALUE, not missing — it renders ``"0"``. This is the single
|
||
place the ``rss_mb=?`` convention lives (task 02 formats every line
|
||
through here).
|
||
"""
|
||
return "?" if value is None else str(value)
|