feat(cluster): bound runner entity residency and storage reads - #7206
Merged
Conversation
Adds admission control to cluster runners:
- ShardingConfig.maxResidentEntities (default 10_000) caps how many
entities can be resident on a runner at once. The spawn gate lives in
front of the entity ResourceMap, so the storage read loop and volatile
sends share one counter. At the cap, volatile sends to new addresses
fail with MailboxFull, persisted sends still succeed, and messages for
new addresses stay in storage until a slot frees up.
- ShardingConfig.unprocessedMessageBatchSize (default 1024) bounds each
storage read. The read loop delivers to already-resident addresses
first, then admits new addresses while slots remain, reopening the
read latch after a full batch and waking on entity removal at the cap.
- MessageStorage.unprocessedMessages accepts { limit, addresses } and
claims only the rows it returns (SQL, memory and noop drivers).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: d8061b5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 30 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.
|
Simplicity pass over the admission-control change, no behavior changes: - Derive the capped state from cappedAddresses instead of a parallel boolean, and merge the notification sweep into one guarded block. - Read-and-process helper for the two storage read steps; hoisted markDelivered effect instead of per-message closures. - Drop resetAddress from the encoded storage driver contract; drivers implement only the batched resetAddresses and the singular form is derived in makeEncoded. Group the SQL bulk reset with IN lists. - One shared UnprocessedOptions type (ReadonlyArray) for the decoded and encoded storage APIs, removing the conversion glue in makeEncoded. - Hoist option handling out of the per-dialect query builders; reuse Arr.groupBy for the address filter. - Memory driver uses one addressKey helper and a named claim-window constant; entity reaper no longer depends on the spawn error union. - Shared test config defaults and a saveGetUserRequest helper replace duplicated envelope plumbing in Sharding.test.ts. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…929db96c # Conflicts: # packages/effect/test/cluster/MessageStorage.test.ts
Workflow and durable clock entities now use a ten-second idle time instead of the one-minute entityMaxIdleTime default. Their state is durable, so a completed or suspended execution can be evicted safely and is rebuilt from storage when its next message arrives. This keeps finished executions from holding maxResidentEntities slots. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…alues The ten-second workflow entity idle time is now a cap applied with Duration.min over the configured entityMaxIdleTime, so operators who tune the global idle time below ten seconds keep their value. The config is read optionally from the layer context, keeping ShardingConfig out of ClusterWorkflowEngine.layer's public requirements; without it the cap applies as-is. The eviction test now resumes the workflow after its entities were released and asserts it completes, and a new test pins the sub-cap config behaviour. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
tim-smart
commented
Aug 13, 2026
Merged
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-603
Adds admission control to cluster runners so a backlog of persisted messages for many distinct entity ids can no longer materialize unbounded entities and OOM the runner.
Changes
ShardingConfiggains two knobs (env config accepts integers only):maxResidentEntities(default10_000): a runner-wide cap on resident entities. The spawn gate sits in front of the entityResourceMap, so the sequential storage read loop and concurrent volatilesendLocalshare one counter and neither can bypass the cap. Workflow clock entities count toward the same cap."unbounded"(programmatic only) restores the previous spawn behaviour.unprocessedMessageBatchSize(default1024): bounds each storage read. Cap unit is entities, batch unit is messages.Storage read loop is now two-step on each wake:
rowidorder, admitting a new address only while a slot is free.Latch behaviour: a full batch with delivery progress reopens the read latch immediately; at the cap the loop waits for an entity removal (any
entities.removeopens the latch) or the poll interval instead of spinning. Rows skipped at the cap have their claims released (resetAddress) so they are re-read as soon as a slot frees up.At-cap send paths:
resumeEntityFromStorage; the resident-entityMailboxFullresume path for a full per-entity mailbox is unchanged.MailboxFullerror, which now also means runner-full (documented).waitUntilReadnotifications resolve successfully at the cap instead of hanging or failing.MessageStorage.unprocessedMessages(decoded + encoded, SQL + memory + noop) accepts{ limit, addresses }and claims (last_read) only the rows actually returned; unclaimed rows stay eligible.Tests
ShardingConfig: defaults, env int-only parsing, programmatic"unbounded".Sharding: cap held while a backlog drains via idle reaping, volatileMailboxFullat the cap while persisted sends succeed, resident-prefer delivery with a newer-id backlog in front, full-batch drain within one poll interval, no busy-polling at the cap,"unbounded"behaviour.MessageStorage(memory) andSqlMessageStorage.integration(pg / mysql / sqlite):limit,addressesfilter, andlast_readset only on returned rows.Residency.test.ts, pg + mysql): a single runner withmaxResidentEntities: 3holds the cap against a backlog of new ids, residents keep receiving messages, and the backlog drains in waves once slots free up.pnpm check,pnpm lint, the fulleffecttest suite (8135 tests), platform-node cluster tests, and the cluster-integration suite all pass locally.🤖 Generated with Claude Code