feat(templates): serve stale gallery cache immediately, revalidate in background (BE-3427)#566
Conversation
… background (BE-3427) A TTL-expired but present gallery index is now served immediately and re-fetched in a detached background process (stale-while-revalidate), instead of blocking the current call on the 15s fetch. On an offline/firewalled machine this removes the per-invocation 15s hang that the synchronous stale-fallback (BE-3393) still incurred once past the TTL. - _load_gallery: stale-but-present cache is returned right away + a detached refresher is spawned; --refresh and no-cache stay synchronous. - _spawn_background_refresh: launches a fully detached 'comfy templates _refresh-cache' (new session, stdio -> /dev/null), mirroring the 'comfy run' async job-watcher idiom, so it outlives the parent without ever blocking it. - _refresh-cache (hidden): fetch + atomic persist only, swallows all errors and never clobbers the last-known-good cache with a bad body. - staleness is signalled via a non-fatal renderer warning (stderr), consistent with the existing fallback warning.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 8 finding(s).
| Severity | Count |
|---|---|
| 🟠 High | 2 |
| 🟡 Medium | 3 |
| 🟢 Low | 2 |
| ⚪ Nit | 1 |
Panel: 6/8 reviewers contributed findings.
Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)
…427)
Address the cursor-review panel findings on the stale-while-revalidate
gallery refresh:
- Debounce the background refresher (index.refresh marker, 60s window) so
an offline host past the TTL can't spawn a detached `_refresh-cache` on
every `templates ls/show/fetch` — bounds the PID fan-out / local DoS.
- Anchor the detached child in our cache dir + pass -P (3.11+) so the
`-m comfy_cli` cwd-import vector can't load an attacker-planted
comfy_cli.py from the directory the user ran the command in.
- Opt the child out of telemetry (COMFY_NO_TELEMETRY/DO_NOT_TRACK) so its
entry callback can't persist an anonymous user_id via a non-atomic
config.ini rewrite that races the foreground process.
- Validate the payload SHAPE (JSON array), not just that it parses, before
persisting or serving — a valid-JSON captive-portal `{"error": …}` /
`null` / number can no longer poison the cache or crash _flatten_templates.
- Catch ValueError (covers UnicodeDecodeError on a non-UTF-8 body, a
ValueError subclass that is not JSONDecodeError) in the gallery load/serve
paths so it routes to gallery_load_failed instead of an uncaught crash.
- show/fetch (exact-name lookups) opt out of SWR (background_ok=False): a
template added upstream after the cache went stale resolves on this call
instead of reporting not-found until a later background refresh; still
falls back to the stale cache when offline.
- Detach correctly on Windows (CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS;
start_new_session is a no-op there).
- Only claim "refreshing in the background" when a refresh actually started.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ELI-5
comfy templates ls/show/fetchreads its catalog from a small JSON file cached at~/.cache/comfy-cli/gallery/index.json. PR #559 (BE-3393) gave that cache a 24h expiry, but past the expiry it re-fetches synchronously — so on an offline/firewalled machine everytemplatescommand hangs for the full 15s fetch timeout before falling back to the stale cache, once per invocation past the TTL.This makes it stale-while-revalidate: a stale-but-present cache is served immediately, and the refresh happens in a detached background process that updates the cache for the next invocation. The current call never blocks on the network — online or offline.
Stacked on #559 (BE-3393)
This is a stacked PR: base =
matt/be-3393-gallery-cache-ttl(the TTL + synchronous stale-fallback foundation, still open). It builds directly on that branch and changes its stale-path behavior from synchronous to SWR — explicitly the follow-up BE-3427 that #559 scoped out. GitHub will retarget this tomainwhen #559 merges; review sees only the net SWR diff.What changed
_load_gallery— a TTL-expired but present cache is now returned right away, and_spawn_background_refresh()is fired to revalidate it. No inline fetch on this path._spawn_background_refresh— launches a fully detachedcomfy templates _refresh-cache(start_new_session=True, stdio →/dev/null, spawn failure swallowed), mirroring the existingcomfy runasync job-watcher idiom (command/run/watcher.py). It outlives the parent without ever blocking it._refresh-cache(hidden command) — fetch + atomic persist only; no output, no telemetry, never a non-zero exit. It validates the body before persisting and swallows every error, so a bad/garbage/offline response can never clobber the last-known-good cache.renderer.warnon stderr ("serving the cached copy and refreshing in the background"), consistent with the foundation's existing fallback warning. The JSON envelope shape is unchanged (no per-payloadstalefield added).Design decision (the ticket asked for one)
Detached subprocess, not a thread. CLI processes are short-lived, which rules out both thread options:
A fully detached subprocess is the only option that returns the parent instantly in both the online and offline cases and reliably persists the refreshed cache for the next call. It's also the pattern already used in this repo for
comfy run's async watcher. The "next invocation" refresh the ticket mentions is exactly what the detached child delivers — it writes the cache that the nextls/show/fetchreads.Behavior preserved (Chesterton's fence)
--refreshstays synchronous and still surfaces fetch errors (fatalgallery_load_failed) — an explicit user request is not deferred._refresh-cachebackground entrypoint (which uses the same atomic_persist_cache+ validate-before-write).Tests
tests/comfy_cli/command/test_templates.py(updates the #559 TTL tests that asserted synchronous refetch, since the stale path is now SWR):_fetch_gallerymonkeypatched to fail loudly), cache untouched by the foreground._refresh-cacheentrypoint → persists a fresh index; keeps the stale cache on fetch failure / garbage-200 / non-200; never exits non-zero._spawn_background_refresh→ asserts the detached spawn shape (argv endstemplates _refresh-cache,start_new_session=True, stdio → DEVNULL); swallowsOSErrorspawn failure.--refreshsynchronous-path tests (fatal on failure, read-only-dir resilience) retained.python -m comfy_cli; a stale cache serves in ~1s (no 15s block) with the staleness warning on stderr.Full suite green locally (2587 passed, 37 skipped);
ruff check+ruff format --checkclean on the touched files.Negative-claim falsification
Not applicable — this diff adds a capability (opportunistic background refresh) and makes stale-serving more available; it introduces no "unsupported"/deny/dead-end path. The staleness warning is a positive affordance, not a capability denial.
Judgment calls / known minors
os.replaceatomically (last-writer-wins, no corruption); once the first completes, the cache is fresh again and subsequent calls don't spawn. Not worth a lockfile for this path.ttl_auto_refreshsynchronous fallback block below the SWR branch is now only reachable for the corrupt-stale-cache edge; left intact to minimize churn on the foundation's structure.(BE-####)PR/commit refs (per feat(templates): add 24h TTL with stale-cache fallback to gallery cache (BE-3393) #559 and repo history), so this follows that convention rather than the no-Linear-ref guardrail that applies to the ComfyUI/ComfyUI_frontend public repos.