Skip to content

Deliver scheduled cluster messages at their deadline instead of the next poll#1

Draft
sbking wants to merge 4 commits into
deliver-at-basefrom
feat/cluster-deliver-at-rebuild
Draft

Deliver scheduled cluster messages at their deadline instead of the next poll#1
sbking wants to merge 4 commits into
deliver-at-basefrom
feat/cluster-deliver-at-rebuild

Conversation

@sbking

@sbking sbking commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Fork-only review draft. The base branch pins Effect-TS/effect main at 212493b so the diff is exactly the two commits under review. Before an upstream PR this rebases over Effect-TS#6441 and the 0003 migration renumbers past its 0003_mssql_schema_fixes.

Summary

Messages saved with a future deliverAt are only discovered by the periodic storage poll, so a scheduled message can be delivered up to a full poll interval after its deadline.

  • add a required MessageStorage.nextDeliverAt operation that returns the earliest future scheduled delivery time for a set of shards, with memory and SQL driver implementations
  • after every storage read, Sharding re-arms a single replaceable timer for that deadline; when it fires, it runs the existing storage read path
  • the wake affects promptness only: if nextDeliverAt fails, the error is logged at debug level and delivery falls back to the existing poll
  • use explicit null checks in the memory driver so due-message selection and nextDeliverAt agree when deliverAt is zero

PostgreSQL and SQLite answer the query with MIN(deliver_at) over a partial index on (shard_id, deliver_at) filtered to unprocessed scheduled rows. MySQL and SQL Server have no partial indexes, so they seek per shard (correlated scalar subquery and CROSS APPLY ... TOP 1) over a plain (shard_id, processed, deliver_at, last_read) index, which stays flat as processed rows accumulate. Migration 0003_deliver_at_index creates the index idempotently on all four dialects.

Local container benchmarks used 1 million message rows, 300 assigned shards, and roughly 200 to 180,000 pending scheduled messages. With the selected per-dialect queries and indexes, the deadline lookup stayed below 8ms on PostgreSQL, around 4ms on MySQL, below 0.1ms on SQLite, and around 0.7ms on SQL Server. These are warm-cache local measurements; production calls still pay network latency. Synthetic bulk inserts measured roughly 0.3-2μs of additional index work per inserted row.

Testing

  • new tests: a message scheduled before a runner starts is delivered at its deadline instead of the next poll; a failing nextDeliverAt degrades to poll-cadence delivery with the read loop still usable; deliverAt at epoch zero across the due boundary
  • pnpm vitest run packages/effect/test/cluster/MessageStorage.test.ts packages/effect/test/cluster/Sharding.test.ts packages/effect/test/cluster/Entity.test.ts (42/42)
  • the shared SQL storage suite's nextDeliverAt cases pass against PostgreSQL, MySQL, and SQLite (pnpm vitest run packages/platform-node/test/cluster/SqlMessageStorage.test.ts -t nextDeliverAt); the SQL Server leg runs after the rebase over Support SQL Server in cluster SqlMessageStorage Effect-TS/effect#6441 lands its container fixture
  • pnpm check
  • pnpm lint

@sbking sbking changed the title Prompt deadline wakes for scheduled cluster messages (fork review) Deliver scheduled cluster messages at their deadline instead of the next poll Jul 17, 2026
@sbking
sbking force-pushed the feat/cluster-deliver-at-rebuild branch from cf081fe to 6e63cbb Compare July 17, 2026 06:46
Stephen Brian King added 2 commits July 17, 2026 02:14
The NVARCHAR literals cause SQL Server to convert the indexed VARCHAR `shard_id` column, preventing the per-shard index seek.
Shard identifiers now reach the nextDeliverAt queries as bound parameters
in all three dialect branches, with SQL Server keeping its VARCHAR cast.
The storage read loop discovers the next scheduled deadline before reading
due messages, so a deadline crossing between the two reads arms a wake
instead of waiting for the next poll interval. Regression tests cover a
quote-bearing shard group and a deadline that lands between the reads.
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.

1 participant