mirror of
https://github.com/zrepl/zrepl.git
synced 2024-11-23 00:43:51 +01:00
178 lines
3.4 KiB
Go
178 lines
3.4 KiB
Go
package jobrun
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Logger interface {
|
|
Printf(format string, v ...interface{})
|
|
}
|
|
|
|
type jobLogger struct {
|
|
MainLog Logger
|
|
JobName string
|
|
}
|
|
|
|
func (l jobLogger) Printf(format string, v ...interface{}) {
|
|
l.MainLog.Printf(fmt.Sprintf("job[%s]: %s", l.JobName, format), v...)
|
|
}
|
|
|
|
type Job interface {
|
|
JobName() string
|
|
JobDo(log Logger) (err error)
|
|
JobRepeatStrategy() RepeatStrategy
|
|
}
|
|
|
|
type JobMetadata struct {
|
|
Job Job
|
|
name string
|
|
LastStart time.Time
|
|
LastError error
|
|
DueAt time.Time
|
|
}
|
|
|
|
type JobRunResult struct {
|
|
Start time.Time
|
|
Finish time.Time
|
|
Error error
|
|
}
|
|
|
|
func (r JobRunResult) RunTime() time.Duration { return r.Finish.Sub(r.Start) }
|
|
|
|
type RepeatStrategy interface {
|
|
ShouldReschedule(lastResult JobRunResult) (nextDue time.Time, reschedule bool)
|
|
}
|
|
|
|
type JobRunner struct {
|
|
logger Logger
|
|
notificationChan chan JobMetadata
|
|
newJobChan chan Job
|
|
finishedJobChan chan JobMetadata
|
|
scheduleTimer <-chan time.Time
|
|
pending map[string]JobMetadata
|
|
running map[string]JobMetadata
|
|
wait sync.WaitGroup
|
|
}
|
|
|
|
func NewJobRunner(logger Logger) *JobRunner {
|
|
return &JobRunner{
|
|
logger: logger,
|
|
notificationChan: make(chan JobMetadata),
|
|
newJobChan: make(chan Job),
|
|
finishedJobChan: make(chan JobMetadata),
|
|
pending: make(map[string]JobMetadata),
|
|
running: make(map[string]JobMetadata),
|
|
}
|
|
}
|
|
|
|
func (r *JobRunner) AddJob(j Job) {
|
|
r.newJobChan <- j
|
|
}
|
|
|
|
func (r *JobRunner) NotificationChan() <-chan JobMetadata {
|
|
return r.notificationChan
|
|
}
|
|
|
|
func (r *JobRunner) Start() {
|
|
r.wait.Add(1)
|
|
go func() {
|
|
r.loop()
|
|
r.wait.Done()
|
|
}()
|
|
}
|
|
|
|
func (r *JobRunner) Wait() {
|
|
r.wait.Wait()
|
|
}
|
|
|
|
func (r *JobRunner) loop() {
|
|
|
|
loop:
|
|
select {
|
|
|
|
case j := <-r.newJobChan:
|
|
|
|
jn := j.JobName()
|
|
|
|
_, jobPending := r.pending[jn]
|
|
_, jobRunning := r.running[jn]
|
|
|
|
if jobPending || jobRunning {
|
|
panic("job already in runner")
|
|
}
|
|
|
|
jm := JobMetadata{name: jn, Job: j}
|
|
|
|
r.pending[jn] = jm
|
|
|
|
case finishedJob := <-r.finishedJobChan:
|
|
|
|
delete(r.running, finishedJob.name)
|
|
|
|
res := JobRunResult{
|
|
Start: finishedJob.LastStart,
|
|
Finish: time.Now(),
|
|
Error: finishedJob.LastError,
|
|
}
|
|
|
|
r.logger.Printf("[%s] finished after %s\n", finishedJob.name, res.RunTime())
|
|
|
|
dueTime, resched := finishedJob.Job.JobRepeatStrategy().ShouldReschedule(res)
|
|
if resched {
|
|
r.logger.Printf("[%s] rescheduling to %s", finishedJob.name, dueTime)
|
|
finishedJob.DueAt = dueTime
|
|
r.pending[finishedJob.name] = finishedJob
|
|
}
|
|
|
|
case <-r.scheduleTimer:
|
|
}
|
|
|
|
if len(r.pending) == 0 && len(r.running) == 0 {
|
|
return
|
|
}
|
|
|
|
// Find jobs to run
|
|
var now time.Time
|
|
var jobPending bool
|
|
|
|
now = time.Now()
|
|
jobPending = false
|
|
|
|
nextJobDue := now.Add(time.Minute) // max(pending.Interval)
|
|
|
|
for jobName, job := range r.pending {
|
|
|
|
if job.DueAt.After(now) {
|
|
if job.DueAt.Before(nextJobDue) {
|
|
nextJobDue = job.DueAt
|
|
}
|
|
jobPending = true
|
|
continue
|
|
}
|
|
// This job is due, run it
|
|
|
|
delete(r.pending, jobName)
|
|
r.running[jobName] = job
|
|
job.LastStart = now
|
|
|
|
go func(job JobMetadata) {
|
|
jobLog := jobLogger{r.logger, job.name}
|
|
if err := job.Job.JobDo(jobLog); err != nil {
|
|
job.LastError = err
|
|
r.notificationChan <- job
|
|
}
|
|
r.finishedJobChan <- job
|
|
}(job)
|
|
|
|
}
|
|
|
|
if jobPending || len(r.running) > 0 {
|
|
r.logger.Printf("waiting until %v\n", nextJobDue)
|
|
r.scheduleTimer = time.After(nextJobDue.Sub(now))
|
|
goto loop
|
|
}
|
|
|
|
}
|