Skip to content

Fix cron overlap skipping by persisting executionJobId#21

Open
leventov wants to merge 1 commit intoget-convex:mainfrom
leventov:fix/execution-job-id
Open

Fix cron overlap skipping by persisting executionJobId#21
leventov wants to merge 1 commit intoget-convex:mainfrom
leventov:fix/execution-job-id

Conversation

@leventov
Copy link
Copy Markdown

@leventov leventov commented Feb 25, 2026

Problem

rescheduler tries to prevent overlapping cron executions by checking cron.executionJobId against _scheduled_functions (skip if the previous execution is still pending/
inProgress). However, the component never persisted the job id returned from ctx.scheduler.runAfter(...), so executionJobId stayed unset and the “still running, skipping” logic never
triggered.

Change

  • Capture the scheduled function id returned by runAfter.
  • Persist it on the cron document as executionJobId (in src/component/public.ts), alongside scheduling the next schedulerJobId.

Notes

del already attempts to cancel executionJobId; this makes that cancellation effective too.

Summary by CodeRabbit

  • Refactor
    • Enhanced the job scheduling system to better track and persist execution information across scheduling cycles. The scheduling mechanism now more reliably captures and maintains execution identifiers when determining the next scheduled run, resulting in improved consistency and overall reliability of scheduled task management throughout the complete lifecycle of recurring scheduled jobs.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

This change enhances a scheduling system by adding an optional parameter to the scheduleNextRun function, allowing callers to patch additional fields like executionJobId. The rescheduler now captures the execution job ID returned from the previous cron execution and persists it for the next scheduling cycle by merging it with existing scheduler data.

Changes

Cohort / File(s) Summary
Scheduling Enhancement
src/component/public.ts
Added optional extraPatch parameter to scheduleNextRun to support patching additional fields. Updated db.patch logic to merge schedulerJobId with extraPatch. Modified rescheduler to capture executionJobId from cron execution and pass it via extraPatch when scheduling the next run.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 The scheduler hops with delight,
Chasing execution IDs into the night,
Each cycle persists what came before,
Extra patches patch the core,
No data lost in this refined flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: persisting executionJobId to fix cron overlap skipping. It matches the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/component/public.ts (2)

296-315: Consider consolidating the two scheduleNextRun calls for readability.

The early return inside the else block means line 315 is only reachable when stillRunning is true, which readers must mentally verify. Moving scheduleNextRun inside each branch eliminates the implicit fall-through and makes the two paths self-contained:

♻️ Proposed refactor
     if (stillRunning) {
       console.log(`Cron ${cronJob._id} still running, skipping this run.`);
+      await scheduleNextRun(ctx, id, new Date(schedulerJob.scheduledTime), cronJob.schedule);
     } else {
       console.log(`Running cron ${cronJob._id}.`);
       const executionJobId = await ctx.scheduler.runAfter(
         0,
         cronJob.functionHandle as FunctionHandle<"mutation" | "action">,
         cronJob.args,
       );
       await scheduleNextRun(
         ctx,
         id,
         new Date(schedulerJob.scheduledTime),
         cronJob.schedule,
         { executionJobId },
       );
-      return;
     }
-
-    await scheduleNextRun(ctx, id, new Date(schedulerJob.scheduledTime), cronJob.schedule);
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/component/public.ts` around lines 296 - 315, The two calls to
scheduleNextRun are split between the else branch (after creating
executionJobId) and after the if/else, making control flow implicit; refactor so
each branch calls scheduleNextRun explicitly: in the stillRunning branch call
await scheduleNextRun(ctx, id, new Date(schedulerJob.scheduledTime),
cronJob.schedule) and in the running branch call await scheduleNextRun(ctx, id,
new Date(schedulerJob.scheduledTime), cronJob.schedule, { executionJobId })
immediately after obtaining executionJobId from ctx.scheduler.runAfter; keep use
of cronJob._id, stillRunning, ctx.scheduler.runAfter, executionJobId, and
scheduleNextRun unchanged.

128-136: Partial<...> type permits silent field removal via db.patch.

Partial<Pick<Doc<"crons">, "executionJobId">> allows callers to pass { executionJobId: undefined }. In Convex, passing { a: undefined } to db.patch removes the field from the document, while passing {} does not change it — so that variant would silently wipe executionJobId rather than being a no-op.

No current call site does this, but the type signature permits it. Use a non-optional field type in the patch object to close the gap:

♻️ Proposed fix: tighten the type to eliminate the footgun
 async function scheduleNextRun(
   ctx: MutationCtx,
   id: Id<"crons">,
   lastScheduled: Date,
   schedule: Schedule,
-  extraPatch?: Partial<Pick<Doc<"crons">, "executionJobId">>,
+  executionJobId?: Id<"_scheduled_functions">,
 ) {
   const nextRun = calculateNextRun(lastScheduled, schedule);
   const schedulerJobId = await ctx.scheduler.runAt(
     nextRun,
     internal.public.rescheduler,
     { id },
   );
-  await ctx.db.patch(id, { schedulerJobId, ...extraPatch });
+  await ctx.db.patch(id, { schedulerJobId, ...(executionJobId !== undefined && { executionJobId }) });
 }

And update the call site in rescheduler:

-      await scheduleNextRun(
-        ctx,
-        id,
-        new Date(schedulerJob.scheduledTime),
-        cronJob.schedule,
-        { executionJobId },
-      );
+      await scheduleNextRun(ctx, id, new Date(schedulerJob.scheduledTime), cronJob.schedule, executionJobId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/component/public.ts` around lines 128 - 136, The extraPatch parameter
type currently allows callers to pass { executionJobId: undefined } which Convex
interprets as deleting the field; replace the optional
Partial<Pick<Doc<"crons">, "executionJobId">> with an explicit optional property
that forbids undefined values (e.g. extraPatch?: { executionJobId?:
Doc<"crons">["executionJobId"] }), update any call sites such as the rescheduler
invocation to pass either a concrete executionJobId value or omit the property,
and ensure the ctx.db.patch(id, { schedulerJobId, ...extraPatch }) call only
receives defined executionJobId values so fields are not accidentally removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/component/public.ts`:
- Around line 296-315: The two calls to scheduleNextRun are split between the
else branch (after creating executionJobId) and after the if/else, making
control flow implicit; refactor so each branch calls scheduleNextRun explicitly:
in the stillRunning branch call await scheduleNextRun(ctx, id, new
Date(schedulerJob.scheduledTime), cronJob.schedule) and in the running branch
call await scheduleNextRun(ctx, id, new Date(schedulerJob.scheduledTime),
cronJob.schedule, { executionJobId }) immediately after obtaining executionJobId
from ctx.scheduler.runAfter; keep use of cronJob._id, stillRunning,
ctx.scheduler.runAfter, executionJobId, and scheduleNextRun unchanged.
- Around line 128-136: The extraPatch parameter type currently allows callers to
pass { executionJobId: undefined } which Convex interprets as deleting the
field; replace the optional Partial<Pick<Doc<"crons">, "executionJobId">> with
an explicit optional property that forbids undefined values (e.g. extraPatch?: {
executionJobId?: Doc<"crons">["executionJobId"] }), update any call sites such
as the rescheduler invocation to pass either a concrete executionJobId value or
omit the property, and ensure the ctx.db.patch(id, { schedulerJobId,
...extraPatch }) call only receives defined executionJobId values so fields are
not accidentally removed.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d0e559 and bf29564.

📒 Files selected for processing (1)
  • src/component/public.ts

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