Skip to content

fix periodic job uniqueness at period boundaries - #1427

Draft
bgentry wants to merge 1 commit into
masterfrom
bg/periodic-job-period-uniqueness
Draft

bgentry wants to merge 1 commit into
masterfrom
bg/periodic-job-period-uniqueness

Conversation

@bgentry

@bgentry bgentry commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

An hourly periodic job with UniqueOpts{ByPeriod: time.Hour} can be skipped at noon even when the 11:00 job has already finished. River uses one timer for all periodic jobs, and whenever that timer wakes it also inserts jobs due within the next 100 milliseconds. For example, a separate PeriodicInterval(time.Minute) job scheduled when a leader starts at 11:58:59.950 wakes the enqueuer at 11:59:59.950. The hourly cron occurrence due at 12:00:00 is then eligible to be inserted 50 milliseconds early.

Before this fix, the periodic constructor builds the insert parameters and unique key before the enqueuer supplies the occurrence's scheduled time. With no explicit ScheduledAt, it rounds the insertion time of 11:59:59.950 down to the 11:00 period. If the 11:00 job is still in the database, the noon insert is skipped as a duplicate: completed jobs are included in the default uniqueness states. The noon occurrence is advanced to the next hour, so it is lost. This is why the bug can occur during an ordinary timer wake, without any clock skew or process failure.

Early insertion can also undermine deduplication during an OSS leader handoff. If the first leader inserts the noon occurrence before noon and the next leader starts just before noon, both independently schedule that same occurrence. The new leader's timer can fire at or after noon, producing the 12:00 key instead of the first leader's 11:00 key. If no existing 11:00 key prevented the first insert, both copies can be inserted. Durable periodic jobs commit the job insert and next-run update together; the durable regression deliberately replays an occurrence through a pilot mock to check key stability across the boundary.

Pass the occurrence time through the internal periodic constructor and use it as a fallback in a copy of the constructor options before computing the unique key. Keep the shared insert-param helper and ordinary insertion callers unchanged. Explicit constructor and job-argument schedules retain precedence, and the periodic adapter preserves available or pending state when it supplies the occurrence default. The regression test seeds the previous hour's job, adds another periodic schedule to wake the shared timer early, and freezes the insertion clock just before noon. It checks that the noon occurrence gets its own period key, and that explicit schedules still win.

Comment thread client.go Outdated
}

func insertParamsFromConfigArgsAndOptions(archetype *baseservice.Archetype, config *Config, args JobArgs, insertOpts *InsertOpts) (*rivertype.JobInsertParams, error) {
func insertParamsFromConfigArgsAndOptions(archetype *baseservice.Archetype, config *Config, args JobArgs, insertOpts *InsertOpts, defaultScheduledAt *time.Time) (*rivertype.JobInsertParams, error) {

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.

I don't like this change, going to iterate on this more when I get some time (or feel free to do so yourself).

Periodic occurrences can be inserted just before their scheduled time.
With `UniqueOpts.ByPeriod`, computing the key before applying that
schedule can place the occurrence in the previous period. This can
skip a valid occurrence or insert it twice across a leader handoff.

Pass the occurrence time into the internal periodic constructor and
apply it to copied insertion options before generating the unique key.
Keep explicit constructor and job-argument schedules ahead of the
occurrence default, and preserve available and pending states for
periodic defaults.

Cover early inserts caused by another periodic schedule's timer wake,
explicit schedule precedence, reused constructor options, and durable
occurrence restores on either side of a period boundary.
@bgentry
bgentry force-pushed the bg/periodic-job-period-uniqueness branch from d41036e to 2f43a0d Compare October 4, 2026 19:15
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