| 1 | package chronos |
| 2 | |
| 3 | import ( |
| 4 | "time" |
| 5 | |
| 6 | "go.bigb.es/auxilia/culpa" |
| 7 | ) |
| 8 | |
| 9 | // JobFunc is the work a [Job] runs on each fire. It receives a |
| 10 | // [ChronosContext] scoped to the run (cancelled on timeout or |
| 11 | // shutdown) and returns an error that the supervisor records. |
| 12 | type JobFunc func(cc *ChronosContext) error |
| 13 | |
| 14 | // OverlapPolicy decides what happens when a [Job]'s fire time arrives |
| 15 | // while its previous run is still in flight. |
| 16 | type OverlapPolicy int |
| 17 | |
| 18 | const ( |
| 19 | // OverlapSkip drops the new fire when the prior run is still active. |
| 20 | OverlapSkip OverlapPolicy = iota |
| 21 | // OverlapAllow starts the new run concurrently with the prior one. |
| 22 | OverlapAllow |
| 23 | ) |
| 24 | |
| 25 | // Job is a schedule paired with the function to run and per-job |
| 26 | // options. Construct one with [NewCron] or [NewEvery], then refine it |
| 27 | // with the chained builders ([Job.Delta], [Job.Timeout], etc.). |
| 28 | // |
| 29 | // Construction errors are stashed in buildErr rather than panicked |
| 30 | // (IV6); they surface when the job is registered. The Must* variants |
| 31 | // panic instead. The paired *Set bools record whether [Job.Timeout] |
| 32 | // and [Job.SlotWait] were called, so the supervisor can tell an |
| 33 | // explicit zero from "unset" (GPC1). |
| 34 | type Job struct { |
| 35 | name string |
| 36 | sched Schedule |
| 37 | fn JobFunc |
| 38 | delta time.Duration |
| 39 | timeout time.Duration |
| 40 | timeoutSet bool |
| 41 | slotWait time.Duration |
| 42 | slotWaitSet bool |
| 43 | overlap OverlapPolicy |
| 44 | buildErr error |
| 45 | } |
| 46 | |
| 47 | // NewCron builds a [Job] from a cron spec (5- or 6-field, an @-macro, or |
| 48 | // "@every <dur>"). A malformed spec is stashed in buildErr (IV6) rather |
| 49 | // than panicked; see [MustCron] for the panicking variant. |
| 50 | func NewCron(spec string, fn JobFunc) *Job { |
| 51 | sched, err := parseSpec(spec) |
| 52 | return &Job{sched: sched, fn: fn, buildErr: err} |
| 53 | } |
| 54 | |
| 55 | // NewEvery builds a [Job] that fires every total of the given spans. |
| 56 | // The spans are summed component-wise (AS1); a non-positive total (empty, |
| 57 | // all-zero, or any negative component) is rejected via buildErr (IV6), |
| 58 | // since [everySchedule] requires a forward-moving span to honour the |
| 59 | // strictly-after contract (IV1). |
| 60 | func NewEvery(fn JobFunc, every ...Span) *Job { |
| 61 | var sum Span |
| 62 | for _, s := range every { |
| 63 | sum = sum.Add(s) |
| 64 | } |
| 65 | if sum.IsZero() || sum.Years < 0 || sum.Months < 0 || sum.Days < 0 || sum.Dur < 0 { |
| 66 | return &Job{fn: fn, buildErr: culpa.New("chronos: NewEvery requires a positive total span")} |
| 67 | } |
| 68 | return &Job{sched: everySchedule{span: sum}, fn: fn} |
| 69 | } |
| 70 | |
| 71 | // MustCron is [NewCron] that panics on a malformed spec. |
| 72 | func MustCron(spec string, fn JobFunc) *Job { |
| 73 | j := NewCron(spec, fn) |
| 74 | if j.buildErr != nil { |
| 75 | panic(j.buildErr) |
| 76 | } |
| 77 | return j |
| 78 | } |
| 79 | |
| 80 | // MustEvery is [NewEvery] that panics on an empty or all-zero span. |
| 81 | func MustEvery(fn JobFunc, every ...Span) *Job { |
| 82 | j := NewEvery(fn, every...) |
| 83 | if j.buildErr != nil { |
| 84 | panic(j.buildErr) |
| 85 | } |
| 86 | return j |
| 87 | } |
| 88 | |
| 89 | // Delta offsets each fire by d relative to the schedule's nominal time. |
| 90 | func (j *Job) Delta(d time.Duration) *Job { |
| 91 | j.delta = d |
| 92 | return j |
| 93 | } |
| 94 | |
| 95 | // Timeout caps each run at d; the run's [ChronosContext] is cancelled |
| 96 | // when it elapses. Calling it marks the timeout as explicitly set (GPC1). |
| 97 | func (j *Job) Timeout(d time.Duration) *Job { |
| 98 | j.timeout = d |
| 99 | j.timeoutSet = true |
| 100 | return j |
| 101 | } |
| 102 | |
| 103 | // SlotWait bounds how long a fire waits for an in-flight run before the |
| 104 | // overlap policy applies. Calling it marks slotWait as explicitly set. |
| 105 | func (j *Job) SlotWait(d time.Duration) *Job { |
| 106 | j.slotWait = d |
| 107 | j.slotWaitSet = true |
| 108 | return j |
| 109 | } |
| 110 | |
| 111 | // Name sets the job's name, used for logging and self-deactivation. |
| 112 | func (j *Job) Name(name string) *Job { |
| 113 | j.name = name |
| 114 | return j |
| 115 | } |
| 116 | |
| 117 | // OnOverlap sets the [OverlapPolicy] applied when a fire arrives during |
| 118 | // a still-running prior invocation. |
| 119 | func (j *Job) OnOverlap(p OverlapPolicy) *Job { |
| 120 | j.overlap = p |
| 121 | return j |
| 122 | } |
| 123 | |