Skip to content

fix: flag restart when redeploying an active jsResource component (#1817) - #1820

Merged
kriszyp merged 5 commits into
mainfrom
fix/1817-jsresource-redeploy-restart-flag
Jul 17, 2026
Merged

kriszyp merged 5 commits into
mainfrom
fix/1817-jsresource-redeploy-restart-flag

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Redeploying an existing, active jsResource-backed component with restart: false neither flagged that a restart was needed (get_status.restartRequired stayed false) 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.ts now tracks the set of files each scope has already loaded (a closure Set that 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 a change: 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:

  • Restart flag: restartRequired now becomes true after such a redeploy (and in auto-reload/dev mode drives an actual reload).
  • Stale code: because the re-'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 (we return on 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 the loadedResourceFiles set + the extended 'add' gate.

Tests

  • unitTests/resources/jsResource.test.js — a re-add of a known path requests a restart and does not re-import; a distinct new file still hot-loads. The first test fails on main, passes here.
  • integrationTests/deploy/redeploy-restart-flag.test.ts (+ fixture) — end-to-end: deploy a jsResource component, redeploy a modified copy with restart: false, assert get_status.restartRequired flips to true (and the old code stays live until a restart). Fails on main (30s poll never sees the flag), passes here.

Notes

  • Generated by an LLM (Claude Opus 4.8). Standard cross-model review was not run in this headless worker — flagging for the human reviewer.

Refs #1817

🤖 Generated with Claude Code

)

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>

@gemini-code-assist gemini-code-assist Bot 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.

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.

Comment thread resources/jsResource.ts
Comment thread integrationTests/deploy/redeploy-restart-flag.test.ts Outdated
Comment thread integrationTests/deploy/redeploy-restart-flag.test.ts Outdated
@claude

claude Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

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>
Comment thread resources/jsResource.ts
kriszyp and others added 2 commits July 15, 2026 10:24
… 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.
Comment thread integrationTests/deploy/redeploy-restart-flag-deletion.test.ts Outdated
…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.
@kriszyp
kriszyp marked this pull request as ready for review July 15, 2026 19:25

@heskew heskew 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.

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.

@kriszyp
kriszyp merged commit 1684014 into main Jul 17, 2026
56 checks passed
@kriszyp
kriszyp deleted the fix/1817-jsresource-redeploy-restart-flag branch July 17, 2026 20:08
kriszyp pushed a commit that referenced this pull request Jul 27, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants