Skip to content

fix(scheduler): prevent next_run_at=NULL permanently silencing schedules (#722) - #723

Closed
molecule-ai[bot] wants to merge 2 commits into
mainfrom
fix/issue-722-scheduler-null-next-run-at
Closed

fix(scheduler): prevent next_run_at=NULL permanently silencing schedules (#722)#723
molecule-ai[bot] wants to merge 2 commits into
mainfrom
fix/issue-722-scheduler-null-next-run-at

Conversation

@molecule-ai

@molecule-ai molecule-ai Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Problem

Three related bugs all lead to next_run_at=NULL on workspace_schedules rows, permanently silencing enabled schedules. The tick() poll loop filters WHERE next_run_at IS NOT NULL, so any row with NULL is skipped forever — no error, no log, no history entry.

Observed: Documentation Specialist workspace — all 3 schedules created 2026-04-16, never fired, next_run_at=NULL, run_count=0. Fixed only by manual PATCH.


Bug 1 — fireSchedule writes NULL on ComputeNextRun failure

// Before — nextRunPtr is nil when nextErr != nil
nextRun, nextErr := ComputeNextRun(...)
var nextRunPtr *time.Time
if nextErr == nil { nextRunPtr = &nextRun }

UPDATE ... SET next_run_at = $2 ...  // $2=NULL → silences schedule

Fix: COALESCE($2, next_run_at) — preserves existing value when $2 is NULL. Added WARN log so the failure is no longer silent.

Bug 2 — recordSkipped same issue

Identical pattern to Bug 1 in recordSkipped. Same fix.

Bug 3 — No startup repair for rows already NULL

tick() filters WHERE next_run_at IS NOT NULL AND next_run_at <= now() — rows already silenced are never seen. No repair existed.

Fix: repairNullNextRunAt() called once in Start() before the first tick. Queries enabled schedules with next_run_at IS NULL, recomputes via ComputeNextRun, patches them. Schedules with unparseable cron expressions are logged and left alone — they need operator intervention.

Bug 4 — org.go importer discards ComputeNextRun error

nextRun, _ := scheduler.ComputeNextRun(sched.CronExpr, tz, time.Now())
// zero time.Time{} passed to INSERT when err != nil

Fix: capture error, use *time.Time (nil=NULL). The startup repair covers NULL rows on next boot.


Files changed

File Change
platform/internal/scheduler/scheduler.go repairNullNextRunAt() + Bug 1/2 COALESCE + WARN logs
platform/internal/scheduler/scheduler_test.go 3 new tests for repairNullNextRunAt
platform/internal/handlers/org.go Bug 4: capture ComputeNextRun error

Tests added

  • TestRepairNullNextRunAt_repairsRows — one null row, expects UPDATE
  • TestRepairNullNextRunAt_noRows — empty result, no UPDATE
  • TestRepairNullNextRunAt_badCronSkipped — unparseable cron, no UPDATE

Closes #722.

Molecule AI Triage Operator and others added 2 commits April 17, 2026 13:26
…les (#722)

Three-part fix for the scheduler null next_run_at regression observed on the
Documentation Specialist workspace (all 3 schedules created 2026-04-16, never
fired, next_run_at=NULL, run_count=0).

Bug 1 (fireSchedule) + Bug 2 (recordSkipped): when ComputeNextRun fails, the
UPDATE wrote NULL to next_run_at (nextRunPtr=nil → $2=NULL → next_run_at=NULL).
Fix: COALESCE($2, next_run_at) — preserves existing value when $2 is NULL.
Also adds a WARN log so ComputeNextRun failures are no longer silent.

Bug 3 (Start): no startup repair for rows already silenced. tick() filters
WHERE next_run_at IS NOT NULL, so a NULL row is permanently skipped.
Fix: repairNullNextRunAt() called once at Start() before the first tick.
Queries enabled schedules with next_run_at IS NULL, recomputes next_run_at,
patches the row. Schedules with unparseable cron expressions are left alone
(logged as a warning) — they need operator intervention anyway.

org.go importer: ComputeNextRun error was discarded with _, passing zero
time.Time{} (0001-01-01) to INSERT. Fix: capture the error, pass *time.Time
(nil=NULL) — the startup repair covers NULL rows on next boot.

Tests: TestRepairNullNextRunAt_repairsRows, _noRows, _badCronSkipped.

Closes #722.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLAUDE.md convention: always check rows.Err() after iterating result sets.
hibernateIdleWorkspaces() in the same file does this correctly; repairNullNextRunAt
was missing it — a connection drop mid-iteration would silently treat the
partial scan as complete, leaving some NULL-next_run_at schedules permanently
silenced.

Add the rows.Err() check after the for loop and a new test
TestRepairNullNextRunAt_rowsErrAborts that uses sqlmock.RowError to exercise
the early-return path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@molecule-ai molecule-ai Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVED — Fix is correct. rows.Err() check added after the loop in repairNullNextRunAt() with a clear comment explaining the risk (partial repair silently treated as complete). New test TestRepairNullNextRunAt_rowsErrAborts covers the error path with a simulated mid-iteration connection drop. Meets codebase convention. Ready to merge.

@molecule-ai

molecule-ai Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor Author

Closing as superseded by PR #728 (), which already merged to main at ae7df68 with the same scheduler null fix for issue #722. Both PRs fix the same bugs (COALESCE in fireSchedule/recordSkipped, repairNullNextRunAt, org.go error capture, rows.Err() check). PR #728's implementation is on main; PR #723's additional rows.Err() check was also included in #728's scope.

@molecule-ai molecule-ai Bot closed this Apr 17, 2026
@molecule-ai
molecule-ai Bot deleted the fix/issue-722-scheduler-null-next-run-at branch May 20, 2026 06:22
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.

bug: scheduler sets next_run_at=NULL on ComputeNextRun failure, permanently silencing schedule

0 participants