Cluster sharding on Cloudflare Durable Objects - #7322
Open
tim-smart wants to merge 36 commits into
Open
Conversation
🦋 Changeset detectedLatest commit: 969c782 The changes in this PR will be included in the next version bump. This PR includes changesets to release 31 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
Bundle Size AnalysisGenerated from PR build output; treat the content below as untrusted.
|
Adds the @effect/platform-cloudflare package with the length-prefixed Durable Object name encoding shared by entity, workflow, queue, and singleton addresses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onstructor Adds the four SQLite-backed Durable Object classes the cluster binds: the shared entity class plus workflow, durable queue, and singleton placeholders. The entity constructor opens SQLite, ensures the mailbox tables, and re-arms the single alarm; user handlers are never built in the constructor. Includes a Miniflare smoke test for the bindings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…harding Provides the cluster Sharding service from the four Durable Object namespace bindings. Entity clients resolve their object with the length-prefixed name and getByName; unknown entity types fail at the Worker before contacting a Durable Object. Entity handlers register per EntityType at Worker init. Messaging paths land with the mailbox work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Exclude the package from deno check (workers-types globals are not visible to Deno) and ship @cloudflare/workers-types as a runtime dependency since the public types reference it. - Match core Sharding registerEntity semantics: duplicates are a no-op and registrations are removed when the registering scope closes. - Handle alarm() on ClusterEntity so an armed alarm cannot fire into a missing handler, and only block object construction on alarm work when a pending deliver_at row exists. - Replace async/await storage glue with Effect-based helpers typed against the official workers-types signatures. - Reject empty entity types in decodeName, single-source the ClusterName type, simplify the stub client to a plain record, align reply-table uniqueness with the cluster reply protocol, and fix JSDoc categories. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…clocks Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Recover the Durable Object name from the stored execution on alarm wakes, where ctx.id.name is undefined, so due clocks fire after eviction - Persist a resume_pending marker with each deferred exit and replay on wake, so a resume lost with the isolate cannot strand a suspended execution - Route in-run engine operations through a context-provided execution handle instead of a module-level map that could outlive its Durable Object - Link a late-arriving parent to an existing child execution - Batch due-clock completions into a single replay per alarm - Honor an explicit inMemoryThreshold of zero in DurableClock.sleep Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aths - Persist the interrupted completion when a hard interrupt kills the run fiber before it can encode its result, instead of rejecting callers - Set resume_pending for alarm-completed clocks and keep a guard alarm armed while a resume is pending, so a replay lost with the isolate is retried - Record an in-flight resume request synchronously with the deferred write, closing the window where a settling attempt could clear it unserviced - Retry and log parent resume instead of a single swallowed RPC, and resume the parent on a defect exit like the cluster engine Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fold loadExecutionName into loadExecution and share one detach helper for fire-and-forget promises - Flatten run()'s discard branching and route the wake self-heal through resume() - Deduplicate the per-workflow codec caches and drop the redundant conflict clause on the single-threaded deferred insert Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One queue name is one Durable Object: items, attempt counts, and in-flight leases live on the object's SQLite storage behind its single alarm, which acts as a watchdog redelivering items whose worker died. CloudflareCluster now also provides PersistedQueueFactory, so the DurableQueue user API works on the Cloudflare path out of the box. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Single leasing code path through the waiter wake pass, an eagerly built queue runtime without the lazy indirection, a derived RPC item type instead of a duplicated interface, and one shared stub retry policy. Behavior and test coverage unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- dedup the three module-level registries behind a shared makeRegistry - dedup bounded-map eviction (request targets, delivered queue items) - move request envelope encoding next to its decoder in entityWire - cache the per-rpc chunk values codec so parser compilation can memoize - carry reply metadata (kind/terminal) instead of re-parsing reply JSON in the entity session flow; loadNextReply returns its kind column - merge the duplicated delayed-duplicate branches and replay loops in ClusterEntity; skip replay decode for requests with an active session - split the mailbox capacity check into two indexed counts and add a partial index for unacked chunks; index queue position for MAX lookups - drop dead surface: PersistResult.lastReceivedReply, the reply_to migration fallback, clearReplies RPC, makeRunnerAddress, optional EntityStub methods that are always present - fast-path the 2 MB size check to avoid encoding small strings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tim-smart
force-pushed
the
eff-698-cloudflare-cluster
branch
from
August 18, 2026 22:43
eb32985 to
fc958ca
Compare
tim-smart
commented
Aug 18, 2026
Move the ClusterEntity state machine out of the Durable Object class into
an Effect-land entity manager in internal/entityRuntime.ts. The class
methods are now one-line Effect.runPromise adapters; Effect.runPromise
appears only at the DO RPC membrane.
- Replace the hand-rolled #serial promise chain with a Semaphore(1)
permit around invoke/alarm entry.
- Replace ReplySession taker arrays with a Queue per session plus a
Deferred for chunk acknowledgements; handler failure travels as Cause.
- Replace #workerWaiters {resolve, reject} pairs with Deferred values.
- Define the DO invoke result as a Schema tagged union in entityWire.ts;
encode once in the DO, decode once in CloudflareCluster.ts, removing
the string-literal error discriminants and the related casts. Same
treatment for the replay-envelope tag peek.
- Delete the module-global reply handler map in entityReply.ts; pinned
callers now wait on a Deferred in a per-object reply registry provided
through handler context, and deliverReply completes it.
- Remove the `let client!` definite-assignment in CloudflareCluster.ts
and the RequestId casts on client.write.
Behavior preserving: no changes to the locked design, wire semantics
beyond the invoke-result envelope shape (private same-package RPC), or
SQLite schemas. Tests updated only where they fake the internal wire.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Durable Objects Split the blanket invoke serialization in makeEntityManager into two levels: storage entry (decode, persist-before-run, dedupe, duplicate resume, alarm arming) keeps the single entry permit, while handler execution runs in forked fibers governed by a per-entity semaphore sized from the entity's concurrency build option. Replayed mailbox rows and alarm-due runs draw from the same budget, and a handler's permit is released while a stream chunk is parked on its client acknowledgement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Replace the Done/Wait/Continue InvokeOutcome ADT with a nested effect: invokeEntry returns the post-permit continuation directly and invoke flattens it. - Move the handler semaphore into makeEntityRuntime next to the other build options, deleting the manager's lazy tri-state resolution. - Use a plain Semaphore.withPermit on the discard/scheduled path (no stream acks there) and build the pausable permit only for sessions. - Register one waitUntil per replay batch instead of one per row, skip replay entirely for an empty mailbox, and drop redundant empty-array guards around Fiber.awaitAll. - Reuse encodeName and a shared invoke helper in the test fixture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Consolidate the PR changesets into one effect patch changeset - Export Entity.KeepAliveHandler and use it from platform-cloudflare - Return Effects from the entity mailbox operations - Use services with optional access instead of References for the entity reply context - Build mutateAndWake inside a single Effect.suspend Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tim-smart
commented
Aug 19, 2026
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.
Closes EFF-698
Shared PR for the Cluster Sharding on Cloudflare Durable Objects effort. Serial slices land here one by one; this first slice adds the package scaffold and the Worker/Durable Object glue.
What's in this slice
@effect/platform-cloudflare(cluster plus the minimum Worker/DO glue; no HttpServer/Crypto/FS parity).ClusterEntity(shared entity class),ClusterWorkflow,ClusterDurableQueue, andClusterSingleton(placeholders reserving their bindings).`${type.length}:${type}${id}`shared by entity/workflow/queue/singleton addresses, with strict decode.CloudflareCluster.layer({ entities, entityNamespace, workflowNamespace, queueNamespace, singletonNamespace })providing the clusterShardingservice.Entity.clientstays the user API; clients resolve objects viagetByName; unknown entity types fail at the Worker before contacting a Durable Object. Handlers register perEntityTypeat Worker init.CloudflareCluster.makeRunnerAddress: syntheticCurrentRunnerAddressstub from DO identity, no peer dialing.Tests
Handler concurrency slice (EFF-727)
Entity.toLayer(..., { concurrency })is now honored inside the entity Durable Object. Storage entry (decode, persist-before-run, dedupe, duplicate resume, alarm arming) keeps the single entry permit; handler execution runs in forked fibers governed by a per-entity semaphore sized from the option (default 1, numeric N,"unbounded"). Replayed rows and alarm-due runs draw from the same budget, and a handler's permit is released while a stream chunk waits for its client acknowledgement.Why not
RpcServer.makeNoSerialization({ concurrency }): it was evaluated and does not fit the Durable Object flow. Its permit is held for a stream handler's whole lifetime, including client-ack waits; on Cloudflare acks arrive from remote callers over separate DO requests, so an unacked or abandoned stream would pin the only default permit and brick the entity (the existing "acknowledges stream chunks without holding the entity lock" test fails under that model). It also owns request lifecycle state that this path keeps in SQLite: persist-before-run andPrimaryKeyduplicate resume must consult the mailbox before any handler starts, replies must be written throughsaveReplyinsidetransactionSync, andlastSentChunksequence resume plus the per-registrationdefectRetryPolicyrebuild live in the hand-rolled runtime. Wiring the server in would mean re-adapting all of that around it, as the classicentityManagerdoes, for no net simplification. A scoped permit protocol on the existing loop keeps the locked persist/dedupe semantics untouched.Later slices (mailbox, DeliverAt, keepAlive, workflow engine, queue, singleton/cron, proxies/metrics/docs) push to this branch.
Interrupted persisted stream cancellation (EFF-731)
waitUntilsession.Closes EFF-731
🤖 Generated with Claude Code