Optionally recover a recently-missed tick when an entry is re-registered late (env-gated, opt-in) - #3
Open
s-kondamuri wants to merge 5 commits into
Open
Optionally recover a recently-missed tick when an entry is re-registered late (env-gated, opt-in)#3s-kondamuri wants to merge 5 commits into
s-kondamuri wants to merge 5 commits into
Conversation
…st after a recently-missed tick
…hRefID and RecoveryObserver
…elper ScheduleFirst had recovery logic nested 4 levels deep inside the existing Prev-is-zero branch. Extracts it into recoveryFireTime, a small guard-clause-only helper that reports whether (and when) to fire immediately, leaving ScheduleFirst itself as two flat early returns. Pure refactor - every branch produces the same result as before (verified against the existing test suite, no test changes needed). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Author
|
Flattened `ScheduleFirst`'s nested recovery check into a guard clause + a small `recoveryFireTime` helper (40c00d9) - pure refactor, no behavior change (verified branch-by-branch against the existing test suite, which passes unchanged). 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Entry.ScheduleFirst(called whenever an entry is (re-)registered while thescheduler is already running, e.g.
Cron.AddJobon a liveCron) computes:NextWithAfter(t, after)starts its search from whichever of its twoarguments is later. Since
nowis virtually always later thanPrev, thesearch effectively always starts from
now— so if the entry's natural nexttick (
Schedule.Next(Prev)) already passed by the time it's (re-)registered,it is silently skipped in favor of the following occurrence. No error, no
signal that anything was missed.
This is most likely to bite right after a process restart: a consumer's
in-memory "already registered" bookkeeping resets, so every entry has to be
freshly re-registered at once, and depending on how long that takes relative
to any given entry's next due tick, some entries can land in this exact gap
purely by timing.
We hit this concretely in
segmentio/reverse-etl's scheduler-worker — acustomer's scheduled Reverse ETL sync silently didn't fire because its entry
was re-registered 34 seconds after its scheduled instant, following an
OOM-kill restart. Full writeup / evidence:
SRC-4984.Fix
Opt-in only, so existing consumers of this library are unaffected unless they
explicitly enable it:
RECOVERY_TIME_LIMIT(atime.ParseDurationstring, e.g.20m). Unset/invalid →0→ disabled,ScheduleFirstbehaves exactlyas before.
Schedule.Next(Prev)) is foundto have already passed, but only within that limit,
ScheduleFirstschedules it to fire ~30s from now instead of rolling forward silently to
the next occurrence. Misses older than the limit are left untouched
(rolled forward as before) — this intentionally never causes a "catch-up
burst," it only recovers the single most recent miss.
Entry.RefIDfield +WithRefID(id string) EntryOption: an opaque,caller-defined identifier attached to an entry. This package never
interprets it.
RecoveryObserver func(refID string, recovered bool, naturalNext, now time.Time)package var (nil/no-op by default). When
RECOVERY_TIME_LIMITis set,ScheduleFirstcalls it once for every detected miss —recovered=truewhen it was fired immediately,
recovered=falsewhen it was too old andleft to roll forward — passing along whatever
RefIDthe caller attachedvia
WithRefID. Lets a caller emit its own metrics/logs (e.g. amissed_tick_recovered/missed_tick_unrecoveredcounter tagged byrefID) without this package needing to know what a "source" or"subscription" is. In reverse-etl's case
RefIDwould be something likeretl:<source_id>:<subscription_id>.Tests
retl_recovery_test.gocovers: recovers a recent miss, leaves an older missalone, confirms behavior is byte-for-byte unchanged when the env var is
unset (default), env var parsing, and
RecoveryObserverfiring correctly(recovered / unrecovered / not-called-when-not-due), with
RefIDpassed through.
Compatibility
No breaking API changes —
RefID/WithRefID/RecoveryObserverareadditive. No behavior change for any consumer that doesn't set
RECOVERY_TIME_LIMIT.