Skip to content

Commit b3e23c6

Browse files
committed
support continuous schedule
1 parent 1350b17 commit b3e23c6

File tree

3 files changed

+83
-56
lines changed

3 files changed

+83
-56
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ require (
2929
github.com/go-audio/audio v1.0.0
3030
github.com/go-audio/transforms v0.0.0-20180121090939-51830ccc35a5
3131
github.com/go-audio/wav v1.1.0
32-
github.com/go-co-op/gocron/v2 v2.16.2
32+
github.com/go-co-op/gocron/v2 v2.18.0
3333
github.com/go-git/go-git/v5 v5.16.2
3434
github.com/go-gl/mathgl v1.0.0
3535
github.com/go-nlopt/nlopt v0.0.0-20230219125344-443d3362dcb5
@@ -397,7 +397,7 @@ require (
397397
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
398398
github.com/stoewer/go-strcase v1.3.0 // indirect
399399
github.com/stretchr/objx v0.5.2 // indirect
400-
github.com/stretchr/testify v1.10.0 // indirect
400+
github.com/stretchr/testify v1.11.1 // indirect
401401
github.com/subosito/gotenv v1.4.1 // indirect
402402
github.com/tdakkota/asciicheck v0.2.0 // indirect
403403
github.com/tetafro/godot v1.4.17 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,8 @@ github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
464464
github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE=
465465
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
466466
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
467-
github.com/go-co-op/gocron/v2 v2.16.2 h1:r08P663ikXiulLT9XaabkLypL/W9MoCIbqgQoAutyX4=
468-
github.com/go-co-op/gocron/v2 v2.16.2/go.mod h1:4YTLGCCAH75A5RlQ6q+h+VacO7CgjkgP0EJ+BEOXRSI=
467+
github.com/go-co-op/gocron/v2 v2.18.0 h1:DS3Uhru66q1jy/5f9V0itmi3cLXcn2b7N+duGfgT7gU=
468+
github.com/go-co-op/gocron/v2 v2.18.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
469469
github.com/go-critic/go-critic v0.5.4/go.mod h1:cjB4YGw+n/+X8gREApej7150Uyy1Tg8If6F2XOAUXNE=
470470
github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My6OyEU=
471471
github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc=
@@ -1392,8 +1392,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
13921392
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
13931393
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
13941394
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
1395-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
1396-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
1395+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
1396+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
13971397
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
13981398
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
13991399
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=

robot/jobmanager/jobmanager.go

Lines changed: 77 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,13 @@ func (jm *JobManager) createDescriptorSourceAndgRPCMethod(
206206
}
207207

208208
// createJobFunction returns a function that the job scheduler puts on its queue.
209-
func (jm *JobManager) createJobFunction(jc config.JobConfig) func(ctx context.Context) error {
209+
func (jm *JobManager) createJobFunction(jc config.JobConfig, continuous bool) func(ctx context.Context) error {
210210
jobLogger := jm.logger.Sublogger(jc.Name)
211211
// To support logging for quick jobs (~ on the seconds schedule), we disable log
212212
// deduplication for job loggers.
213213
jobLogger.NeverDeduplicate()
214-
return func(ctx context.Context) error {
214+
215+
jobFunc := func(ctx context.Context) error {
215216
res, err := jm.getResource(jc.Resource)
216217
if err != nil {
217218
jobLogger.CWarnw(jm.ctx, "Could not get resource", "error", err.Error())
@@ -272,6 +273,7 @@ func (jm *JobManager) createJobFunction(jc config.JobConfig) func(ctx context.Co
272273
jobLogger.CWarnw(jm.ctx, "Job failed", "error", err.Error())
273274
return err
274275
} else if h.Status != nil && h.Status.Err() != nil {
276+
// if job panics, it seems to be captured here.
275277
jobLogger.CWarnw(jm.ctx, "Job failed", "error", h.Status.Err())
276278
return h.Status.Err()
277279
}
@@ -284,6 +286,34 @@ func (jm *JobManager) createJobFunction(jc config.JobConfig) func(ctx context.Co
284286
jobLogger.CDebugw(jm.ctx, "Job succeeded", "response", response)
285287
return nil
286288
}
289+
290+
return func(ctx context.Context) error {
291+
var err error
292+
for {
293+
select {
294+
case <-ctx.Done():
295+
// Job cancelled (e.g. from schedule modification)
296+
return err
297+
case <-jm.ctx.Done():
298+
// JM shutting down
299+
return err
300+
default:
301+
}
302+
err = jobFunc(ctx)
303+
now := timestamppb.Now()
304+
if jh, ok := jm.JobHistories.Load(jc.Name); ok {
305+
if err != nil {
306+
// this includes captured panics (from InvokeRPC).
307+
jh.AddFailure(now)
308+
} else {
309+
jh.AddSuccess(now)
310+
}
311+
}
312+
if !continuous {
313+
return err
314+
}
315+
}
316+
}
287317
}
288318

289319
// removeJob removes the job from the scheduler and clears the internal map entry.
@@ -307,32 +337,21 @@ func (jm *JobManager) scheduleJob(jc config.JobConfig, verbose bool) {
307337
return
308338
}
309339

310-
var jobType gocron.JobDefinition
311-
var jobLimitMode gocron.LimitMode
312-
t, err := time.ParseDuration(jc.Schedule)
313-
if err != nil {
314-
withSeconds := len(strings.Split(jc.Schedule, " ")) >= 6
315-
jobType = gocron.CronJob(jc.Schedule, withSeconds)
316-
jobLimitMode = gocron.LimitModeReschedule
340+
var continuous bool
341+
var jobDefinition gocron.JobDefinition
342+
var jobOptions []gocron.JobOption
343+
if strings.ToLower(jc.Schedule) == "continuous" {
344+
continuous = true
345+
// used with WithIntervalFromCompletion: if job unexpectedly exits, try to restart later.
346+
// since we capture panics, this is largely unused, but helps reduce scheduler overhead.
347+
jobDefinition = gocron.DurationJob(time.Second * 5)
348+
jobOptions = append(jobOptions,
349+
// don't queue up if running
350+
gocron.WithSingletonMode(gocron.LimitModeReschedule),
351+
gocron.WithIntervalFromCompletion())
317352
} else {
318-
jobType = gocron.DurationJob(t)
319-
jobLimitMode = gocron.LimitModeWait
320-
}
321-
322-
if _, ok := jm.JobHistories.Load(jc.Name); !ok {
323-
jm.JobHistories.Store(jc.Name, &JobHistory{
324-
successTimes: ring.New(historyLength),
325-
failureTimes: ring.New(historyLength),
326-
})
327-
jm.NumJobHistories.Add(1)
328-
}
329-
330-
jobLogger := jm.logger.Sublogger(jc.Name)
331-
332-
jobFunc := jm.createJobFunction(jc)
333-
j, err := jm.scheduler.NewJob(
334-
jobType,
335-
gocron.NewTask(jobFunc),
353+
// Regular gocron-supported modes:
354+
//
336355
// WithSingletonMode option allows us to perform jobs on the same schedule
337356
// sequentially. This will guarantee that there is only one instance of a particular
338357
// job running at the same time. If a job reaches its schedule while the previous
@@ -361,30 +380,38 @@ func (jm *JobManager) scheduleJob(jc config.JobConfig, verbose bool) {
361380

362381
// It is also important to note that DURATION jobs start relative to when they were
363382
// queued on the job scheduler, while CRON jobs are tied to the physical clock.
364-
gocron.WithSingletonMode(jobLimitMode),
383+
t, err := time.ParseDuration(jc.Schedule)
384+
if err != nil {
385+
// TODO: exit if cron job is also invalid. Currently it's stored as an invalid string and validated at NewJob call.
386+
withSeconds := len(strings.Split(jc.Schedule, " ")) >= 6
387+
jobDefinition = gocron.CronJob(jc.Schedule, withSeconds)
388+
jobOptions = append(jobOptions, gocron.WithSingletonMode(gocron.LimitModeReschedule))
389+
390+
} else {
391+
jobDefinition = gocron.DurationJob(t)
392+
jobOptions = append(jobOptions, gocron.WithSingletonMode(gocron.LimitModeWait))
393+
}
394+
395+
if _, ok := jm.JobHistories.Load(jc.Name); !ok {
396+
jm.JobHistories.Store(jc.Name, &JobHistory{
397+
successTimes: ring.New(historyLength),
398+
failureTimes: ring.New(historyLength),
399+
})
400+
jm.NumJobHistories.Add(1)
401+
}
402+
}
403+
404+
jobOptions = append(jobOptions,
365405
gocron.WithName(jc.Name),
366-
gocron.WithContext(jm.ctx),
367-
gocron.WithEventListeners(
368-
// May be slightly more accurate to use j.LastRun(), but we don't have direct reference to it here, and we don't want the job to
369-
// complete before we can store the returned Job.
370-
gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
371-
now := timestamppb.Now()
372-
if jh, ok := jm.JobHistories.Load(jobName); ok {
373-
jh.AddSuccess(now)
374-
}
375-
}),
376-
gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
377-
now := timestamppb.Now()
378-
if jh, ok := jm.JobHistories.Load(jobName); ok {
379-
jh.AddFailure(now)
380-
}
381-
}),
382-
gocron.AfterJobRunsWithPanic(func(jobID uuid.UUID, jobName string, recoverData any) {
383-
now := timestamppb.Now()
384-
if jh, ok := jm.JobHistories.Load(jobName); ok {
385-
jh.AddFailure(now)
386-
}
387-
})),
406+
gocron.WithContext(jm.ctx))
407+
408+
jobLogger := jm.logger.Sublogger(jc.Name)
409+
410+
jobFunc := jm.createJobFunction(jc, continuous)
411+
j, err := jm.scheduler.NewJob(
412+
jobDefinition,
413+
gocron.NewTask(jobFunc),
414+
jobOptions...,
388415
)
389416
if err != nil {
390417
jobLogger.CErrorw(jm.ctx, "Failed to create a new job", "name", jc.Name, "error", err.Error())

0 commit comments

Comments
 (0)