mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-05 06:45:44 +00:00
The intermittent Windows test crashes came from step errors formatting a nil `rc.Run`. `fmt` recovers the nil dereference, but on CPUs with AMX the recovered fault overflows into the heap (https://github.com/golang/go/issues/81238). The errors now name the job, and the Windows test VM hides AMX and runs on UTC, so cached boots no longer jump back 8 hours mid-test. Windows commands now start suspended and resume once they are in their job objects, so no child escapes them. That drops the process scan reading other processes' memory and the last `unsafe` code, which `depguard` now denies. Step processes outlive a crashed runner, and commands are no longer looked up in the runner's working directory, both as with GitHub's runner. Like GitHub's runner, host jobs on Linux and macOS now end the processes carrying the job's `RUNNER_TRACKING_ID`, and image pulls and Dockerfile action builds are retried. Reviewed-on: https://gitea.com/gitea/runner/pulls/1376 Reviewed-by: bircni <[email protected]> Co-authored-by: silverwind <[email protected]>
199 lines
5.1 KiB
Go
199 lines
5.1 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// Copyright 2020 The nektos/act Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package common
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"runtime/debug"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/act/common/logging"
|
|
)
|
|
|
|
// Executor define contract for the steps of a workflow
|
|
type Executor func(ctx context.Context) error
|
|
|
|
// Conditional define contract for the conditional predicate
|
|
type Conditional func(ctx context.Context) bool
|
|
|
|
// NewInfoExecutor is an executor that logs messages
|
|
func NewInfoExecutor(format string, args ...any) Executor {
|
|
return func(ctx context.Context) error {
|
|
Logger(ctx).Infof(format, args...)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NewDebugExecutor is an executor that logs messages
|
|
func NewDebugExecutor(format string, args ...any) Executor {
|
|
return func(ctx context.Context) error {
|
|
Logger(ctx).Debugf(format, args...)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NewPipelineExecutor creates a new executor from a series of other executors
|
|
func NewPipelineExecutor(executors ...Executor) Executor {
|
|
if len(executors) == 0 {
|
|
return NewErrorExecutor(nil)
|
|
}
|
|
var rtn Executor
|
|
for _, executor := range executors {
|
|
if rtn == nil {
|
|
rtn = executor
|
|
} else {
|
|
rtn = rtn.Then(executor)
|
|
}
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
// NewErrorExecutor creates a new executor that always errors out
|
|
func NewErrorExecutor(err error) Executor {
|
|
return func(ctx context.Context) error {
|
|
return err
|
|
}
|
|
}
|
|
|
|
func RecoverPanic(err *error) { // defer directly in workers to keep stacks in host logs
|
|
if recovered := recover(); recovered != nil {
|
|
logging.Errorf("panic: %v\n%s", recovered, debug.Stack())
|
|
*err = fmt.Errorf("panic: %v", recovered)
|
|
}
|
|
}
|
|
|
|
// NewParallelExecutor creates a new executor from a parallel of other executors
|
|
func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
|
if len(executors) == 0 {
|
|
return func(ctx context.Context) error {
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
return func(ctx context.Context) error {
|
|
work := make(chan Executor, len(executors))
|
|
errs := make(chan error, len(executors))
|
|
|
|
if 1 > parallel {
|
|
logging.Debugf("Parallel tasks (%d) below minimum, setting to 1", parallel)
|
|
parallel = 1
|
|
}
|
|
|
|
logging.Infof("NewParallelExecutor: Creating %d workers for %d executors", parallel, len(executors))
|
|
|
|
for workerID := range parallel {
|
|
go func() {
|
|
logging.Debugf("Worker %d started", workerID)
|
|
taskCount := 0
|
|
for executor := range work {
|
|
taskCount++
|
|
logging.Debugf("Worker %d executing task %d", workerID, taskCount)
|
|
// Recover from panics in executors to avoid crashing the worker
|
|
// goroutine which would leave the runner process hung.
|
|
// https://gitea.com/gitea/runner/issues/371
|
|
errs <- func() (err error) {
|
|
defer RecoverPanic(&err)
|
|
return executor(ctx)
|
|
}()
|
|
}
|
|
logging.Debugf("Worker %d finished (%d tasks executed)", workerID, taskCount)
|
|
}()
|
|
}
|
|
|
|
for i := range executors {
|
|
work <- executors[i]
|
|
}
|
|
close(work)
|
|
|
|
// Executor waits all executors to cleanup these resources.
|
|
var firstErr error
|
|
for range executors {
|
|
err := <-errs
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
return firstErr
|
|
}
|
|
}
|
|
|
|
// Then runs another executor if this executor succeeds
|
|
func (e Executor) Then(then Executor) Executor {
|
|
return func(ctx context.Context) error {
|
|
if err := e(ctx); err != nil {
|
|
return err
|
|
}
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return then(ctx)
|
|
}
|
|
}
|
|
|
|
// If only runs this executor if conditional is true
|
|
func (e Executor) If(conditional Conditional) Executor {
|
|
return func(ctx context.Context) error {
|
|
if conditional(ctx) {
|
|
return e(ctx)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// IfNot only runs this executor if conditional is true
|
|
func (e Executor) IfNot(conditional Conditional) Executor {
|
|
return e.If(func(ctx context.Context) bool { return !conditional(ctx) })
|
|
}
|
|
|
|
// IfBool only runs this executor if conditional is true
|
|
func (e Executor) IfBool(conditional bool) Executor {
|
|
return e.If(func(ctx context.Context) bool {
|
|
return conditional
|
|
})
|
|
}
|
|
|
|
// RetryBackoff is the wait between Retry's attempts, random between 1 and 10 seconds like GitHub's runner.
|
|
var RetryBackoff = func() time.Duration { return time.Second + rand.N(9*time.Second) }
|
|
|
|
// Retry runs the executor up to attempts times until it succeeds.
|
|
func (e Executor) Retry(attempts int) Executor {
|
|
return func(ctx context.Context) error {
|
|
for attempt := 1; ; attempt++ {
|
|
err := e(ctx)
|
|
if err == nil || attempt == attempts || ctx.Err() != nil {
|
|
return err
|
|
}
|
|
backoff := RetryBackoff()
|
|
Logger(ctx).Warnf("%v, retrying in %s", err, backoff.Round(time.Second))
|
|
select {
|
|
case <-ctx.Done():
|
|
return err
|
|
case <-time.After(backoff):
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally adds an executor to run after other executor
|
|
func (e Executor) Finally(finally Executor) Executor {
|
|
return func(ctx context.Context) error {
|
|
err := e(ctx)
|
|
err2 := finally(ctx)
|
|
if err2 != nil {
|
|
if err != nil {
|
|
return fmt.Errorf("error occurred running finally: %w (original error: %w)", err2, err)
|
|
}
|
|
return fmt.Errorf("error occurred running finally: %w (original error: %v)", err2, err)
|
|
}
|
|
return err
|
|
}
|
|
}
|