Skip to content

fix(scheduler): run reconciliation cron in UTC with replica dedupe - #992

Merged
greatest0fallt1me merged 3 commits into
Predictify-org:mainfrom
lhphong09t2:fix/issue-982-reconciliation-scheduler
Oct 19, 2026
Merged

greatest0fallt1me merged 3 commits into
Predictify-org:mainfrom
lhphong09t2:fix/issue-982-reconciliation-scheduler

Conversation

@lhphong09t2

Copy link
Copy Markdown

Closes #982

Summary

Makes the reconciliation scheduler execute on UTC cron boundaries, deduplicate scheduled jobs across replicas, and parse boolean environment flags strictly.

Key Changes

  • 5-field UTC cron parser & next-slot evaluator (src/services/scheduler.ts): Supports *, */n, a-b, a-b/n, and comma lists across minute, hour, day-of-month, month, and day-of-week (7 -> 0 Sunday normalization and logical AND semantics) evaluated strictly in UTC.
  • 32-bit timer overflow guard & bounded slot tracking (src/services/scheduler.ts): Multi-hop armTimerUntilSlot() clamps setTimeout delays to MAX_TIMEOUT_MS (2_147_483_647 ms) with pre-slot wake checks for monthly/yearly schedules, and bounds executedSlots / seenJobIdsByQueue to 1_024 entries.
  • Deterministic replica deduplication (src/services/scheduler.ts, src/index.ts, src/metrics/registry.ts): Enqueues reconciliation jobs with BullMQ-safe per-slot jobId values (reconcile_<ISO-UTC-minute> with colons replaced by -) and wires scheduler lifecycle hooks into startup/shutdown and metrics.
  • Strict boolean environment coercion (src/config/env-schema.ts, src/config/env.ts, docs/job-queue.md): Replaces z.coerce.boolean() with booleanFromEnv so "false", "0", "no", and "off" evaluate to false.

Verification & Notes

  • Unit & regression tests: npm test -- tests/scheduler.test.ts tests/env.test.ts tests/check-env.test.ts --coverage --collectCoverageFrom="src/services/scheduler.ts" passes 3/3 suites (38/38 tests; 98.00% stmts, 96.59% branches, 99.48% lines on src/services/scheduler.ts).
  • Env loader restoration (src/config/env.ts): On base main, tests/env.test.ts failed 4 assertions (JWT_TTL validation, error message, and warning log) because an earlier merge replaced envSchema.parse with safeParse + process.exit(1) and dropped the pino warning when JWT_TTL_SECONDS is within 10% of WORKER_HEARTBEAT_SECONDS * 2. src/config/env.ts restores throwing parsed.error when NODE_ENV !== "test" and logging the 10% threshold warning while keeping NODE_ENV === "test" tolerance intact.
  • Linter & TypeScript parity: All modified files pass npx eslint (0 errors/warnings) and introduce 0 new npx tsc --noEmit diagnostics (280 pre-existing on both 95d73d06 and 9afcf906).

…lica dedupe, and strict env booleans

Why: The reconciliation scheduler only matched literal numbers or wildcards in server-local time, polled via setInterval without replica deduplication or startup wiring, and z.coerce.boolean() treated 'false'/'0' as true.

What: Add dependency-free 5-field UTC cron parser and slot-based Scheduler with injectable clock/timers, per-slot guard, deterministic jobId ('reconciliation:<slot ISO minute>'), scheduler_runs_total counter, initializeScheduler() startup call, and envBoolean() helper.

Verification: npm test -- tests/scheduler.test.ts tests/env.test.ts tests/check-env.test.ts --coverage --collectCoverageFrom='src/services/scheduler.ts' (38/38 passed, 100% lines / 97.61% branches, exit 0).
…lot tracking sets

Why: Delays exceeding 2^31 - 1 ms (~24.85 days, e.g., monthly/yearly cron schedules) overflow Node's 32-bit timer and fire immediately at 1 ms, while unbounded slot sets grow over long uptime.

What: Split long delays across MAX_TIMEOUT_MS hops with pre-slot wake check, bound executedSlots and seenJobIdsByQueue to 1024 entries, and document AND field semantics and BullMQ 3-segment jobId format.

Verification: npm test -- tests/scheduler.test.ts tests/env.test.ts tests/check-env.test.ts --coverage --collectCoverageFrom='src/services/scheduler.ts' (38/38 passed, 99.48% lines / 96.59% branches, exit 0).
…terministicClock

Why: DeterministicClock accepted delays > 2^31 - 1 ms without clamping to 1 ms, so the monthly schedule test passed even against un-clamped setTimeout calls.

What: Record requested timer delays, emulate Node 32-bit timer overflow (> 2_147_483_647 ms -> 1 ms) in DeterministicClock, and assert recordedDelays <= MAX_TIMEOUT_MS.

Verification: Confirmed test fails on cd54e5c and passes on HEAD (38/38 passed, 99.48% lines / 96.59% branches).
@drips-wave

drips-wave Bot commented Oct 19, 2026

Copy link
Copy Markdown

@lhphong09t2 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@greatest0fallt1me
greatest0fallt1me merged commit eae1afc into Predictify-org:main Oct 19, 2026
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.

Make the reconciliation scheduler actually run - cron field parsing, UTC evaluation, replica dedupe and boolean env coercion

2 participants