fix(themes): move the theme-detail target stats off the event loop (#5963) - #6190
Conversation
…5963) The api_theme_detail handler still made seven inline exists()/is_dir() call expressions across six condition sites on its two target paths. Cheap on local disk, but on a UNC data home each stat is SMB-backed and can block for as long as the network takes -- and a blocked event loop stalls every other request the gateway serves. These sites became reachable on Windows when #5943 removed the theme-pack 501 gate and offloaded the asset routes' resolver; this pays down the debt it tracked for the detail route. All the stats now ride one discovery_executor() hop as a single synchronous helper -- every method branch consumes the pair as one logical check, so one hop replaces six -- and the DELETE-dir branch keeps its off-loop re-check under the per-slug install lock. The DELETE-file unlink gains missing_ok=True: the stat now rides an earlier hop, so a concurrent delete winning the race is answered as the idempotent ok it is rather than an escaping FileNotFoundError. A mutation-verified test spies on Path.exists/Path.is_dir for the handler's target paths across GET/PUT/DELETE and fails if any such stat runs on the loop thread. Closes #5963
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Real event-loop stall, fixed at the right layer with the same off-loop discipline #5943 established, TOCTOU races handled (missing_ok, locked re-check). [DESIGN-REVIEWED] 0d9f5dc |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of The review contract is a first-principles gate: inventory the change, test each item for necessity, duplication, and root-cause depth. I've read the intent file, the patch, and the surrounding handler; the one thing worth surfacing is a counted, unfixed sibling of the same root cause in the same file. First-Principles-Verdict: CONCERNS The fix itself is derived and minimal, but the same file still runs an on-loop What this change shipsIntent: stop a theme-detail request from freezing the whole gateway when the data home is on a network share — a FIX.
No new public surface, config keys, or exported symbols; the change reuses the existing Watch
[FIRST-PRINCIPLES-REVIEWED] 0d9f5dc |
NicholasRBowers
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix with clear root cause — theme-detail handler stats (exists/is_dir) moved off the event loop onto the discovery executor, matching the asset routes' established off-loop discipline; handler + regression test only.
|
🤖 Kiro Crew Auto-Pipeline [operator: chenmingwei23#de330d0c]: Disposition for the First Principles CONCERNS (advisory, head 0d9f5dc) -- accepted and deferred. Verified in live code: |
What is the problem?
api_theme_detail(GET/PUT/DELETE/api/themes/{slug}) still makes seven inlineexists()/is_dir()call expressions across six condition sites on the asyncio event loop. On a local disk these are microseconds; on a UNC data home (KIROCREW_HOMEon an SMB share) every stat is a network round trip that can block for as long as the network takes. These sites became reachable on Windows when #5943 removed the theme-pack 501 gate; #5943 offloaded the asset routes' resolver and tracked the detail route's remaining stats as debt in #5963.Why this issue matters to the user
The event loop is shared by every request the gateway serves. One theme-detail request against a slow SMB data home stalls chat streaming, approvals, and every other dashboard route until the stat returns -- a whole-gateway hang triggered by a single settings page visit.
How our fix solves it
All target stats now ride one
discovery_executor()hop: a small synchronous helper (_stat_targets) returns(target.exists(), dir_target.is_dir()), and every method branch consumes the captured pair. One hop replaces six, matching how the route already offloads its reads/writes and how #5943 offloaded_resolve_theme_assetat the asset/overlay/topbar routes. Branch-for-branch behavior parity with main was verified (same status codes, same ordering, same error paths); the DELETE-dir branch keeps its off-loopis_dir()re-check under the per-slug install lock. The DELETE-fileunlinkgainsmissing_ok=True: the stat now rides an earlier hop, so a concurrent delete winning the race is answered as the idempotent ok it is, rather than escaping as a 500.What tests we did
TestApiThemeDetailStatsOffLoop(3 parametrized cases, GET/PUT/DELETE): spies onPath.exists/Path.is_dirfor the handler's two target paths and fails if any such stat runs on the loop thread. Mutation-verified: inlining the helper turns all three red (2 target stat(s) ran on the event loop). Watched paths are built through the handler's own resolver so the assertion survives Darwin's/tmpsymlink.test_dashboard_themes_coverage.py+test_theme_install.py).isort/flake8clean; black + brand gates pass;mypyshows only 4 pre-existing errors in untouched files (identical on clean base via stash A/B).pytest: 69,974 passed; 86 failures + 2 errors are host-environment classes (AF_UNIX path length, CPU-cap assertions, sandbox floors) with zero overlap with this diff, reproduced identically on clean base.Any other suggestions on the work
Pre-push review (GPT 5.6 + Opus): both no blocking findings. Three advisories folded in before opening (Darwin-safe watched paths, lock-comment narrowing,
missing_okon the unlink). One suggestion declined: short-circuiting the second stat for GET/DELETE when the first suffices -- the issue explicitly asks for the stats grouped as one logical check in a single hop, and one extra stat inside an already-offloaded worker costs no loop time. The pre-existing PUT-vs-DELETE recreate race noted by Opus is unchanged by this diff and is left for its own issue if it earns one.Closes #5963