fix(backend): add semaphore to executor cost log tasks; fix type annotation

- Add `_log_semaphore = asyncio.Semaphore(50)` to cost_tracking.py to bound
  concurrent DB inserts (mirrors platform_cost.py's existing semaphore)
- Narrow `_extract_model_name` param type from `Any` to `str | dict | None`
- Add `test_get_dashboard_cache_hit` to verify TTL cache deduplicates DB calls
- Add `scope="col"` to all table `<th>` elements for screen-reader accessibility
- Add `(local time)` labels to date filter inputs to clarify timezone behaviour
This commit is contained in:
Zamil Majdy
2026-04-07 18:15:57 +07:00
parent 7c685c6677
commit 630d6d4705
6 changed files with 106 additions and 35 deletions

View File

@@ -168,3 +168,25 @@ def test_get_dashboard_invalid_date_format() -> None:
"""Malformed start date must be rejected with 422."""
response = client.get("/platform-costs/dashboard", params={"start": "not-a-date"})
assert response.status_code == 422
def test_get_dashboard_cache_hit(
mocker: pytest_mock.MockerFixture,
) -> None:
"""Second identical request returns cached result without calling the DB again."""
real_dashboard = PlatformCostDashboard(
by_provider=[],
by_user=[],
total_cost_microdollars=42,
total_requests=1,
total_users=1,
)
mock_fn = mocker.patch(
"backend.api.features.admin.platform_cost_routes.get_platform_cost_dashboard",
AsyncMock(return_value=real_dashboard),
)
client.get("/platform-costs/dashboard")
client.get("/platform-costs/dashboard")
mock_fn.assert_awaited_once() # second request hit the cache

View File

@@ -34,6 +34,8 @@ _WALLTIME_BILLED_PROVIDERS = frozenset(
# Hold strong references to in-flight log tasks so the event loop doesn't
# garbage-collect them mid-execution. Tasks remove themselves on completion.
_pending_log_tasks: set[asyncio.Task] = set()
# Bound concurrent DB inserts to avoid unbounded queue growth under load.
_log_semaphore = asyncio.Semaphore(50)
async def drain_pending_cost_logs(timeout: float = 5.0) -> None:
@@ -79,26 +81,27 @@ def _schedule_log(
db_client: "DatabaseManagerAsyncClient", entry: PlatformCostEntry
) -> None:
async def _safe_log() -> None:
try:
await db_client.log_platform_cost(entry)
except Exception:
logger.exception(
"Failed to log platform cost for user=%s provider=%s block=%s",
entry.user_id,
entry.provider,
entry.block_name,
)
async with _log_semaphore:
try:
await db_client.log_platform_cost(entry)
except Exception:
logger.exception(
"Failed to log platform cost for user=%s provider=%s block=%s",
entry.user_id,
entry.provider,
entry.block_name,
)
task = asyncio.create_task(_safe_log())
_pending_log_tasks.add(task)
task.add_done_callback(_pending_log_tasks.discard)
def _extract_model_name(raw: Any) -> str | None:
def _extract_model_name(raw: str | dict | None) -> str | None:
"""Return a string model name from a block input field, or None.
Handles str (returned as-is), dict (e.g. an enum wrapper, skipped), and
any other type (coerced to str as a best-effort fallback).
None (no model field). Unexpected types are coerced to str as a fallback.
"""
if raw is None:
return None

View File

@@ -23,16 +23,36 @@ function LogsTable({ logs, pagination, onPageChange }: Props) {
<table className="w-full text-left text-sm">
<thead className="border-b text-xs uppercase text-muted-foreground">
<tr>
<th className="px-3 py-3">Time</th>
<th className="px-3 py-3">User</th>
<th className="px-3 py-3">Block</th>
<th className="px-3 py-3">Provider</th>
<th className="px-3 py-3">Type</th>
<th className="px-3 py-3">Model</th>
<th className="px-3 py-3 text-right">Cost</th>
<th className="px-3 py-3 text-right">Tokens</th>
<th className="px-3 py-3 text-right">Duration</th>
<th className="px-3 py-3">Session</th>
<th scope="col" className="px-3 py-3">
Time
</th>
<th scope="col" className="px-3 py-3">
User
</th>
<th scope="col" className="px-3 py-3">
Block
</th>
<th scope="col" className="px-3 py-3">
Provider
</th>
<th scope="col" className="px-3 py-3">
Type
</th>
<th scope="col" className="px-3 py-3">
Model
</th>
<th scope="col" className="px-3 py-3 text-right">
Cost
</th>
<th scope="col" className="px-3 py-3 text-right">
Tokens
</th>
<th scope="col" className="px-3 py-3 text-right">
Duration
</th>
<th scope="col" className="px-3 py-3">
Session
</th>
</tr>
</thead>
<tbody>

View File

@@ -48,7 +48,7 @@ function PlatformCostContent({ searchParams }: Props) {
<div className="flex flex-wrap items-end gap-3 rounded-lg border p-4">
<div className="flex flex-col gap-1">
<label htmlFor="start-date" className="text-sm text-muted-foreground">
Start Date
Start Date <span className="text-xs">(local time)</span>
</label>
<input
id="start-date"
@@ -60,7 +60,7 @@ function PlatformCostContent({ searchParams }: Props) {
</div>
<div className="flex flex-col gap-1">
<label htmlFor="end-date" className="text-sm text-muted-foreground">
End Date
End Date <span className="text-xs">(local time)</span>
</label>
<input
id="end-date"

View File

@@ -21,13 +21,29 @@ function ProviderTable({ data, rateOverrides, onRateOverride }: Props) {
<table className="w-full text-left text-sm">
<thead className="border-b text-xs uppercase text-muted-foreground">
<tr>
<th className="px-4 py-3">Provider</th>
<th className="px-4 py-3">Type</th>
<th className="px-4 py-3 text-right">Usage</th>
<th className="px-4 py-3 text-right">Requests</th>
<th className="px-4 py-3 text-right">Known Cost</th>
<th className="px-4 py-3 text-right">Est. Cost</th>
<th className="px-4 py-3 text-right" title="Per-session only">
<th scope="col" className="px-4 py-3">
Provider
</th>
<th scope="col" className="px-4 py-3">
Type
</th>
<th scope="col" className="px-4 py-3 text-right">
Usage
</th>
<th scope="col" className="px-4 py-3 text-right">
Requests
</th>
<th scope="col" className="px-4 py-3 text-right">
Known Cost
</th>
<th scope="col" className="px-4 py-3 text-right">
Est. Cost
</th>
<th
scope="col"
className="px-4 py-3 text-right"
title="Per-session only"
>
Rate <span className="text-[10px] font-normal">(unsaved)</span>
</th>
</tr>

View File

@@ -11,11 +11,21 @@ function UserTable({ data }: Props) {
<table className="w-full text-left text-sm">
<thead className="border-b text-xs uppercase text-muted-foreground">
<tr>
<th className="px-4 py-3">User</th>
<th className="px-4 py-3 text-right">Known Cost</th>
<th className="px-4 py-3 text-right">Requests</th>
<th className="px-4 py-3 text-right">Input Tokens</th>
<th className="px-4 py-3 text-right">Output Tokens</th>
<th scope="col" className="px-4 py-3">
User
</th>
<th scope="col" className="px-4 py-3 text-right">
Known Cost
</th>
<th scope="col" className="px-4 py-3 text-right">
Requests
</th>
<th scope="col" className="px-4 py-3 text-right">
Input Tokens
</th>
<th scope="col" className="px-4 py-3 text-right">
Output Tokens
</th>
</tr>
</thead>
<tbody>