129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
package health
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidSchedulerRunner = errors.New("invalid health scheduler runner")
|
|
ErrInvalidDueSource = errors.New("invalid health due source response")
|
|
ErrInvalidTaskSink = errors.New("invalid health task sink response")
|
|
)
|
|
|
|
// DueSource is implemented by the Controller's future Redis due-index. Both
|
|
// calls are bounded and stay outside the Gateway request path.
|
|
type DueSource interface {
|
|
InFlight(context.Context, time.Time) (int, error)
|
|
DueCandidates(context.Context, time.Time, int) ([]Candidate, error)
|
|
}
|
|
|
|
// TaskSink atomically offers an already bounded batch to the shared leased
|
|
// task store. It must leave unaccepted candidates eligible for a later tick.
|
|
type TaskSink interface {
|
|
Offer(context.Context, []PlannedTask) (int, error)
|
|
}
|
|
|
|
type SchedulerRunnerOptions struct {
|
|
PollInterval time.Duration
|
|
BatchSize int
|
|
Now func() time.Time
|
|
}
|
|
|
|
// SchedulerRunner performs one bounded tick at a time. Run has one ticker
|
|
// loop, and Tick performs no concurrent fan-out.
|
|
type SchedulerRunner struct {
|
|
planner *Planner
|
|
source DueSource
|
|
sink TaskSink
|
|
options SchedulerRunnerOptions
|
|
}
|
|
|
|
func NewSchedulerRunner(
|
|
planner *Planner,
|
|
source DueSource,
|
|
sink TaskSink,
|
|
options SchedulerRunnerOptions,
|
|
) (*SchedulerRunner, error) {
|
|
if planner == nil || nilInterface(source) || nilInterface(sink) || options.PollInterval <= 0 ||
|
|
options.BatchSize <= 0 || options.Now == nil {
|
|
return nil, ErrInvalidSchedulerRunner
|
|
}
|
|
return &SchedulerRunner{planner: planner, source: source, sink: sink, options: options}, nil
|
|
}
|
|
|
|
type TickResult struct {
|
|
Planned int
|
|
Offered int
|
|
}
|
|
|
|
func (runner *SchedulerRunner) Tick(ctx context.Context) (TickResult, error) {
|
|
if ctx == nil || runner == nil || runner.planner == nil || nilInterface(runner.source) ||
|
|
nilInterface(runner.sink) || runner.options.Now == nil {
|
|
return TickResult{}, ErrInvalidSchedulerRunner
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return TickResult{}, err
|
|
}
|
|
now := runner.options.Now()
|
|
if now.IsZero() {
|
|
return TickResult{}, ErrInvalidSchedulerRunner
|
|
}
|
|
inFlight, err := runner.source.InFlight(ctx, now.UTC())
|
|
if err != nil {
|
|
return TickResult{}, err
|
|
}
|
|
if inFlight < 0 {
|
|
return TickResult{}, ErrInvalidDueSource
|
|
}
|
|
if inFlight >= runner.planner.policy.MaxInFlight {
|
|
return TickResult{}, nil
|
|
}
|
|
candidates, err := runner.source.DueCandidates(ctx, now.UTC(), runner.options.BatchSize)
|
|
if err != nil {
|
|
return TickResult{}, err
|
|
}
|
|
if len(candidates) > runner.options.BatchSize {
|
|
return TickResult{}, ErrInvalidDueSource
|
|
}
|
|
plans, err := runner.planner.Plan(now.UTC(), inFlight, runner.options.BatchSize, candidates)
|
|
if err != nil {
|
|
return TickResult{}, err
|
|
}
|
|
result := TickResult{Planned: len(plans)}
|
|
if len(plans) == 0 {
|
|
return result, nil
|
|
}
|
|
offered, err := runner.sink.Offer(ctx, plans)
|
|
if err != nil {
|
|
return TickResult{}, err
|
|
}
|
|
if offered < 0 || offered > len(plans) {
|
|
return TickResult{}, ErrInvalidTaskSink
|
|
}
|
|
result.Offered = offered
|
|
return result, nil
|
|
}
|
|
|
|
func (runner *SchedulerRunner) Run(ctx context.Context) error {
|
|
if ctx == nil || runner == nil {
|
|
return ErrInvalidSchedulerRunner
|
|
}
|
|
if _, err := runner.Tick(ctx); err != nil {
|
|
return err
|
|
}
|
|
ticker := time.NewTicker(runner.options.PollInterval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
if _, err := runner.Tick(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|