refactor(usage): address round-2 review on PR #871

- remove accidentally-committed .claude/scheduled_tasks.lock (Claude Code
  runtime artifact swept in by `git add -A`) and gitignore it; the rest
  of .claude/ stays tracked.
- store: cache UsageEventStore.shared() as a process-wide instance so the
  hot search path doesn't allocate a fresh wrapper per metered query (the
  wrapper is stateless beyond its storage handle).
- hooks: pass enabled=True directly (the outer guard already confirmed
  the flag) instead of re-reading settings.usage_metering_enabled.
- migration: document the no-TTL retention design (control-plane rollup
  owns the lifecycle; the data plane only appends).
- tests: assert the best-effort error path logs at WARNING (observability
  contract).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-07 15:28:24 +02:00
co-authored by Claude Opus 4.8
parent 702f66e6b1
commit 2bbf4ed967
7 changed files with 38 additions and 14 deletions
@@ -34,6 +34,9 @@ depends_on = None
def upgrade() -> None:
is_pg = op.get_bind().dialect.name == "postgresql"
# Retention: this table has no TTL by design — the control-plane rollup
# owns the lifecycle (it pulls rows read-only into usage_daily, then
# prunes once a day is reconciled). The data plane only appends.
op.create_table(
"usage_events",
# Pod-generated idempotency key. UUID on Postgres; TEXT on SQLite,
+5 -3
View File
@@ -534,9 +534,11 @@ def configure_semantic_tools(mcp: FastMCP):
"fusion": fusion,
"doc_types": doc_types,
},
# Pass the already-resolved flag so the store doesn't
# rebuild Settings on this hot query path (ADR-024).
enabled=settings.usage_metering_enabled,
# The outer guard already confirmed the flag, so pass
# enabled=True directly — the store then skips a second
# uncached Settings build on this hot query path
# (ADR-024).
enabled=True,
)
except Exception:
# Reached only when shared()/store construction itself
+14 -5
View File
@@ -51,18 +51,27 @@ _INSERT_SQL = (
class UsageEventStore:
"""Append-only writer for the app-DB ``usage_events`` table."""
# Process-wide cached instance returned by ``shared()`` so the hot search
# path doesn't allocate a fresh wrapper per metered query. The store is
# stateless beyond its storage handle, so one instance is reusable.
_shared_instance: "UsageEventStore | None" = None
def __init__(self, storage: RefreshTokenStorage) -> None:
self._storage = storage
@classmethod
async def shared(cls) -> "UsageEventStore":
"""Build a store backed by the process-wide storage singleton.
"""Return the process-wide store backed by the storage singleton.
``get_shared_storage()`` runs ``initialize()`` (and thus Alembic
migrations) on first access, so the ``usage_events`` table is present
by the time any event is recorded.
Cached after first build: ``get_shared_storage()`` already returns the
cached :class:`RefreshTokenStorage` (running ``initialize()`` / Alembic
on first access, so ``usage_events`` exists), and the wrapper itself is
stateless, so reusing one instance avoids a per-call allocation on the
``nc_semantic_search`` hot path.
"""
return cls(await get_shared_storage())
if cls._shared_instance is None:
cls._shared_instance = cls(await get_shared_storage())
return cls._shared_instance
async def record_usage_event(
self,
+4 -3
View File
@@ -839,9 +839,10 @@ async def _index_document(
"user_id": doc_task.user_id,
"total_chars": total_chars,
},
# Pass the already-resolved flag so the store doesn't
# rebuild Settings here (ADR-024).
enabled=settings.usage_metering_enabled,
# The outer guard already confirmed the flag, so pass
# enabled=True directly — the store then skips a second
# uncached Settings build here (ADR-024).
enabled=True,
)
except Exception:
# Reached only when shared()/store construction itself