Skip to content

fix(cloudflare/workers): keep the previous dev worker instance serving until its replacement starts#863

Draft
sam-goodwin wants to merge 1 commit into
mainfrom
claude/issue-859-f2df0a
Draft

fix(cloudflare/workers): keep the previous dev worker instance serving until its replacement starts#863
sam-goodwin wants to merge 1 commit into
mainfrom
claude/issue-859-f2df0a

Conversation

@sam-goodwin

Copy link
Copy Markdown
Contributor

Fixes #859.

Under alchemy dev, a reconcile that replaces a Worker instance ("Changes detected, interrupting existing instance") tore down the running workerd — and deleted its dev-registry entry — before the replacement completed runtime.start. For DO/container hosts, whose start includes docker image prep, that dark window spans seconds to minutes and re-opens on every change. Cross-script DO bindings resolve through the registry, so consumers 503 with exactly the reported error for the whole window:

Durable Object "EvalHost" defined in worker "eval-host-dev" not found.

I reproduced the issue's exact forensic state live (host worker present only as its stable-port proxy, no --debug-port instance, no registry entry, "Changes detected" logged with no subsequent "Started") — it is the window between an instance interrupt and the replacement's serve completing, held open by churn or a slow container start.

The serve path is now make-before-break:

  const serveWith = (worker, bundle, proxy) =>
    Semaphore.withPermits(serveLock(worker.id), 1)(
-     // close previous workerd, THEN start the replacement (dark window)
-     Scope.close(previous) → runtime.start(...)
+     // start the replacement while the previous instance keeps serving
+     // (and stays registered), then cut the proxy over and close it
+     runtime.start(...) → proxy.set(url) → Scope.close(previous)
    )
  • workerd scopes fork from the provider root scope instead of the instance scope, so tearing down an instance's bundle watcher no longer kills its running workerd — the last good instance keeps serving until the replacement's first serve completes (reconcile replacement, sibling-triggered restartWorker, and rebuilds all included).
  • delete / dev: external teardown closes the workerd scope explicitly.
  • Both instances share one registry key; safe old-after-new closing needs owner-aware entry removal in cloudflare-runtime (fix(runtime): owner-aware registry entry removal cloudflare-tools#72 — submodule bumped here), which is what forced fix(cloudflare): preserve dev worker registration on reload #812's close-old-first ordering.

The regression test drives the real dev topology (persistent provider sidecar via RpcSpawner + RpcProviderProxy, like alchemy dev): deploy a DO-hosting Effect Worker, redeploy with a changed env var, and assert the old instance still answers mid-handoff and the registry entry never vanishes (stable inode). On main it fails with the issue's failure mode (502 ProxyError: Network connection lost); with this change it passes.

…g until its replacement starts (#859)

Under alchemy dev, a reconcile that replaced a Worker instance ("Changes
detected, interrupting existing instance") tore down the running workerd —
and deleted its dev-registry entry — before the replacement completed
runtime.start. For DO/container hosts, whose start includes docker image
prep, that dark window spans seconds to minutes and re-opens on every
change; cross-script DO bindings resolve through the registry and 503 with
'Durable Object "X" defined in worker "y" not found' the whole time.

Make the serve path make-before-break:
- workerd scopes are forked from the provider root scope, not the instance
  scope, so tearing down an instance's bundle watcher no longer kills the
  running workerd; the last good instance keeps serving until the
  replacement's first serve completes.
- serveWith starts the replacement first, cuts the proxy over, then closes
  the previous scope. Registry safety for the shared key comes from
  owner-aware entry removal in cloudflare-runtime (cloudflare-tools#72),
  which unblocks reverting the close-old-first ordering #812 introduced.
- delete/external-mode teardown now closes the workerd scope explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alchemy-version-bot

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/4e3eb4e

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/4e3eb4e

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/4e3eb4e

@sam-goodwin
sam-goodwin requested a review from john-royal July 21, 2026 06:56

@john-royal john-royal 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.

Makes sense, LGTM

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.

alchemy dev: Effect-hosted DO workers never register in the dev registry — cross-script DO bindings 503

2 participants