Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/server/management/usage-aggregate-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const pinnedAggregates = new Set<RetainedUsageAggregate>();
let baseFlight: Promise<UsageAggregateResult> | null = null;
const filteredFlights = new Map<string, Promise<UsageAggregateResult>>();
const retainedFilteredAggregates = new Map<string, RetainedUsageAggregate>();
const MAX_CONCURRENT_FILTERED_AGGREGATES = 4;

function currentTimeZone(): string {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
Expand Down Expand Up @@ -283,6 +284,9 @@ export async function getFilteredUsageAggregate(filter: {
]);
const existing = filteredFlights.get(key);
if (existing) return existing;
if (filteredFlights.size >= MAX_CONCURRENT_FILTERED_AGGREGATES) {
throw new Error("too many concurrent filtered usage aggregates");
Comment on lines +287 to +288

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface scan saturation as a request failure

When four distinct scans are active, a filtered management /api/usage request reaches this throw, but logs-usage-routes.ts catches it and returns HTTP 200 with zero totals and error: "read_failed". Consumers such as gui/src/pages/Usage.tsx and src/cli/observe.ts accept successful responses without checking that field, so temporary saturation is displayed as genuine zero usage. Translate this overload condition to a non-success response, or otherwise ensure clients reject the synthetic summary.

Useful? React with 👍 / 👎.

Comment on lines +287 to +288

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check retained aggregates before enforcing the scan cap

When four other keys are scanning, this guard also rejects a request whose own key already has a current entry in retainedFilteredAggregates, because the unchanged-cache check does not happen until refreshFilteredAggregate. That request would perform no ledger scan, yet four slow callers can deny the otherwise available cached report; enforce the limit only on the rebuild/append path that actually invokes the scanner. The changed structure contract specifically describes this as a bound on concurrent filtered scans.

AGENTS.md reference: structure/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

}

const flight = refreshFilteredAggregate(key, normalizedFilter, fixedWindow);
filteredFlights.set(key, flight);
Expand Down
8 changes: 4 additions & 4 deletions structure/gui-and-management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,10 @@ keeps the saved state and renders fixed `ocx sync` guidance without server/accou

`src/server/hub-usage.ts` serves `GET /v1/usage` on hubs for an explicit configured data key. The authenticated key selects the aggregate; query parameters cannot select an API-key identity. Unscoped environment/admin credentials and loopback bypass are not admitted. The response projects only this client's numeric totals, provider/model/day rows and incomplete-history metadata through `src/remote/hub-usage.ts`; accounts, raw records and key IDs are omitted. Unknown fields are stripped at every object boundary and the serialized body is capped at 1 MiB.

Custom usage windows are immutable bounds on the streaming accumulator, applied to each
ledger entry before attribution and daily aggregation. The filtered aggregate cache includes
both inclusive millisecond bounds in its identity and retains the existing ledger revision,
overlay-version and timezone checks. Preset warming never consumes custom summaries.
Custom usage windows are immutable bounds on the streaming accumulator, applied before attribution and daily
aggregation. The filtered cache includes both inclusive millisecond bounds in its identity and retains the existing ledger revision, overlay-version and timezone checks.
At most four distinct filtered scans may run concurrently; identical requests share one scan and excess distinct
work fails closed. Preset warming never consumes custom summaries.
The response retains its preset range discriminator for compatibility and explicitly marks
`customWindow`, `since`, and `until`; the chart uses the window's local calendar days with
the existing 366-day cap. GUI custom reports bypass the held preset/session cache.
Expand Down
30 changes: 30 additions & 0 deletions tests/usage/usage-aggregate-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,36 @@ describe("retained usage aggregate cache", () => {
}
});

test("distinct filtered scans have bounded concurrency while identical callers share a flight", async () => {
writeFileSync(join(testDir, "usage.jsonl"), line("one"));
const originalScan = usageLedgerScannerModule.scanUsageLedgerCooperatively;
let releaseScans!: () => void;
const scansBlocked = new Promise<void>(resolve => { releaseScans = resolve; });
const scanSpy = spyOn(usageLedgerScannerModule, "scanUsageLedgerCooperatively")
.mockImplementation(async options => {
await scansBlocked;
return originalScan(options);
});
try {
const flights = Array.from({ length: 4 }, (_, index) =>
getFilteredUsageAggregate({ provider: `provider-${index}` }));
await Bun.sleep(0);
expect(scanSpy).toHaveBeenCalledTimes(4);

const shared = getFilteredUsageAggregate({ provider: "provider-0" });
await expect(getFilteredUsageAggregate({ provider: "provider-4" }))
.rejects.toThrow("too many concurrent filtered usage aggregates");
expect(scanSpy).toHaveBeenCalledTimes(4);

releaseScans();
const results = await Promise.all([...flights, shared]);
expect(results[4]!.accumulator).toBe(results[0]!.accumulator);
} finally {
releaseScans();
scanSpy.mockRestore();
}
});

test("filtered retention invalidates when pricing inputs change", async () => {
writeFileSync(join(testDir, "usage.jsonl"), line("one"));
const originalScan = usageLedgerScannerModule.scanUsageLedgerCooperatively;
Expand Down
Loading