mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 20:53:15 +00:00
* feat: out-of-process usage-snapshot builder (Phase 1) Move all usage-tab compute off the run-serving HTTP process (the #1434 bug class). Read path is now a pure cache read with a typed computing placeholder on cold miss; a dedicated usage_snapshot graph + global ~10min cron rebuild every period's snapshot out-of-process, wrapped in asyncio.timeout and gated by USAGE_SNAPSHOT_CRON_ENABLED. Lifespan makes one fire-and-forget scheduling call, never a retained/looping task. * fix: address PR review on usage-snapshot builder - admin guard on /admin/usage/rebuild (was any logged-in user) - cap lifespan loopback calls with asyncio.timeout(5) so a startup hang can't block boot - memoize cron registration so steady-state requests skip the loopback check; reap duplicate crons from concurrent-replica races - thread the computing flag through the usage payload too
75 lines
2.7 KiB
Python
75 lines
2.7 KiB
Python
"""Out-of-process builder graph for the usage-tab snapshots.
|
|
|
|
A single pure-python node (no LLM, no sandbox) that rebuilds every period's
|
|
usage leaderboard + reviewer-stats snapshot and writes them to the cache
|
|
namespaces. Scheduled by a global cron (see ``dashboard/usage_snapshot_cron.py``)
|
|
so heavy/looping work never runs on the run-serving HTTP process (the #1434 bug
|
|
class). The whole build is bounded by ``asyncio.timeout`` so a wedged build
|
|
self-cancels before the next tick.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from typing import Any, TypedDict
|
|
|
|
from langgraph.graph import END, START, StateGraph
|
|
from langgraph.graph.state import RunnableConfig
|
|
|
|
from .dashboard.agent_usage import (
|
|
refresh_reviewer_stats_cache,
|
|
refresh_usage_leaderboard_cache,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_PERIODS = ("7d", "30d", "all")
|
|
_BUILD_TIMEOUT_S = 120
|
|
|
|
|
|
class UsageSnapshotState(TypedDict, total=False):
|
|
result: dict[str, Any]
|
|
|
|
|
|
def _cron_enabled() -> bool:
|
|
value = os.environ.get("USAGE_SNAPSHOT_CRON_ENABLED", "true").strip().lower()
|
|
return value not in {"0", "false", "no", "off"}
|
|
|
|
|
|
async def _build(state: UsageSnapshotState, config: RunnableConfig) -> dict[str, Any]:
|
|
if not _cron_enabled():
|
|
logger.info("Usage snapshot build skipped: USAGE_SNAPSHOT_CRON_ENABLED is off")
|
|
return {"result": {"status": "disabled"}}
|
|
|
|
built: list[str] = []
|
|
failed: list[str] = []
|
|
try:
|
|
async with asyncio.timeout(_BUILD_TIMEOUT_S):
|
|
for period in _PERIODS:
|
|
for label, refresh in (
|
|
("usage", refresh_usage_leaderboard_cache),
|
|
("reviewer", refresh_reviewer_stats_cache),
|
|
):
|
|
try:
|
|
await refresh(period)
|
|
built.append(f"{label}:{period}")
|
|
except Exception:
|
|
logger.exception("Usage snapshot build failed for %s:%s", label, period)
|
|
failed.append(f"{label}:{period}")
|
|
except TimeoutError:
|
|
logger.warning("Usage snapshot build exceeded %ss budget; cancelled", _BUILD_TIMEOUT_S)
|
|
return {"result": {"status": "timeout", "built": built, "failed": failed}}
|
|
|
|
status = "ok" if not failed else "partial"
|
|
logger.info("Usage snapshot build %s: built=%s failed=%s", status, built, failed)
|
|
return {"result": {"status": status, "built": built, "failed": failed}}
|
|
|
|
|
|
def get_usage_snapshot(config: RunnableConfig | None = None):
|
|
builder = StateGraph(UsageSnapshotState)
|
|
builder.add_node("build", _build)
|
|
builder.add_edge(START, "build")
|
|
builder.add_edge("build", END)
|
|
return builder.compile().with_config(config or {})
|