perf(redis): use sorted set index for O(log N) schedule claiming - #14
perf(redis): use sorted set index for O(log N) schedule claiming#14isimisi wants to merge 16 commits into
Conversation
|
Hi! Thanks for the PR. We need a migration path for existing Redis users before merging this. With this change, Can we make the migration automatic/idempotent, or expose and document a required migration step? For example, on startup or first claim, backfill |
|
Hey @RomainLanz, great point. I completely missed the migration path for existing users. I'll add an automatic backfill that runs on startup: on first claimDueSchedule() call (or worker boot), scan schedules::index and populate schedules::due from any schedule that has a next_run_at. I'll make it idempotent so it's safe to run multiple times across worker restarts or multi-instance deployments. |
Existing users upgrading will have schedules in the legacy format (hashes + SET) but not in the new ZSET. Run backfillDueIndex() once per worker process on the first claimDueSchedule() call so schedules keep firing without manual intervention.
|
Hey @RomainLanz, I pushed a follow-up commit that handles the migration automatically. On the first The approach is idempotent ( Does this work for u? Otherwise the we remove the automatic backfill and let users handle it themselves one-time, then we don't need to have a check on |
|
Oops, I have pushed some changes. Can you resolve the conflicts? 😅 |
|
Should be good now! We have some formatting differences in our editors it seem. Don't know if you care about that haha |
|
Thanks for resolving the conflicts and adding the automatic backfill. I'm still not fully comfortable merging this as-is. The main concern is that Since the hash is still the canonical schedule data everywhere else, can we keep the ZSET as a derived index only? For example, the Lua script could validate I'm also a bit concerned about the automatic backfill running on the first The direction is good, but before merging I'd like us to tighten the invariant: the schedule hash stays canonical, PS: You can run |
|
I'll update the Lua script to re-check On the migration side: I've been going back and forth on this. The thing with a fully explicit migration step is that So I'm leaning towards the Redis-level marker approach as a middle ground. Something like a |
I believe it is fine if we document it properly. This will be released as a breaking change with a manual migration path. |
|
So drop the auto-backfill entirely and no check. Let users run the migration themselves. |
Replace auto-backfill with an explicit migrate() lifecycle method on the Adapter interface. Remove #ensureDueIndex() and #dueIndexReady from RedisAdapter — users call migrate() once after upgrading to populate the schedules::due ZSET from pre-existing data. The Lua claim script now validates the hash's next_run_at before claiming, repairing stale ZSET scores on sight. This keeps the hash canonical and the ZSET as a derived index.
|
Hey! Went ahead and pushed the changes. Went with |
|
@RomainLanz did u decide against this? |
|
Hey! The direction is still good, sorry for the delay. Since main has evolved, the PR now needs a refresh, notably adding I'm happy to push those compatibility fixes directly to your branch, keeping the scope limited to the Redis ZSET index and migration path. Is that okay with you? |
|
Hey, yeah go for it! Push whatever u need to the branch. Sorry for the delay, been deep in thesis writing haha. Let me know if u want me to take anything. |
|
Thanks again for authorizing the maintainer updates. GitHub still reports |
|
ah, yes - the fork is org-owned. I've sent you a collaborator invite on |
Keep paused and exhausted schedules out of the derived due index, rebuild it idempotently during migration, and restore the Adapter contract after merging current main. Document the breaking Redis migration and cover schedule lifecycle consistency across adapters.
|
Damned, my agent asked for it directly lol. Thanks! |
|
I have made a request so my agent can push to your repository. Let me know once that's approved! |
|
You should be good now |
Move schedule upserts, updates, and due-index backfills into Lua so canonical hashes and the derived ZSET cannot diverge under concurrent writes. Add regression coverage for resume, upsert, and migration interleavings.
Finalize cron schedules only when the claimed canonical state is still current, preventing concurrent pause or delete operations from being undone. Repair malformed due scores during claiming so they cannot block valid schedules.
There was a problem hiding this comment.
Pull request overview
This PR improves Redis schedule-claim performance by introducing a schedules::due sorted-set index (scored by next_run_at) so due schedule lookup no longer requires scanning every schedule ID client-side.
Changes:
- Add and maintain a Redis ZSET due index (
schedules::due) and switch schedule claiming toZRANGEBYSCORE ... LIMIT 1-based selection. - Introduce an idempotent
Adapter#migrate()contract and implement Redis migration via an atomic backfill script. - Expand Redis adapter test coverage to validate index correctness, idempotency, and concurrent mutation/claim scenarios.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/queue_manager.spec.ts | Update adapter mocks to include the new migrate() method. |
| tests/adapter.spec.ts | Add comprehensive Redis schedule index + migration + concurrency behavior tests. |
| tests/_utils/register_driver_test_suite.ts | Add a cross-adapter test ensuring migrate() is safe to call repeatedly. |
| tests/_mocks/memory_adapter.ts | Implement no-op migrate() for the mock adapter. |
| src/drivers/sync_adapter.ts | Implement no-op migrate() for the sync adapter. |
| src/drivers/redis_scripts.ts | Add Lua scripts for upsert/update/backfill/claim using the due ZSET index and safe cron finalization. |
| src/drivers/redis_adapter.ts | Wire new Lua scripts, maintain schedules::due, and add migrate()/backfillDueIndex(). |
| src/drivers/kysely_adapter.ts | Implement no-op migrate() for the Kysely adapter. |
| src/drivers/knex_adapter.ts | Implement no-op migrate() for the Knex adapter. |
| src/drivers/fake_adapter.ts | Implement no-op migrate() for the fake adapter. |
| src/contracts/adapter.ts | Extend the Adapter contract with an idempotent migrate() method. |
| README.md | Document Redis schedule migration requirement and recommended deployment usage. |
| .changelog/redis-schedule-due-index.md | Add changelog entry describing the indexed claims and upgrade notes. |
| .changelog/hot-reloading-jobs.md | Remove an older changelog entry (cleanup). |
| .changelog/consistent-job-runtime.md | Remove an older changelog entry (cleanup). |
| .changelog/adapter-aware-workers-and-schedules.md | Remove an older changelog entry (cleanup). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
explanation: #13 (comment)