137 lines
3.1 KiB
Go
137 lines
3.1 KiB
Go
package taskscheduler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
|
)
|
|
|
|
type PollClient interface {
|
|
Declare(context.Context, string, []string) error
|
|
FetchTask(context.Context, int64) (*runnerv1.FetchTaskResponse, error)
|
|
}
|
|
|
|
type PollerConfig struct {
|
|
Version string
|
|
Labels []string
|
|
EmptyBackoff time.Duration
|
|
ErrorBackoff time.Duration
|
|
Capacity int
|
|
}
|
|
|
|
// Poller is the scheduler component. Each fetcher keeps its assigned task
|
|
// until that assignment is durably dispatched; all fetchers share one runner
|
|
// declaration and a monotonic tasks version.
|
|
type Poller struct {
|
|
Client PollClient
|
|
Scheduler *Scheduler
|
|
Config PollerConfig
|
|
OnError func(error)
|
|
}
|
|
|
|
func (p Poller) Run(ctx context.Context) error {
|
|
if p.Client == nil || p.Scheduler == nil {
|
|
return errors.New("Gitea poll client and task scheduler are required")
|
|
}
|
|
if p.Config.Version == "" || len(p.Config.Labels) == 0 {
|
|
return errors.New("runner version and labels are required")
|
|
}
|
|
if err := p.Client.Declare(ctx, p.Config.Version, p.Config.Labels); err != nil {
|
|
return fmt.Errorf("declare scheduler labels: %w", err)
|
|
}
|
|
|
|
emptyBackoff := p.Config.EmptyBackoff
|
|
if emptyBackoff <= 0 {
|
|
emptyBackoff = time.Second
|
|
}
|
|
errorBackoff := p.Config.ErrorBackoff
|
|
if errorBackoff <= 0 {
|
|
errorBackoff = 5 * time.Second
|
|
}
|
|
capacity := p.Config.Capacity
|
|
if capacity < 1 {
|
|
capacity = 1
|
|
}
|
|
var tasksVersion atomic.Int64
|
|
group, groupContext := errgroup.WithContext(ctx)
|
|
for range capacity {
|
|
group.Go(func() error { return p.runFetcher(groupContext, &tasksVersion, emptyBackoff, errorBackoff) })
|
|
}
|
|
return group.Wait()
|
|
}
|
|
|
|
func (p Poller) runFetcher(ctx context.Context, tasksVersion *atomic.Int64, emptyBackoff, errorBackoff time.Duration) error {
|
|
for {
|
|
response, err := p.Client.FetchTask(ctx, tasksVersion.Load())
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return nil
|
|
}
|
|
p.report(fmt.Errorf("fetch Gitea task: %w", err))
|
|
if !wait(ctx, errorBackoff) {
|
|
return nil
|
|
}
|
|
continue
|
|
}
|
|
if response == nil {
|
|
p.report(errors.New("fetch Gitea task returned an empty response"))
|
|
if !wait(ctx, errorBackoff) {
|
|
return nil
|
|
}
|
|
continue
|
|
}
|
|
storeMaximum(tasksVersion, response.GetTasksVersion())
|
|
task := response.GetTask()
|
|
if task == nil {
|
|
if !wait(ctx, emptyBackoff) {
|
|
return nil
|
|
}
|
|
continue
|
|
}
|
|
for {
|
|
if err := p.Scheduler.Run(ctx, task); err == nil {
|
|
break
|
|
} else {
|
|
if ctx.Err() != nil {
|
|
return nil
|
|
}
|
|
p.report(fmt.Errorf("dispatch Gitea task %d: %w", task.GetId(), err))
|
|
}
|
|
if !wait(ctx, errorBackoff) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func storeMaximum(value *atomic.Int64, candidate int64) {
|
|
for current := value.Load(); candidate > current; current = value.Load() {
|
|
if value.CompareAndSwap(current, candidate) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p Poller) report(err error) {
|
|
if p.OnError != nil {
|
|
p.OnError(err)
|
|
}
|
|
}
|
|
|
|
func wait(ctx context.Context, duration time.Duration) bool {
|
|
timer := time.NewTimer(duration)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-timer.C:
|
|
return true
|
|
}
|
|
}
|