Files

232 lines
5.5 KiB
Go

package mirror
import "context"
import "time"
import "sync"
import "codit/internal/db"
import codit_logger "codit/logger"
type Result struct {
Success bool
Step string
Total int64
Done int64
Failed int64
Deleted int64
Revision string
Error string
MinRetryDelaySec int64
}
type Runner[T any] interface {
Name() string
ResetRunning(ctx context.Context) error
Cleanup(ctx context.Context) error
ListDue(ctx context.Context, now int64, limit int) ([]T, error)
TryStart(ctx context.Context, task T, now int64) (string, bool, error)
Run(ctx context.Context, task T, runID string) Result
Finish(ctx context.Context, task T, runID string, result Result, finishedAt int64) error
TaskKey(task T) string
DescribeTask(task T) string
}
type SchedulerOptions struct {
Interval time.Duration
Limit int
MaxActive int
Logger codit_logger.Logger
LogID string
}
type Scheduler[T any] struct {
runner Runner[T]
interval time.Duration
limit int
maxActive int
logger codit_logger.Logger
logID string
stopCh chan struct{}
stopOnce sync.Once
wg sync.WaitGroup
activeMu sync.Mutex
active map[string]context.CancelFunc
}
func NewScheduler[T any](runner Runner[T], options SchedulerOptions) *Scheduler[T] {
var s *Scheduler[T]
s = &Scheduler[T]{
runner: runner,
interval: options.Interval,
limit: options.Limit,
maxActive: options.MaxActive,
logger: options.Logger,
logID: options.LogID,
stopCh: make(chan struct{}),
active: map[string]context.CancelFunc{},
}
if s.interval <= 0 { s.interval = 15 * time.Second }
if s.limit <= 0 { s.limit = 8 }
if s.maxActive <= 0 { s.maxActive = s.limit }
return s
}
func (s *Scheduler[T]) Start(ctx context.Context) error {
var err error
if s == nil { return nil }
err = s.runner.ResetRunning(ctx)
if err != nil {
if s.logger != nil { s.logger.Write(s.logID, codit_logger.LOG_ERROR, "reset running tasks failed err=%v", err) }
return err
}
err = s.runner.Cleanup(ctx)
if err != nil {
if s.logger != nil { s.logger.Write(s.logID, codit_logger.LOG_ERROR, "retention cleanup failed err=%v", err) }
}
s.wg.Add(1)
go s.loop(ctx)
return nil
}
func (s *Scheduler[T]) Stop() {
if s == nil { return }
s.stopOnce.Do(func() {
var cancel context.CancelFunc
close(s.stopCh)
s.activeMu.Lock()
for _, cancel = range s.active {
cancel()
}
s.activeMu.Unlock()
})
}
func (s *Scheduler[T]) Wait() {
if s == nil { return }
s.wg.Wait()
}
func (s *Scheduler[T]) Cancel(key string) bool {
var cancel context.CancelFunc
if s == nil { return false }
s.activeMu.Lock()
cancel = s.active[key]
s.activeMu.Unlock()
if cancel == nil { return false }
cancel()
return true
}
func (s *Scheduler[T]) loop(ctx context.Context) {
var ticker *time.Ticker
defer s.wg.Done()
ticker = time.NewTicker(s.interval)
defer ticker.Stop()
for {
s.runDue(ctx)
select {
case <-ctx.Done():
return
case <-s.stopCh:
return
case <-ticker.C:
}
}
}
func (s *Scheduler[T]) runDue(ctx context.Context) {
var tasks []T
var err error
var i int
var now int64
var remaining int
var runID string
var started bool
var writeCtx context.Context
var cancel context.CancelFunc
var key string
remaining = s.remainingCapacity()
if remaining <= 0 { return }
if remaining > s.limit { remaining = s.limit }
now = time.Now().UTC().Unix()
tasks, err = s.runner.ListDue(ctx, now, remaining)
if err != nil {
if s.logger != nil { s.logger.Write(s.logID, codit_logger.LOG_ERROR, "list due tasks failed err=%v", err) }
return
}
for i = 0; i < len(tasks); i++ {
if s.remainingCapacity() <= 0 { return }
key = s.runner.TaskKey(tasks[i])
if s.isActive(key) { continue }
writeCtx, cancel = context.WithTimeout(context.Background(), db.IndependentDBOperationTimeout)
runID, started, err = s.runner.TryStart(writeCtx, tasks[i], now)
cancel()
if err != nil {
if s.logger != nil { s.logger.Write(s.logID, codit_logger.LOG_ERROR, "try start failed task=%s err=%v", s.runner.DescribeTask(tasks[i]), err) }
continue
}
if !started { continue }
s.startTask(ctx, key, tasks[i], runID)
}
}
func (s *Scheduler[T]) startTask(parent context.Context, key string, task T, runID string) {
var ctx context.Context
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(parent)
s.activeMu.Lock()
s.active[key] = cancel
s.activeMu.Unlock()
s.wg.Add(1)
go s.runOne(ctx, key, task, runID, cancel)
}
func (s *Scheduler[T]) runOne(ctx context.Context, key string, task T, runID string, cancel context.CancelFunc) {
var result Result
var err error
var writeCtx context.Context
var writeCancel context.CancelFunc
var finishedAt int64
defer s.wg.Done()
defer func() {
cancel()
s.activeMu.Lock()
delete(s.active, key)
s.activeMu.Unlock()
}()
result = s.runner.Run(ctx, task, runID)
finishedAt = time.Now().UTC().Unix()
writeCtx, writeCancel = context.WithTimeout(context.Background(), db.IndependentDBOperationTimeout)
err = s.runner.Finish(writeCtx, task, runID, result, finishedAt)
writeCancel()
if err != nil && s.logger != nil {
s.logger.Write(s.logID, codit_logger.LOG_ERROR, "finish sync state failed task=%s run=%s err=%v", s.runner.DescribeTask(task), runID, err)
}
}
func (s *Scheduler[T]) remainingCapacity() int {
var n int
s.activeMu.Lock()
n = len(s.active)
s.activeMu.Unlock()
return s.maxActive - n
}
func (s *Scheduler[T]) isActive(key string) bool {
var ok bool
s.activeMu.Lock()
_, ok = s.active[key]
s.activeMu.Unlock()
return ok
}