2024-09-24 16:45:09 +02:00

164 lines
3.4 KiB
Go

package scheduler
import (
"context"
"cycle-scheduler/internal/job"
"math"
"sync"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
const ExponentialFactor = 1.8
// SchedulerCycle is a simple scheduler handling jobs and executes them at regular interval.
// If a task is not in desired state, the task is re-scheduled with a backoff.
type SchedulerCycle struct {
wg sync.WaitGroup
ctx context.Context
fnCancel context.CancelFunc
interval time.Duration
tasks tasks
chTasks chan *task
}
func NewSchedulerCycle(ctx context.Context, interval time.Duration, workers uint32) *SchedulerCycle {
ctxChild, fnCancel := context.WithCancel(ctx)
c := SchedulerCycle{
wg: sync.WaitGroup{},
ctx: ctxChild,
fnCancel: fnCancel,
interval: interval,
tasks: newTasks(),
chTasks: make(chan *task),
}
c.run(workers)
return &c
}
func (c *SchedulerCycle) backoff(t *task) {
backoff := c.interval + time.Duration(math.Pow(ExponentialFactor, float64(t.attempts.Load())))
t.timer.set(
time.AfterFunc(backoff, func() {
select {
case c.chTasks <- t:
default:
log.Error().Str("task id", t.GetID().String()).Msg("unable to execute task to the worker, delayed it")
c.backoff(t)
}
}),
)
}
// exec runs the task now or if all the workers are in use, delayed it.
func (c *SchedulerCycle) exec(t *task) {
select {
case c.chTasks <- t:
default:
log.Error().Str("task id", t.GetID().String()).Msg("unable to execute the task to a worker now, delayed it")
c.backoff(t)
}
}
func (c *SchedulerCycle) getTask(id uuid.UUID) *task {
return c.tasks.get(id)
}
// run launches a number of worker to execute tasks.
// If a task returns `ErrJobNotCompletedYet`, it re-schedules with a backoff.
func (c *SchedulerCycle) run(n uint32) {
for i := 0; i < int(n); i++ {
c.wg.Add(1)
go func() {
defer c.wg.Done()
for {
select {
case t := <-c.chTasks:
c.execute(t, c.backoff)
case <-c.ctx.Done():
log.Error().Msg("context done, worker is stopping...")
return
}
}
}()
}
}
func (c *SchedulerCycle) execute(t *task, fnFallBack func(*task)) {
t.run(c.ctx)
if t.GetState() == job.Pending {
fnFallBack(t)
}
}
func (c *SchedulerCycle) Stop() {
c.fnCancel()
}
func (c *SchedulerCycle) Done() <-chan struct{} {
done := make(chan struct{})
go func() {
<-c.ctx.Done()
c.wg.Wait()
done <- struct{}{}
}()
return done
}
func (c *SchedulerCycle) Len() int {
return c.tasks.len()
}
// TasksDone checks whether all the tasks has been completed.
func (c *SchedulerCycle) TasksDone() bool {
return c.tasks.completed()
}
func (c *SchedulerCycle) GetTasksDetails() []TaskDetails {
return c.tasks.getAllDetails()
}
// GetTaskDetails returns the task details by id.
func (c *SchedulerCycle) GetTaskDetails(id uuid.UUID) TaskDetails {
return c.tasks.getDetails(id)
}
// Delay builds a task and add it to the scheduler engine.
func (c *SchedulerCycle) Delay(fnJob job.FnJob) uuid.UUID {
select {
case <-c.Done():
log.Error().Msg("context done unable to add new job")
default:
}
t := newTask(fnJob)
c.tasks.add(t)
c.exec(t)
log.Info().Str("task", t.GetID().String()).Msg("task added successfully")
return t.GetID()
}
// Abort aborts the task given by its id if it exists.
func (c *SchedulerCycle) Abort(id uuid.UUID) bool {
if t := c.getTask(id); t != nil {
t.abort()
log.Info().Str("task id", t.GetID().String()).Msg("abort task done")
return true
}
return false
}