Skip to content

Commit 03133f3

Browse files
committed
add new features, OneTimeJob and Job.RunNow()
1 parent f7cd2bc commit 03133f3

8 files changed

+418
-52
lines changed

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ Jobs can be run every x days at specific times.
8787
Jobs can be run every x weeks on specific days of the week and at specific times.
8888
- [**Monthly**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#MonthlyJob):
8989
Jobs can be run every x months on specific days of the month and at specific times.
90+
- [**One time**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#OneTimeJob):
91+
Jobs can be run once at a specific time. These are non-recurring jobs.
9092

9193
### Concurrency Limits
9294
Jobs can be limited individually or across the entire scheduler.

Diff for: errors.go

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var (
1212
ErrDurationRandomJobMinMax = fmt.Errorf("gocron: DurationRandomJob: minimum duration must be less than maximum duration")
1313
ErrEventListenerFuncNil = fmt.Errorf("gocron: eventListenerFunc must not be nil")
1414
ErrJobNotFound = fmt.Errorf("gocron: job not found")
15+
ErrJobRunNowFailed = fmt.Errorf("gocron: Job: RunNow: scheduler unreachable")
1516
ErrMonthlyJobDays = fmt.Errorf("gocron: MonthlyJob: daysOfTheMonth must be between 31 and -31 inclusive, and not 0")
1617
ErrMonthlyJobAtTimeNil = fmt.Errorf("gocron: MonthlyJob: atTime within atTimes must not be nil")
1718
ErrMonthlyJobAtTimesNil = fmt.Errorf("gocron: MonthlyJob: atTimes must not be nil")
@@ -22,6 +23,7 @@ var (
2223
ErrNewJobTaskNotFunc = fmt.Errorf("gocron: NewJob: Task.Function must be of kind reflect.Func")
2324
ErrNewJobWrongNumberOfParameters = fmt.Errorf("gocron: NewJob: Number of provided parameters does not match expected")
2425
ErrNewJobWrongTypeOfParameters = fmt.Errorf("gocron: NewJob: Type of provided parameters does not match expected")
26+
ErrOneTimeJobStartDateTimePast = fmt.Errorf("gocron: OneTimeJob: start must not be in the past")
2527
ErrStopExecutorTimedOut = fmt.Errorf("gocron: timed out waiting for executor to stop")
2628
ErrStopJobsTimedOut = fmt.Errorf("gocron: timed out waiting for jobs to finish")
2729
ErrStopSchedulerTimedOut = fmt.Errorf("gocron: timed out waiting for scheduler to stop")

Diff for: example_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,28 @@ func ExampleJob_NextRun() {
196196
fmt.Println(j.NextRun())
197197
}
198198

199+
func ExampleJob_RunNow() {
200+
s, _ := NewScheduler()
201+
defer func() { _ = s.Shutdown() }()
202+
203+
j, _ := s.NewJob(
204+
MonthlyJob(
205+
1,
206+
NewDaysOfTheMonth(3, -5, -1),
207+
NewAtTimes(
208+
NewAtTime(10, 30, 0),
209+
NewAtTime(11, 15, 0),
210+
),
211+
),
212+
NewTask(
213+
func() {},
214+
),
215+
)
216+
s.Start()
217+
// Runs the job one time now, without impacting the schedule
218+
_ = j.RunNow()
219+
}
220+
199221
func ExampleMonthlyJob() {
200222
s, _ := NewScheduler()
201223
defer func() { _ = s.Shutdown() }()
@@ -222,6 +244,32 @@ func ExampleNewScheduler() {
222244
fmt.Println(s.Jobs())
223245
}
224246

247+
func ExampleOneTimeJob() {
248+
s, _ := NewScheduler()
249+
defer func() { _ = s.Shutdown() }()
250+
251+
// run a job once, immediately
252+
_, _ = s.NewJob(
253+
OneTimeJob(
254+
OneTimeJobStartImmediately(),
255+
),
256+
NewTask(
257+
func() {},
258+
),
259+
)
260+
// run a job once in 10 seconds
261+
_, _ = s.NewJob(
262+
OneTimeJob(
263+
OneTimeJobStartDateTime(time.Now().Add(10*time.Second)),
264+
),
265+
NewTask(
266+
func() {},
267+
),
268+
)
269+
270+
s.Start()
271+
}
272+
225273
func ExampleScheduler_NewJob() {
226274
s, _ := NewScheduler()
227275
defer func() { _ = s.Shutdown() }()

Diff for: executor.go

+60-45
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type executor struct {
1414
cancel context.CancelFunc
1515
logger Logger
1616
stopCh chan struct{}
17-
jobsIDsIn chan uuid.UUID
17+
jobsIn chan jobIn
1818
jobIDsOut chan uuid.UUID
1919
jobOutRequest chan jobOutRequest
2020
stopTimeout time.Duration
@@ -25,8 +25,13 @@ type executor struct {
2525
locker Locker
2626
}
2727

28+
type jobIn struct {
29+
id uuid.UUID
30+
shouldSendOut bool
31+
}
32+
2833
type singletonRunner struct {
29-
in chan uuid.UUID
34+
in chan jobIn
3035
rescheduleLimiter chan struct{}
3136
}
3237

@@ -35,7 +40,7 @@ type limitModeConfig struct {
3540
mode LimitMode
3641
limit uint
3742
rescheduleLimiter chan struct{}
38-
in chan uuid.UUID
43+
in chan jobIn
3944
// singletonJobs is used to track singleton jobs that are running
4045
// in the limit mode runner. This is used to prevent the same job
4146
// from running multiple times across limit mode runners when both
@@ -72,7 +77,7 @@ func (e *executor) start() {
7277
// are run immediately.
7378
// 2. sent from time.AfterFuncs in which job schedules
7479
// are spun up by the scheduler
75-
case id := <-e.jobsIDsIn:
80+
case jIn := <-e.jobsIn:
7681
select {
7782
case <-e.stopCh:
7883
e.stop(standardJobsWg, singletonJobsWg, limitModeJobsWg)
@@ -111,14 +116,16 @@ func (e *executor) start() {
111116
// the executor from building up a waiting queue
112117
// and forces rescheduling
113118
case e.limitMode.rescheduleLimiter <- struct{}{}:
114-
e.limitMode.in <- id
119+
e.limitMode.in <- jIn
115120
default:
116121
// all runners are busy, reschedule the work for later
117122
// which means we just skip it here and do nothing
118123
// TODO when metrics are added, this should increment a rescheduled metric
119-
select {
120-
case e.jobIDsOut <- id:
121-
default:
124+
if jIn.shouldSendOut {
125+
select {
126+
case e.jobIDsOut <- jIn.id:
127+
default:
128+
}
122129
}
123130
}
124131
} else {
@@ -127,51 +134,53 @@ func (e *executor) start() {
127134
// to work through the channel backlog. A hard limit of 1000 is in place
128135
// at which point this call would block.
129136
// TODO when metrics are added, this should increment a wait metric
130-
e.limitMode.in <- id
137+
e.limitMode.in <- jIn
131138
}
132139
} else {
133140
// no limit mode, so we're either running a regular job or
134141
// a job with a singleton mode
135142
//
136143
// get the job, so we can figure out what kind it is and how
137144
// to execute it
138-
j := requestJobCtx(ctx, id, e.jobOutRequest)
145+
j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
139146
if j == nil {
140147
// safety check as it'd be strange bug if this occurred
141148
return
142149
}
143150
if j.singletonMode {
144151
// for singleton mode, get the existing runner for the job
145152
// or spin up a new one
146-
runner, ok := e.singletonRunners[id]
153+
runner, ok := e.singletonRunners[jIn.id]
147154
if !ok {
148-
runner.in = make(chan uuid.UUID, 1000)
155+
runner.in = make(chan jobIn, 1000)
149156
if j.singletonLimitMode == LimitModeReschedule {
150157
runner.rescheduleLimiter = make(chan struct{}, 1)
151158
}
152-
e.singletonRunners[id] = runner
159+
e.singletonRunners[jIn.id] = runner
153160
singletonJobsWg.Add(1)
154-
go e.singletonModeRunner("singleton-"+id.String(), runner.in, singletonJobsWg, j.singletonLimitMode, runner.rescheduleLimiter)
161+
go e.singletonModeRunner("singleton-"+jIn.id.String(), runner.in, singletonJobsWg, j.singletonLimitMode, runner.rescheduleLimiter)
155162
}
156163

157164
if j.singletonLimitMode == LimitModeReschedule {
158165
// reschedule mode uses the limiter channel to check
159166
// for a running job and reschedules if the channel is full.
160167
select {
161168
case runner.rescheduleLimiter <- struct{}{}:
162-
runner.in <- id
169+
runner.in <- jIn
163170
default:
164171
// runner is busy, reschedule the work for later
165172
// which means we just skip it here and do nothing
166173
// TODO when metrics are added, this should increment a rescheduled metric
167-
select {
168-
case e.jobIDsOut <- id:
169-
default:
174+
if jIn.shouldSendOut {
175+
select {
176+
case e.jobIDsOut <- jIn.id:
177+
default:
178+
}
170179
}
171180
}
172181
} else {
173182
// wait mode, fill up that queue (buffered channel, so it's ok)
174-
runner.in <- id
183+
runner.in <- jIn
175184
}
176185
} else {
177186
select {
@@ -187,7 +196,7 @@ func (e *executor) start() {
187196
// complete.
188197
standardJobsWg.Add(1)
189198
go func(j internalJob) {
190-
e.runJob(j)
199+
e.runJob(j, jIn.shouldSendOut)
191200
standardJobsWg.Done()
192201
}(*j)
193202
}
@@ -200,11 +209,11 @@ func (e *executor) start() {
200209
}
201210
}
202211

203-
func (e *executor) limitModeRunner(name string, in chan uuid.UUID, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {
212+
func (e *executor) limitModeRunner(name string, in chan jobIn, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {
204213
e.logger.Debug("gocron: limitModeRunner starting", "name", name)
205214
for {
206215
select {
207-
case id := <-in:
216+
case jIn := <-in:
208217
select {
209218
case <-e.ctx.Done():
210219
e.logger.Debug("gocron: limitModeRunner shutting down", "name", name)
@@ -214,24 +223,28 @@ func (e *executor) limitModeRunner(name string, in chan uuid.UUID, wg *waitGroup
214223
}
215224

216225
ctx, cancel := context.WithCancel(e.ctx)
217-
j := requestJobCtx(ctx, id, e.jobOutRequest)
226+
j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
218227
cancel()
219228
if j != nil {
220229
if j.singletonMode {
221230
e.limitMode.singletonJobsMu.Lock()
222-
_, ok := e.limitMode.singletonJobs[id]
231+
_, ok := e.limitMode.singletonJobs[jIn.id]
223232
if ok {
224233
// this job is already running, so don't run it
225234
// but instead reschedule it
226235
e.limitMode.singletonJobsMu.Unlock()
227-
select {
228-
case <-e.ctx.Done():
229-
return
230-
case <-j.ctx.Done():
231-
return
232-
case e.jobIDsOut <- j.id:
236+
if jIn.shouldSendOut {
237+
select {
238+
case <-e.ctx.Done():
239+
return
240+
case <-j.ctx.Done():
241+
return
242+
case e.jobIDsOut <- j.id:
243+
}
233244
}
234-
// remove the limiter block to allow another job to be scheduled
245+
// remove the limiter block, as this particular job
246+
// was a singleton already running, and we want to
247+
// allow another job to be scheduled
235248
if limitMode == LimitModeReschedule {
236249
select {
237250
case <-rescheduleLimiter:
@@ -240,14 +253,14 @@ func (e *executor) limitModeRunner(name string, in chan uuid.UUID, wg *waitGroup
240253
}
241254
continue
242255
}
243-
e.limitMode.singletonJobs[id] = struct{}{}
256+
e.limitMode.singletonJobs[jIn.id] = struct{}{}
244257
e.limitMode.singletonJobsMu.Unlock()
245258
}
246-
e.runJob(*j)
259+
e.runJob(*j, jIn.shouldSendOut)
247260

248261
if j.singletonMode {
249262
e.limitMode.singletonJobsMu.Lock()
250-
delete(e.limitMode.singletonJobs, id)
263+
delete(e.limitMode.singletonJobs, jIn.id)
251264
e.limitMode.singletonJobsMu.Unlock()
252265
}
253266
}
@@ -267,11 +280,11 @@ func (e *executor) limitModeRunner(name string, in chan uuid.UUID, wg *waitGroup
267280
}
268281
}
269282

270-
func (e *executor) singletonModeRunner(name string, in chan uuid.UUID, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {
283+
func (e *executor) singletonModeRunner(name string, in chan jobIn, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {
271284
e.logger.Debug("gocron: limitModeRunner starting", "name", name)
272285
for {
273286
select {
274-
case id := <-in:
287+
case jIn := <-in:
275288
select {
276289
case <-e.ctx.Done():
277290
e.logger.Debug("gocron: limitModeRunner shutting down", "name", name)
@@ -281,10 +294,10 @@ func (e *executor) singletonModeRunner(name string, in chan uuid.UUID, wg *waitG
281294
}
282295

283296
ctx, cancel := context.WithCancel(e.ctx)
284-
j := requestJobCtx(ctx, id, e.jobOutRequest)
297+
j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
285298
cancel()
286299
if j != nil {
287-
e.runJob(*j)
300+
e.runJob(*j, jIn.shouldSendOut)
288301
}
289302

290303
// remove the limiter block to allow another job to be scheduled
@@ -302,7 +315,7 @@ func (e *executor) singletonModeRunner(name string, in chan uuid.UUID, wg *waitG
302315
}
303316
}
304317

305-
func (e *executor) runJob(j internalJob) {
318+
func (e *executor) runJob(j internalJob, shouldSendOut bool) {
306319
if j.ctx == nil {
307320
return
308321
}
@@ -327,12 +340,14 @@ func (e *executor) runJob(j internalJob) {
327340
}
328341
_ = callJobFuncWithParams(j.beforeJobRuns, j.id, j.name)
329342

330-
select {
331-
case <-e.ctx.Done():
332-
return
333-
case <-j.ctx.Done():
334-
return
335-
case e.jobIDsOut <- j.id:
343+
if shouldSendOut {
344+
select {
345+
case <-e.ctx.Done():
346+
return
347+
case <-j.ctx.Done():
348+
return
349+
case e.jobIDsOut <- j.id:
350+
}
336351
}
337352

338353
err := callJobFuncWithParams(j.function, j.parameters...)

0 commit comments

Comments
 (0)