fix: move theme-create mkdir and collision check off the event loop (#6198) - #6204
Conversation
…6198) api_themes_create still ran themes_path.mkdir() and a target.exists() pre-check directly on the asyncio event loop. On a UNC/SMB-backed data home either call can block the whole gateway — the same defect class the theme-detail route fixed for #5963. Fix by subtraction: the on-loop exists() pre-check is deleted outright (the in-lock check inside _create_locked already returns the same 409 authoritatively), and the mkdir moves into _create_locked, which already runs on a worker thread under the per-slug lock. The handler now does pure path construction on the loop. A new off-loop spy test pins that the create route never touches the filesystem on the event loop, mirroring the detail route's discipline. The error-code baseline is re-snapshotted because deleting the uncoded pre-check 409 improved the ratchet count for themes.py.
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Fix-by-subtraction on an already-locked path, mirroring the #5963 discipline, with a spy test pinning the invariant — sound and minimal. [DESIGN-REVIEWED] 392187d |
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: |
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: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All verification checks pass. I have everything needed for the review. Verified against the repo:
First-Principles-Verdict: PASS A declared, subtraction-shaped fix: it deletes a redundant on-loop check and moves the rest into an existing off-loop critical section, adding zero surface. What this change shipsIntent: stop a theme-create POST on a slow (UNC/SMB) data home from stalling the whole gateway — a FIX.
No new config key, flag, parameter, or public symbol; nothing to consumer-count. The fix reuses the existing executor path rather than adding a hop, and the deleted pre-check was a second spelling of the in-lock check ( WatchSame defect class survives outside the theme handlers: grep for [FIRST-PRINCIPLES-REVIEWED] 392187d |
chenmingwei23
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (3 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 - move theme-create mkdir and collision check off the event loop, clear root cause, no auth/parsing/trust/sandbox/gate/secret surface.
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (3 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 moves theme-create mkdir and collision check off the event loop into the worker-thread install lock (#6198), a clear root cause with no behaviour change to the theme record.
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (3 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 (#6198 — on-loop mkdir/exists blocks the gateway on UNC/SMB-backed data homes; moved inside _create_locked worker thread, collision semantics preserved).
iamwhatever
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (3 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: moves the theme-create mkdir and slug-collision stat off the event loop into the existing _create_locked worker thread, preserving the 409 via the in-lock check; third file is the error-code-baseline ratchet decrement.
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (3 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 — moves theme-create mkdir and collision check off the event loop into the locked worker-thread section (#6198), preventing SMB/UNC-backed filesystem calls from stalling the gateway; behavior covered by new thread-affinity tests.
dwu96
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (3 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: moves the themes-create mkdir and the pre-lock target.exists() collision check off the event loop into the existing _create_locked worker-thread body, so a UNC/SMB-backed data home cannot stall the gateway on an on-loop stat; the 409 for a taken slug now comes from the in-lock check that already covered the installed-pack case, error-code-baseline.json is ratcheted down 1358 -> 1357, and a parametrized spy test asserts every watched filesystem call ran off the loop.
Problem / Motivation
api_themes_createstill runs two blocking filesystem calls directly on the asyncio event loop:themes_path.mkdir(parents=True, exist_ok=True)and atarget.exists()pre-check. On a UNC/SMB-backed data home either call can block for as long as the network takes, stalling every other request the gateway serves — the identical defect class #5963 fixed for the theme detail route.Closes #6198
Why it matters
One on-loop stat freezes the user's chat turn AND the liveness heartbeat until the watchdog kills the process — the crash-loop wedge the
no-blocking-call-on-event-looprule exists to prevent. The create route was the last on-loop filesystem site in the theme handlers after #5943 and #5963.What changed (motivation → approach → change)
Fix by subtraction, per the First Principles review that raised this on PR #6190:
target.exists()pre-check outright. The in-lock check inside_create_lockedalready returns the same 409 authoritatively, so the pre-check was a redundant second spelling, not protection.mkdirinto_create_locked, which already runs on a worker thread (run_in_executor(discovery_executor(), ...)) under the per-slug lock — no extra executor hop in the handler. The mkdir runs before the collision checks in the same critical section, so_atomic_write_theme_jsonstill finds its parent directory present._themes_dir()is theconfig_dir()memo resolved at boot).error-code-baseline.json: deleting the uncoded pre-check 409 improved the ratchet count forthemes.py(22 → 21), and the stale-baseline test requires improved counts to be recorded.mkdir/existssites inthemes.py(~331, ~372, ~462): all run inside_do_install/_copy_installed_theme, which are executor-dispatched — already off-loop, left untouched.Recorded trade-off: an exact-duplicate-slug POST now always takes a
discovery_executorworker and the per-slug lock where it previously answered 409 from the (on-loop) fast path. Accepted deliberately — a single authoritative in-lock check is worth more than a fast path, and re-adding a cheap pre-check is what created the on-loop stat in the first place. Bounded cost on an authenticated, local-only endpoint.Tests
TestApiThemesCreateOffLoop(parametrized: fresh create + 409 collision): spiesPath.mkdir/exists/is_dir/is_fileon the handler's paths and the data home, asserting every filesystem call ran on a worker thread — mirrorsTestApiThemeDetailStatsOffLoopfrom fix(themes): move the theme-detail target stats off the event loop (#5963) #6190. Mutation-verified: reds on the pre-fix code with "2 filesystem call(s) ran on the event loop".test_installed_pack_with_the_same_slug_is_409(there is no pre-lock check anymore).test_creates_the_themes_directory_when_absent(the mkdir-inside-lock still creates an absent themes dir) and both 409 paths.Local gates: isort / flake8 / mypy / black gate / subprocess-encoding gate / brand gate / harness gate all green; full backend suite 69,972 passed with 87 failures proven pre-existing host-env on the unmodified base (identical failure set, zero theme-related).
Manual verification
N/A — unit coverage sufficient: the off-loop property is asserted directly by the spy test, and the route's observable contract (200/409 bodies) is pinned by the existing tests.
no linked issue: N/A — linked, closes #6198.