Skip to content

fix: preserve interval schedule on re-registration - #27

Merged
dr-dimitru merged 3 commits into
masterfrom
fix/interval-keep-schedule
Sep 26, 2026
Merged

dr-dimitru merged 3 commits into
masterfrom
fix/interval-keep-schedule

Conversation

@dr-dimitru

Copy link
Copy Markdown
Member

Summary

  • Preserve the earlier stored executeAt when an active interval is re-registered with the same delay. Restarts no longer postpone cluster-wide work by a full interval; past-due tasks run after the next scheduler revolution.
  • Apply the decision atomically in MongoDB, Redis, and PostgreSQL. New tasks, changed delays, one-shot timers, and later stored run times retain their existing scheduling behavior.
  • Bump npm and Meteor package metadata to 6.4.0.
  • Rewrite the README section as current scheduling behavior, and shorten the JoSk skill description to trigger conditions.
  • Include a version-gated JoSk 5.x skill reference for legacy Node 14/16 users.

CI coverage

The interval restart integration tests are registered in the Redis, MongoDB, and PostgreSQL Mocha suites. The existing GitHub Actions driver-version and server-version matrices run npm run test:redis, npm run test:mongo, and npm run test:postgres, so all six adapter matrices execute the new restart tests.

Verification

  • npm run test:coverage: 107 passed, 39 live tests skipped because no test-store URLs were configured.
  • npm run test:bun: 107 passed.
  • npm run test:adapter-guards: 6 passed.
  • npm run test:types: passed.
  • npm run prepublishOnly: passed.
  • npm pack --dry-run: reports josk@6.4.0.
  • Workflow YAML, package-version metadata, and adapter-suite test registration validated.

The live Redis, MongoDB, and PostgreSQL suites were not run locally to avoid touching shared dev services. CI runs them against isolated services.

dr-dimitru and others added 3 commits September 13, 2026 00:18
…16 pins

SKILL.md describes 6.x; a project pinned to josk@5.0.0 (the last Node
14/16 release) was being handed PostgresAdapter, pause()/resume(),
concurrency, execute and lockLeaseTime, and the v6 'default' prefix,
none of which apply there. A short version gate at the top of SKILL.md
routes such projects to references/legacy-v5.md, read only behind that
gate: the 3.x–5.x ladder, 5.0.0 exports/options/defaults, the ready()
handler contract (sync zero-arg handlers are never rescheduled), the
'' vs 'default' prefix change, and the Redis 4→5 key layout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017C1PrDpdMoybvFaAWvzktR
…rs it

JoSk#setInterval() calls adapter.add() on every call, and every adapter's
add() reset the task's next run to now + delay. Each process re-registers
its intervals on boot, so every boot of any instance pushed every interval
back by a full delay for the whole cluster. A crash-looping instance,
frequent rolling deploys, or dev hot reloads starved intervals: a
10-minute Mongo-backed interval went 70 minutes without running after
five reloads.

add() now keeps the stored next run when the task already exists, is not
deleted, is an interval, has the same delay, and the stored time is
earlier than now + delay. A past-due task stays past-due and runs on the
first revolution after boot; the atomic claim still lets only one
instance run it. New tasks, deleted tasks, a changed delay, a changed
kind, and every setTimeout/setImmediate registration still schedule at
now + delay. A later stored time (e.g. a zombie park) is replaced by
now + delay, so no run is ever scheduled later than before.

Each adapter decides in one atomic statement:
- MongoAdapter: upsert through an update pipeline with $cond (uid and
  delay wrapped in $literal). setTimeout keeps the plain $set upsert.
  Pipeline updates need mongod 4.2+; README already requires 4.4+.
- RedisAdapter: ADD_TASK_SCRIPT decides in Lua and writes the same value
  to the task hash and the schedule ZSET. It still refuses to revive a
  tombstoned (isDeleted) entry, as before.
- PostgresAdapter: INSERT ... ON CONFLICT DO UPDATE with a CASE on
  execute_at.

The fix is on by default: the old reset is a bug for any multi-instance
or often-restarted deployment. To restart the countdown on purpose,
change delay or clear the task before setInterval().

Tests: Jest unit checks of the add() statement per adapter; live contract
tests for all three adapters (keep earlier, past-due stays due and gets
claimed, later time replaced, changed delay, deleted task, setTimeout and
kind change, $-prefixed uid); mocha restart tests per adapter (first
instance dies, second instance re-registers). Docs: README setInterval
section, adapter contract, blank-example, skill references. index.cjs and
.d.ts regenerated via prepublishOnly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014t7uCinRDEGFWJcVwtk3Uh
@dr-dimitru
dr-dimitru merged commit 9dac2f5 into master Sep 26, 2026
92 checks passed
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