fix(cloudflare/workers): guard Durable Object class move across scripts#800
Closed
danielgangl wants to merge 2 commits into
Closed
fix(cloudflare/workers): guard Durable Object class move across scripts#800danielgangl wants to merge 2 commits into
danielgangl wants to merge 2 commits into
Conversation
…run#799) When a Durable Object class moves from one Worker to another and the former host keeps a cross-script `DurableObject(..., { scriptName })` reference, the class lands in `deletedClasses` while its `class_name` is still bound. A single script upload then ships the delete-class migration alongside the binding that references the class, which Cloudflare rejects ("Cannot apply --delete-class migration to class 'X' without also removing the binding that references it") — its validator matches DO bindings by `class_name` alone, ignoring `script_name`. That rejection is a safety interlock: Durable Object storage does not move with the class, so applying the delete migration irreversibly destroys the old namespace and its data on the former host. So: - By default the deploy now fails before any upload with `DurableObjectClassMoved`, an actionable error explaining the move, the irreversible data loss, and how to proceed — strictly better than the opaque Cloudflare error. - Opt in via the new `deleteMovedDurableObjectClasses` Worker prop to confirm the deletion. The provider then performs the move as two uploads (Cloudflare rejects doing both at once): upload alchemy-run#1 applies the delete-class migration with the conflicting cross-script binding(s) omitted, upload alchemy-run#2 re-attaches the full binding set with no migration, keeping the migration/`alchemy:migration-tag` bookkeeping consistent. Extracts `getConflictingCrossScriptDoBindings` as a unit-testable seam and adds unit coverage for the conflict detection and the error guidance. The live move scenario is covered only by integration tests against real Cloudflare. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Name the destination script(s) in DurableObjectClassMoved: payload is now
`movedClasses: { className, scriptName }[]` and the message says which
worker hosts each moved class.
- Document the brief window between the two uploads where the moved binding
is absent (in-flight requests using it fail) in the
`deleteMovedDurableObjectClasses` JSDoc.
- Drop the `unknown`-typed Set in favor of a plain `.includes` over the
(1-2 element) conflict list.
- Trim the conflict-detection block comment to the Cloudflare-rejection
facts and safety-interlock rationale.
- Remove the zero-signal `_tag` assertion test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
transfer would be better, right? Put together that here #803 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #799.
Problem
When a Durable Object class moves from one Worker to another and the former host keeps a cross-script reference (
Cloudflare.DurableObject(..., { scriptName })), the former host's next deploy fails. The class disappears from the worker's locally-owned DO bindings (getDurableObjectBindingsexcludes cross-script bindings from migration ownership), so it lands indeletedClasses— while the same script upload still carries adurable_object_namespacebinding referencing thatclass_name. Cloudflare rejects the combination:Cloudflare requires binding removal and class deletion in two separate uploads, and matches bindings by
class_namealone — ascript_namepointing at the new host does not exempt the binding. There is no single-upload sequence it accepts.Why this isn't silently auto-fixed
That rejection is effectively a safety interlock. Durable Object storage does not move with the class — the new host gets a fresh, empty namespace, and the old namespace on the former host still holds all its data. A delete-class migration permanently destroys it. Silently splitting the upload would turn an innocent-looking topology refactor into irreversible data loss.
Change
Default — fail loudly, before any upload. When the computed
deletedClassesintersects the class names of current cross-script DO bindings, the deploy now fails with a newDurableObjectClassMovederror that states (a) the class moved to another script, (b) the data does not transfer and the delete is irreversible, and (c) how to proceed. This is strictly better than the opaque raw Cloudflare error users hit today.Opt-in — two-phase upload. Setting the new
deleteMovedDurableObjectClasses: trueprop on the Worker confirms the deletion. The provider then splits the deploy:deleted_classesmigration. Cloudflare accepts it (the binding is gone) and drops the old namespace and its data.alchemy:migration-tag:value, so the recorded tag stays consistent.Non-DO bindings and local DO bindings are present in both uploads; the two-phase path only triggers when the conflict set is non-empty — every other deploy stays single-upload. A
session.noterecords when the split activates.Note: between the two uploads there is a brief window where the moved cross-script binding is absent — in-flight requests that use it will fail during that window. This is inherent to Cloudflare's two-upload requirement; a manual two-phase deploy has the same window. It is documented on the
deleteMovedDurableObjectClassesprop.Test coverage
The conflict detection is extracted into
getConflictingCrossScriptDoBindings(exported for unit testing, matching the existingnormalizeStateDomainsseam) and covered by unit tests, alongside tests asserting theDurableObjectClassMovedguidance. The live local→cross-script move is only exercisable by the integration suite against real Cloudflare, so no new live test was invented here;bun tsgotypecheck, oxlint, oxfmt, andpackages/alchemyWorkerProvider.test.ts(14 passing) all run offline and are green.🤖 Generated with Claude Code