feat(snippets): pull-time freshness — the forge confirms the cache at the moment it's trusted (#2690)
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / integration (push) Successful in 26s
CI & Build / Python tests (push) Failing after 43s
CI & Build / Build & push image (push) Skipped
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / integration (push) Successful in 26s
CI & Build / Python tests (push) Failing after 43s
CI & Build / Build & push image (push) Skipped
First consumer of the forge adapter. attach_live_body decorates both pull surfaces (MCP get_snippet, REST detail) with body_source + body_freshness when the instance has a forge: 'current' means the cached code was just found verbatim (whitespace-normalized, the same normalization the verdict hash uses) in the fetched file, and provenance restamps to the file's last commit — reflected in the response and persisted in the background. A snippet body is a FRAGMENT of its file, so a fetch can honestly CONFIRM the cache or report divergence, never clobber the record with the whole file: 'diverged' is the reader's information, and a 404 stamps the mechanically-true 'missing' verdict into the existing attention state — once, not on every pull of an already-flagged record. The probe never raises and never blocks past 2.5s (tighter than the adapter's own timeout — the pull is where a session decides whether pulling is worth it, #2663's finding); a hung forge costs bounded time and the cache serves. A no-forge instance's response stays byte-identical to today's (rule #115 baseline, pinned by test). services/background.py is the new one home for fire-and-forget tasks with strong references (the #2663 GC footgun) — telemetry's two copies predate it and keep their bespoke canaries; new callers use this. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"""Fire-and-forget background tasks that actually run.
|
||||
|
||||
The event loop holds only a WEAK reference to a task, so a bare
|
||||
``create_task`` with no other holder can be garbage-collected mid-flight — a
|
||||
write that never errors and never lands (the #2663 GC footgun). This module is
|
||||
the one place that gets the pattern right: strong references in ``_pending``,
|
||||
discarded on completion, with failures logged at WARNING instead of vanishing.
|
||||
|
||||
``note_usage`` and ``retrieval_telemetry`` predate this module and carry their
|
||||
own copies with bespoke canary semantics; new fire-and-forget callers use this
|
||||
instead of writing a fourth copy.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Coroutine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_pending: set[asyncio.Task] = set()
|
||||
|
||||
|
||||
def spawn(coro: Coroutine, *, site: str) -> None:
|
||||
"""Schedule ``coro`` fire-and-forget; ``site`` names it in failure logs.
|
||||
|
||||
No running loop (sync context outside the app) closes the coroutine and
|
||||
skips — every app path runs on the loop, and blocking would be worse.
|
||||
"""
|
||||
try:
|
||||
task = asyncio.get_running_loop().create_task(coro)
|
||||
except RuntimeError:
|
||||
coro.close()
|
||||
logger.debug("background task %s skipped — no running event loop", site)
|
||||
return
|
||||
_pending.add(task)
|
||||
|
||||
def _done(t: asyncio.Task) -> None:
|
||||
_pending.discard(t)
|
||||
if not t.cancelled() and t.exception() is not None:
|
||||
logger.warning(
|
||||
"background task %s failed", site, exc_info=t.exception()
|
||||
)
|
||||
|
||||
task.add_done_callback(_done)
|
||||
|
||||
|
||||
async def drain() -> None:
|
||||
"""Await everything in flight — for tests that need the writes landed."""
|
||||
while _pending:
|
||||
await asyncio.gather(*list(_pending), return_exceptions=True)
|
||||
Reference in New Issue
Block a user