Repository navigation
fix: flag restart when redeploying an active jsResource component (#1817) - #1820
Conversation
) Redeploying an existing, active jsResource component with restart:false neither flagged restartRequired nor invalidated its stale module, silently serving old code. The deploy pauses/resumes the component's watcher; the fresh chokidar scan re-emits every existing file as 'add', and jsResource only requested a restart on non-'add' events, so a changed file went undetected. Track the files each jsResource scope has loaded: a re-'add' of a known file is a redeploy of code we can't hot-swap, so treat it like a change and request a restart (which surfaces as restartRequired and, in auto-reload, reloads fresh). A first-time 'add' (initial load or a genuinely new runtime file) still loads without a restart. Because the re-'add' now requests a restart and returns without re-importing, the stale module cache is never consulted for the changed file; a real worker restart rebuilds the per-scope caches. static.ts components are unaffected. Co-Authored-By: Claude Opus <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request fixes an issue where redeploying a jsResource component with restart: false failed to flag restartRequired because the file watcher's post-redeploy scan re-emitted existing files as 'add' events. To resolve this, the changes track loaded files in a Set and request a restart if an already-loaded file is re-added. The review feedback highlights that this approach does not handle file deletions during a redeploy, which would leave stale resources active in memory without requesting a restart. Additionally, the reviewer recommends adding nullish guards in the test helper functions when parsing API and HTTP responses to prevent potential runtime errors.
|
Reviewed; no blockers found. Commit 85cfe7a is a mechanical extraction of the previously-duplicated test helpers (authHeader/operation/getRestartRequired/readVersion) into redeploy-restart-flag-helpers.ts, addressing the prior inline suggestion - verified the extracted code is byte-for-byte identical to what it replaced, and resources/jsResource.ts (the core fix) is untouched by this push. All prior review threads remain resolved. |
…ag test helpers getRestartRequired() and readVersion() assumed a non-nullish JSON body; guard both per gemini-code-assist review feedback on #1820. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… poll readVersion() only guarded against non-200 HTTP responses. While the freshly restarted process is still coming up, fetch() throws (ECONNREFUSED) before any response exists, which the polling loop in "initial deploy (restart:true) serves version 1" didn't catch — crashing that test and leaving the harness mid-restart, which is also why the following redeploy test then timed out waiting for a clean restartRequired=false baseline. Refs #1817 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…(harper#1817 follow-up) The existing modified-file handling (re-'add' of a known file -> requestRestart) has no signal at all when a redeploy instead removes a resource file: the fresh post-redeploy chokidar scan only reports what's currently on disk, so a deleted file produces no event whatsoever (no re-'add', no 'unlink'). Left unhandled, the deleted resource stays registered and active in memory with no indication a restart is needed. Tracks every file the post-redeploy scan reports and, once that scan's 'ready' fires, diffs it against the full set of previously-loaded files -- anything missing was deleted, and is now handled the same as a modified file (requestRestart). The diff window is gated by the scope's deploy:start/first-post-redeploy-ready bracket so it only runs for an actual redeploy rescan, not every time the EntryHandler's 'ready' event fires for an ordinary runtime add/change. - unitTests/resources/jsResource.test.js: new deletion-detection test, plus a test confirming an ordinary runtime add/change (outside a redeploy) doesn't falsely flag other already-loaded files as deleted. - integrationTests/deploy/redeploy-restart-flag-deletion.test.ts: end-to-end repro (deploy -> redeploy deleting the only resource file -> restartRequired flips true), fails on pre-fix code (no event fires for the missing file at all). Verified: 8/8 unit tests (jsResource.test.js), both new and existing integration tests (redeploy-restart-flag-deletion.test.ts and redeploy-restart-flag.test.ts, confirming no regression to the modified-file case), build/lint/format clean.
…mment) authHeader/operation/getRestartRequired/readVersion were byte-for-byte duplicated across redeploy-restart-flag.test.ts and redeploy-restart-flag-deletion.test.ts. Extracted into redeploy-restart-flag-helpers.ts so a future fix to one (e.g. the ECONNREFUSED tolerance in readVersion) only needs applying once. Verified: both integration tests pass unchanged, build/lint/format clean.
heskew
left a comment
There was a problem hiding this comment.
Traced the fix: the restart-flag gate relies on EntryHandler emitting ready once per redeploy rescan batch, which holds (chokidar native ready + in-flight reads draining), and recurs correctly on later unrelated events. Prior bot findings all resolved at head. LGTM.
🤖 Reviewed via cross-model pipeline; approved by @heskew.
…1934) Since #1806 (5.2.0-beta.2), redeploying an already-active component recreates the file watchers instead of raising restartRequired, so a changed file re-emits as a fresh `add`. #1820 handled this for jsResource; loadEnv and fastifyRoutes still had the bare `if (eventType !== 'add') requestRestart()` gate, so a redeployed `.env` (or route file) was silently ignored — the new env values reached only the scanning thread, running workers kept the old ones, get_status stayed restartRequired:false, and the post-deploy restart was skipped (observed in the field on a replicated 5.2.0-beta.2 cluster). Extract the #1820 tracking into a shared `handleEntryWithRedeployRestart` helper (re-add of a known file, change/unlink, or a file missing from the post-deploy re-scan all request a restart) and route loadEnv + fastifyRoutes through it. jsResource keeps its inline copy (it additionally rejects non-file entries before the gate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
Summary
Redeploying an existing, active
jsResource-backed component withrestart: falseneither flagged that a restart was needed (get_status.restartRequiredstayedfalse) nor invalidated its stale cached module — it silently kept serving the old code (harper#1817).Mechanism: a redeploy pauses/resumes the component's file watcher (deploy lifecycle in
Scope/EntryHandler). On resume the fresh chokidar scan re-emits every existing file as'add', including one whose contents just changed.jsResource's handler only requested a restart on non-'add'events, so the changed file was re-imported against the never-invalidated module cache and no restart was ever flagged.The fix (direction 1 from the issue)
resources/jsResource.tsnow tracks the set of files each scope has already loaded (a closureSetthat survives the pause/resume, since the Scope + handler are preserved across a redeploy). A re-'add'of an already-loaded file is a redeploy of code we can't hot-swap, so it's treated exactly like achange: request a restart and return. A first-time'add'— initial load, or a genuinely new file dropped in at runtime — still loads without a restart.This closes both halves of the bug:
restartRequirednow becomestrueafter such a redeploy (and in auto-reload/dev mode drives an actual reload).'add'returns without re-importing, the stale module cache is never consulted for the changed file; a real worker restart rebuilds the per-scope caches from scratch.static.ts-backed components are unaffected (they apply file changes to an in-memory map, no restart by design).Why not also invalidate
ApplicationScope.moduleCache(direction 2)I deliberately did not proactively clear the shared per-scope
moduleCache. With this fix nothing re-imports a changed resource file before a restart (wereturnon the re-add), so the stale cache is never read for it, and a genuine restart recreates the caches anyway. Proactively clearing a live, shared cache mid-life risks module-identity duplication (two instances of a dependency held by old-vs-new code) and re-running module side effects — real correctness hazards for no observable benefit given direction 1. This tradeoff is the main thing worth a second opinion.Where to look
resources/jsResource.ts— the whole change is theloadedResourceFilesset + the extended'add'gate.Tests
unitTests/resources/jsResource.test.js— a re-addof a known path requests a restart and does not re-import; a distinct new file still hot-loads. The first test fails onmain, passes here.integrationTests/deploy/redeploy-restart-flag.test.ts(+ fixture) — end-to-end: deploy a jsResource component, redeploy a modified copy withrestart: false, assertget_status.restartRequiredflips totrue(and the old code stays live until a restart). Fails onmain(30s poll never sees the flag), passes here.Notes
Refs #1817
🤖 Generated with Claude Code