influxdb/task/backend/meta.go

255 lines
7.5 KiB
Go

package backend
import (
"errors"
"math"
"time"
"github.com/influxdata/platform"
cron "gopkg.in/robfig/cron.v2"
)
// This file contains helper methods for the StoreTaskMeta type defined in protobuf.
// FinishRun removes the run matching runID from m's CurrentlyRunning slice,
// and if that run's Now value is greater than m's LatestCompleted value,
// updates the value of LatestCompleted to the run's Now value.
//
// If runID matched a run, FinishRun returns true. Otherwise it returns false.
func (stm *StoreTaskMeta) FinishRun(runID platform.ID) bool {
for i, runner := range stm.CurrentlyRunning {
if platform.ID(runner.RunID) != runID {
continue
}
stm.CurrentlyRunning = append(stm.CurrentlyRunning[:i], stm.CurrentlyRunning[i+1:]...)
rs, re, ra := runner.RangeStart, runner.RangeEnd, runner.RequestedAt
if rs == 0 && re == 0 && ra == 0 {
// It must be a naturally scheduled run.
if runner.Now > stm.LatestCompleted {
stm.LatestCompleted = runner.Now
}
} else {
// It was a requested run. Check if we need to update a latest completed.
for _, q := range stm.ManualRuns {
if q.Start == rs && q.End == re && q.RequestedAt == ra {
// Match.
if runner.Now > q.LatestCompleted {
q.LatestCompleted = runner.Now
}
break
}
}
}
return true
}
return false
}
// CreateNextRun attempts to update stm's CurrentlyRunning slice with a new run.
// The new run's now is assigned the earliest possible time according to stm.EffectiveCron,
// that is later than any in-progress run and stm's LatestCompleted timestamp.
// If the run's now would be later than the passed-in now, CreateNextRun returns a RunNotYetDueError.
//
// makeID is a function provided by the caller to create an ID, in case we can create a run.
// Because a StoreTaskMeta doesn't know the ID of the task it belongs to, it never sets RunCreation.Created.TaskID.
func (stm *StoreTaskMeta) CreateNextRun(now int64, makeID func() (platform.ID, error)) (RunCreation, error) {
if len(stm.CurrentlyRunning) >= int(stm.MaxConcurrency) {
return RunCreation{}, errors.New("cannot create next run when max concurrency already reached")
}
// Not calling stm.DueAt here because we reuse sch.
// We can definitely optimize (minimize) cron parsing at a later point in time.
sch, err := cron.Parse(stm.EffectiveCron)
if err != nil {
return RunCreation{}, err
}
latest := stm.LatestCompleted
for _, cr := range stm.CurrentlyRunning {
if cr.Now > latest {
latest = cr.Now
}
}
nextScheduled := sch.Next(time.Unix(latest, 0))
nextScheduledUnix := nextScheduled.Unix()
if dueAt := nextScheduledUnix + int64(stm.Delay); dueAt > now {
// Can't schedule yet.
if len(stm.ManualRuns) > 0 {
return stm.createNextRunFromQueue(now, dueAt, sch, makeID)
}
return RunCreation{}, RunNotYetDueError{DueAt: dueAt}
}
id, err := makeID()
if err != nil {
return RunCreation{}, err
}
stm.CurrentlyRunning = append(stm.CurrentlyRunning, &StoreTaskMetaRun{
Now: nextScheduledUnix,
Try: 1,
RunID: uint64(id),
})
return RunCreation{
Created: QueuedRun{
RunID: id,
Now: nextScheduledUnix,
},
NextDue: sch.Next(nextScheduled).Unix() + int64(stm.Delay),
HasQueue: len(stm.ManualRuns) > 0,
}, nil
}
// createNextRunFromQueue creates the next run from a queue.
// This should only be called when the queue is not empty.
func (stm *StoreTaskMeta) createNextRunFromQueue(now, nextDue int64, sch cron.Schedule, makeID func() (platform.ID, error)) (RunCreation, error) {
if len(stm.ManualRuns) == 0 {
return RunCreation{}, errors.New("cannot create run from empty queue")
}
q := stm.ManualRuns[0]
latest := q.LatestCompleted
for _, r := range stm.CurrentlyRunning {
if r.RangeStart != q.Start || r.RangeEnd != q.End || r.RequestedAt != q.RequestedAt {
// Doesn't match our queue.
continue
}
if r.Now > latest {
latest = r.Now
}
}
runNow := sch.Next(time.Unix(latest, 0)).Unix()
// Already validated that we have room to create another run, in CreateNextRun.
id, err := makeID()
if err != nil {
return RunCreation{}, err
}
stm.CurrentlyRunning = append(stm.CurrentlyRunning, &StoreTaskMetaRun{
Now: runNow,
Try: 1,
RunID: uint64(id),
RangeStart: q.Start,
RangeEnd: q.End,
RequestedAt: q.RequestedAt,
})
if runNow >= q.End {
// Drop the queue.
stm.ManualRuns = append(stm.ManualRuns[:0], stm.ManualRuns[1:]...)
}
return RunCreation{
Created: QueuedRun{
RunID: id,
Now: runNow,
RequestedAt: q.RequestedAt,
},
NextDue: nextDue,
HasQueue: len(stm.ManualRuns) > 0,
}, nil
}
// NextDueRun returns the Unix timestamp of when the next call to CreateNextRun will be ready.
// The returned timestamp reflects the task's delay, so it does not necessarily exactly match the schedule time.
func (stm *StoreTaskMeta) NextDueRun() (int64, error) {
sch, err := cron.Parse(stm.EffectiveCron)
if err != nil {
return 0, err
}
latest := stm.LatestCompleted
currRun := make([]*StoreTaskMetaRun, len(stm.CurrentlyRunning))
copy(currRun, stm.CurrentlyRunning)
for _, cr := range currRun {
if cr.Now > latest {
latest = cr.Now
}
}
return sch.Next(time.Unix(latest, 0)).Unix() + int64(stm.Delay), nil
}
// ManuallyRunTimeRange requests a manual run covering the approximate range specified by the Unix timestamps start and end.
// More specifically, it requests runs scheduled no earlier than start, but possibly later than start,
// if start does not land on the task's schedule; and as late as, but not necessarily equal to, end.
// requestedAt is the Unix timestamp indicating when this run range was requested.
//
// If adding the range would exceed the queue size, ManuallyRunTimeRange returns ErrManualQueueFull.
func (stm *StoreTaskMeta) ManuallyRunTimeRange(start, end, requestedAt int64) error {
// Arbitrarily chosen upper limit that seems unlikely to be reached except in pathological cases.
const maxQueueSize = 32
if len(stm.ManualRuns) >= maxQueueSize {
return ErrManualQueueFull
}
lc := start - 1
if start == math.MinInt64 {
// Don't roll over in pathological case of starting at minimum int64.
lc = start
}
for _, mr := range stm.ManualRuns {
if mr.Start == start && mr.End == end {
return RetryAlreadyQueuedError{Start: start, End: end}
}
}
run := &StoreTaskMetaManualRun{
Start: start,
End: end,
LatestCompleted: lc,
RequestedAt: requestedAt,
}
stm.ManualRuns = append(stm.ManualRuns, run)
return nil
}
// Equal returns true if all of stm's fields compare equal to other.
// Note that this method operates on values, unlike the other methods which operate on pointers.
//
// Equal is probably not very useful outside of test.
func (stm StoreTaskMeta) Equal(other StoreTaskMeta) bool {
if stm.MaxConcurrency != other.MaxConcurrency ||
stm.LatestCompleted != other.LatestCompleted ||
stm.Status != other.Status ||
stm.EffectiveCron != other.EffectiveCron ||
stm.Delay != other.Delay ||
len(stm.CurrentlyRunning) != len(other.CurrentlyRunning) ||
len(stm.ManualRuns) != len(other.ManualRuns) {
return false
}
for i, o := range other.CurrentlyRunning {
s := stm.CurrentlyRunning[i]
if s.Now != o.Now ||
s.Try != o.Try ||
s.RunID != o.RunID ||
s.RangeStart != o.RangeStart ||
s.RangeEnd != o.RangeEnd ||
s.RequestedAt != o.RequestedAt {
return false
}
}
for i, o := range other.ManualRuns {
s := stm.ManualRuns[i]
if s.Start != o.Start ||
s.End != o.End ||
s.LatestCompleted != o.LatestCompleted ||
s.RequestedAt != o.RequestedAt {
return false
}
}
return true
}