Skip to content

Commit d33602b

Browse files
committed
fix: resolve duplicate job execution due to timer issues
1 parent 5a5658e commit d33602b

2 files changed

Lines changed: 15 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ All notable changes to **NCronJob** will be documented in this file. The project
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- Fixed duplicate job execution caused by system timers firing slightly before the scheduled cron time, resulting in the same cron slot being scheduled again. Reported by [@RShergold](https://github.com/RShergold) in [#327](https://github.com/NCronJob-Dev/NCronJob/issues/327). Fixed by [@linkdotnet](https://github.com/linkdotnet).
12+
913
## [v4.10.0] - 2026-03-30
1014

1115
### Added

src/NCronJob/Scheduler/JobWorker.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ private async Task DispatchJobForProcessing(
110110

111111
if (!nextJob.IsOneTimeJob)
112112
{
113-
ScheduleJob(nextJob.JobDefinition);
113+
ScheduleJob(nextJob.JobDefinition, nextRunTime);
114114
}
115115

116116
shouldReleaseSemaphore = false;
@@ -189,15 +189,23 @@ private bool CanStartJob(JobDefinition jobEntry)
189189
private void UpdateRunningJobCount(string jobFullName, int change) =>
190190
runningJobCounts.AddOrUpdate(jobFullName, change, (_, existingVal) => Math.Max(0, existingVal + change));
191191

192-
public void ScheduleJob(JobDefinition job)
192+
public void ScheduleJob(JobDefinition job, DateTimeOffset? lastScheduledRunTime = null)
193193
{
194194
if (!job.IsEnabled)
195195
{
196196
return;
197197
}
198198

199199
var utcNow = timeProvider.GetUtcNow();
200-
var nextRunTime = job.GetNextCronOccurrence(utcNow);
200+
201+
// When rescheduling after a job fires, the timer may have triggered slightly
202+
// before the scheduled time. Using utcNow directly could return the same cron
203+
// slot again, causing duplicate execution. Using the later of utcNow and the
204+
// last scheduled run time guarantees we always advance past the fired slot.
205+
var baseTime = lastScheduledRunTime.HasValue && lastScheduledRunTime.Value > utcNow
206+
? lastScheduledRunTime.Value
207+
: utcNow;
208+
var nextRunTime = job.GetNextCronOccurrence(baseTime);
201209

202210
if (!nextRunTime.HasValue)
203211
{

0 commit comments

Comments
 (0)