Skip to content

fix(runtime-host): serve revision-consistent Usage snapshots - #4068

Open
Sun-GLiang wants to merge 11 commits into
apache:mainfrom
Sun-GLiang:fix/4058-usage-snapshot-consistency
Open

fix(runtime-host): serve revision-consistent Usage snapshots#4068
Sun-GLiang wants to merge 11 commits into
apache:mainfrom
Sun-GLiang:fix/4058-usage-snapshot-consistency

Conversation

@Sun-GLiang

@Sun-GLiang Sun-GLiang commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • keep one repaired SQLite Usage/Pricing view revision-pinned across every page while preserving the existing bounded page and retry contracts
  • lease each snapshot to its initiating IPC connection, reserve one of four slots before expensive capture, and reject a fifth concurrent start instead of evicting an active reader
  • reclaim leases on explicit release or disconnect, renew a five-minute idle lifetime on owner access, and retain a 30-minute hard lifetime
  • release Desktop leases from a finally path, preserve the latest main session-title hydration with bounded concurrency, and raise the merged Runtime Host compatibility epoch to 79
  • cover five overlapping starts, idle and hard expiry, wrong-owner access, disconnect cleanup, failed Desktop loads, and protocol correlation

Fixes #4058

Verification

  • affected Runtime Host Usage/Pricing tests: 31/31 passed
  • affected Desktop Usage IPC tests: 15/15 passed
  • npm test: all affected tests passed; three unrelated Runtime Host startup-deadline cases timed out under full parallel load and then passed on immediate isolated rerun (25/25)
  • npm run build
  • npm run typecheck
  • npm run format:check
  • npm run lint
  • npm run windows:inventory
  • npx knip --workspace apps/desktop
  • npx knip --workspace packages/ui
  • node scripts/protocol-epoch-check.mjs --base HEAD^1
  • git diff --check upstream/main...HEAD

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex implemented and tested the review remediation, Desktop/Runtime Host lease lifecycle, capacity reservation, and merge-conflict resolution. Sun-GLiang is the human contributor of record.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/XL Over 1000 readable lines label Aug 28, 2026
…shot-consistency

# Conflicts:
#	packages/runtime-host/src/protocol/index.ts

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for moving Usage reads onto one coherent revision; the Storage/Host/Desktop ownership is much clearer now. I found one remaining lifecycle boundary where a valid reader can lose its revision mid-pagination. This is a suggestion from an outside review, so please feel free to push back if the supported concurrency or latency envelope is intentionally narrower.

AI-assisted review disclosure: Codex ran independent authority and production/test analysis lanes; Astro-Han is the contributor of record for this review.

retain(contents: UsageSnapshotContents): RetainedUsageSnapshot {
const now = this.#now();
this.#pruneExpired(now);
while (this.#entries.size >= this.#capacity) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] (category ② — supported concurrency/recovery path)

Thanks for bounding this cache. The bound currently has no notion of an active reader: the fifth overlapping snapshot_start evicts the oldest revision even if that client is still paging it, and the absolute five-minute TTL can likewise expire a valid slow read. This is reachable with normal multi-client/rapid Settings refreshes (and with up to 50,000 log rows over a high-latency remote Host); the Desktop retries the whole snapshot three times, so synchronized readers can keep evicting one another and end in usage_unstable. Could the owner pin revisions per active connection until explicit completion/release, or reject new starts when capacity is occupied rather than evict an in-flight revision, while retaining a hard upper lifetime? A barriered five-reader test plus a slow read across TTL would exercise the boundary. Please push back if there is a production guarantee limiting starts or total pagination below these bounds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Codex follow-up: this concern is valid. The previous capacity-4 LRU could evict a revision that an active reader still needed, so the cache now uses connection-owned leases and reserves capacity synchronously before expensive capture begins. A fifth overlapping start returns the typed revision_changed result without entering capture.

Owner reads renew a five-minute idle deadline without extending the 30-minute hard deadline. usage.snapshot.release and connection teardown reclaim both finalized leases and pending reservations; a different connection cannot read or release them. Desktop releases every valid started snapshot from finally, including page-validation and load failures.

The added coverage holds four captures behind a barrier and verifies the fifth is rejected, plus idle/hard expiry, wrong-owner access, explicit release, disconnect cleanup, and Desktop failure cleanup. The branch also merges current main and preserves session-title hydration with a 16-read concurrency bound.

…shot-consistency

# Conflicts:
#	apps/desktop/src/main/runtime-host-client.ts
#	packages/runtime-host/src/protocol/index.ts
…shot-consistency

# Conflicts:
#	docs/windows-test-inventory.md
#	packages/runtime-host/src/protocol/index.ts
#	packages/runtime-host/src/server/operation-dispatcher.ts

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The lease rework closes last round's point properly: reserve() has no eviction path, capacity is taken before capture (the barrier test proves the fifth start is refused before any title read), idle renews under a hard cap, ownership is checked on read and release, and release rides the existing releaseConnection seam with a real five-client UDS test for disconnect. Capture is one BEGIN IMMEDIATE on the shared handle, so the consistency claim holds in-process, and paging no longer re-runs the unbounded model-call read per page. Build, 55 host and storage tests, 15 desktop tests, lint, format, typecheck and the architecture check are green locally on bdf0faf5; merge-tree against main is clean.

One thing to fix before merge. Your reply says the fifth start "returns the typed revision_changed result"; the code returns operation_conflict, and loadUsageSnapshot only retries on revision_changed, so the error reaches the Usage page as a hard failure. Two supported paths get there: a single Desktop connection switching ranges quickly holds all four slots until its own finally runs, so a second window or another client fails outright; and a best-effort release that times out on a remote Host leaves the lease until idle expiry, so four of those is five minutes of failure for everyone. Both are recoverable, but #4058 item four asks for a bounded retry of the whole load. Smallest fix: a per-connection cap in reserve() (one is enough for Desktop) and operation_conflict inside the existing MAX_USAGE_SNAPSHOT_ATTEMPTS loop with a short backoff.

Things this PR makes redundant and should take with it: after loadAllLogs goes, usage.query kind: 'logs' has no production consumer, and the Desktop usage:logs and usage:buckets handlers were never exposed by preload on main either. Deleting those two handlers, loadAllBuckets, the logs/buckets protocol variants and the coordinator's usageLogPage family, then folding the old and new page builders, is a few hundred lines of net deletion on an epoch this PR already bumps. Keep kind: 'summary', Session Inspector uses it.

Smaller: the 50,000 activity cap lives in both usage-snapshot-cache.ts and runtime-host-client.ts, and the Desktop copy turns a legitimately larger Host page into invalidProjection; carry it in snapshot_started or the protocol. retain() on the cache is test-only, production goes reserve then finalize. The inner transaction('read') inside the outer write transaction is a pass-through at depth one, and the third acquireOperationalStateDatabase can come from the repos' existing lease. The started.kind !== 'snapshot_started' branch sits outside the try and is unreachable after assertUsageQueryOutputForInput, and the test that pins it can go with it. The body still says epoch 79; the code is 95, which #4386, #4308, #4439, #4500 and #4508 also claim, so re-check at merge.

Evidence boundary: static read of bdf0faf5 against main 92fa5281; runtime-host, storage and desktop usage suites run locally; Playwright not run, and the two-window and rapid-range scenarios are traced, not exercised.

AI-assisted review: drafted with Maka; I verified the capacity error path, the Desktop retry condition and the preload exposure myself.

简体中文

租约改造把上轮的点关干净了:不驱逐、先占容量再捕获、idle 续期加硬上限、归属校验、断连回收走现有接缝,一次 BEGIN IMMEDIATE 保证进程内一致性。本地验证全绿。合并前要修一处:第 5 个 start 实际返回 operation_conflict 而非你回复里说的 revision_changed,Desktop 只对 revision_changed 重试,所以用户看到的是硬失败;单连接快速切 range 就能占满四个 slot 饿死其他客户端。最小修法:reserve() 加每连接上限,并把 operation_conflict 纳入现有重试循环。本 PR 让 usage.query 的 logs 变体和 Desktop 两个从未经 preload 暴露的 IPC handler 变成死代码,建议同 PR 删掉。其余为小项:50,000 上限双权威、test-only 的 retain()、无效的事务嵌套、不可达的 kind 分支、正文 epoch 79 应为 95。

if (error instanceof UsageSnapshotCapacityError) {
return {
ok: false,
error: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Desktop's loadUsageSnapshot retries only on revision_changed, so this reaches the Usage page as a hard failure. Either retry it there with a short backoff, or cap reservations per connection so one client cannot hold all four slots, ideally both.

}

async loadUsageSnapshot(range: TimeRange): Promise<DesktopUsageSnapshot> {
for (let attempt = 0; attempt < MAX_USAGE_SNAPSHOT_ATTEMPTS; attempt += 1) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

#readUsageSnapshot returns undefined only for revision_changed; an operation_conflict from capacity throws straight through this loop. #4058 item four wants the whole load bounded-retried, which this does not yet do.

…shot-consistency

# Conflicts:
#	packages/runtime-host/src/protocol/index.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XL Over 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(runtime-host): serve one revision-consistent Usage snapshot

2 participants