Files
gitea-dynamic-runner/internal/taskscheduler/poller.go
T
panxiao81 fadc93a0bf
test / python (pull_request) Successful in 13s
test / shell (pull_request) Successful in 20s
test / go (pull_request) Successful in 5m54s
feat: 为执行后端增加独立容量池
2026-09-21 07:21:38 +00:00

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
}
}