mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-01 20:59:45 +09:00
fix(actions): preserve admitted jobs and runs in their concurrency group (#39461)
This commit is contained in:
+30
-18
@@ -366,25 +366,42 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
||||
|
||||
type ActionRunIndex db.ResourceIndex
|
||||
|
||||
// GetConcurrentRunAttemptsAndJobs returns run attempts and jobs in the same concurrency group by statuses.
|
||||
func GetConcurrentRunAttemptsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRunAttempt, []*ActionRunJob, error) {
|
||||
attempts, err := FindConcurrentRunAttempts(ctx, repoID, concurrencyGroup, status)
|
||||
if err != nil {
|
||||
// expandedCallerCond matches a reusable caller that passed its gate, its status aggregated from its children can still be blocked
|
||||
var expandedCallerCond = builder.Eq{"is_reusable_caller": true, "is_expanded": true}
|
||||
|
||||
// GetConcurrencyHolders returns the run attempts and jobs that passed the gate of the concurrency group and are not done yet.
|
||||
func GetConcurrencyHolders(ctx context.Context, repoID int64, concurrencyGroup string) ([]*ActionRunAttempt, []*ActionRunJob, error) {
|
||||
holding := builder.In("status", StatusWaiting, StatusRunning, StatusCancelling)
|
||||
return findConcurrencyGroupEntries(ctx, repoID, concurrencyGroup, holding, holding.Or(expandedCallerCond.And(builder.Eq{"status": StatusBlocked})))
|
||||
}
|
||||
|
||||
// GetConcurrencyWaiters returns the run attempts and jobs blocked at the gate of the concurrency group.
|
||||
func GetConcurrencyWaiters(ctx context.Context, repoID int64, concurrencyGroup string) ([]*ActionRunAttempt, []*ActionRunJob, error) {
|
||||
blocked := builder.Eq{"status": StatusBlocked}
|
||||
return findConcurrencyGroupEntries(ctx, repoID, concurrencyGroup, blocked, blocked.And(builder.Not{expandedCallerCond}))
|
||||
}
|
||||
|
||||
func findConcurrencyGroupEntries(ctx context.Context, repoID int64, concurrencyGroup string, attemptCond, jobCond builder.Cond) ([]*ActionRunAttempt, []*ActionRunJob, error) {
|
||||
groupCond := builder.Eq{"repo_id": repoID, "concurrency_group": concurrencyGroup}
|
||||
attempts := make([]*ActionRunAttempt, 0)
|
||||
if err := db.GetEngine(ctx).Where(groupCond.And(attemptCond)).Find(&attempts); err != nil {
|
||||
return nil, nil, fmt.Errorf("find run attempts: %w", err)
|
||||
}
|
||||
|
||||
jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{
|
||||
RepoID: repoID,
|
||||
ConcurrencyGroup: concurrencyGroup,
|
||||
Statuses: status,
|
||||
})
|
||||
if err != nil {
|
||||
jobs := make([]*ActionRunJob, 0)
|
||||
if err := db.GetEngine(ctx).Where(groupCond.And(jobCond)).Find(&jobs); err != nil {
|
||||
return nil, nil, fmt.Errorf("find jobs: %w", err)
|
||||
}
|
||||
|
||||
return attempts, jobs, nil
|
||||
}
|
||||
|
||||
func getConcurrencyEntriesToReplace(ctx context.Context, repoID int64, concurrencyGroup string, cancelInProgress bool) ([]*ActionRunAttempt, []*ActionRunJob, error) {
|
||||
if !cancelInProgress {
|
||||
return GetConcurrencyWaiters(ctx, repoID, concurrencyGroup)
|
||||
}
|
||||
unfinished := builder.In("status", StatusBlocked, StatusWaiting, StatusRunning, StatusCancelling)
|
||||
return findConcurrencyGroupEntries(ctx, repoID, concurrencyGroup, unfinished, unfinished)
|
||||
}
|
||||
|
||||
func CancelPreviousJobsByRunConcurrency(ctx context.Context, attempt *ActionRunAttempt) ([]*ActionRunJob, error) {
|
||||
if attempt.ConcurrencyGroup == "" {
|
||||
return nil, nil
|
||||
@@ -392,12 +409,7 @@ func CancelPreviousJobsByRunConcurrency(ctx context.Context, attempt *ActionRunA
|
||||
|
||||
var jobsToCancel []*ActionRunJob
|
||||
|
||||
statusFindOption := []Status{StatusWaiting, StatusBlocked}
|
||||
if attempt.ConcurrencyCancel {
|
||||
statusFindOption = append(statusFindOption, StatusRunning)
|
||||
statusFindOption = append(statusFindOption, StatusCancelling)
|
||||
}
|
||||
attempts, jobs, err := GetConcurrentRunAttemptsAndJobs(ctx, attempt.RepoID, attempt.ConcurrencyGroup, statusFindOption)
|
||||
attempts, jobs, err := getConcurrencyEntriesToReplace(ctx, attempt.RepoID, attempt.ConcurrencyGroup, attempt.ConcurrencyCancel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
||||
}
|
||||
|
||||
@@ -147,17 +147,6 @@ func findPassThroughAttemptIDs(ctx context.Context, attemptIDs []int64) ([]int64
|
||||
Find(&passThroughAttemptIDs)
|
||||
}
|
||||
|
||||
// FindConcurrentRunAttempts returns attempts in the given concurrency group and status set.
|
||||
// Results are unordered; callers must not depend on any particular row order.
|
||||
func FindConcurrentRunAttempts(ctx context.Context, repoID int64, concurrencyGroup string, statuses []Status) ([]*ActionRunAttempt, error) {
|
||||
attempts := make([]*ActionRunAttempt, 0)
|
||||
sess := db.GetEngine(ctx).Where("repo_id=? AND concurrency_group=?", repoID, concurrencyGroup)
|
||||
if len(statuses) > 0 {
|
||||
sess = sess.In("status", statuses)
|
||||
}
|
||||
return attempts, sess.Find(&attempts)
|
||||
}
|
||||
|
||||
func UpdateRunAttempt(ctx context.Context, attempt *ActionRunAttempt, cols ...string) error {
|
||||
if slices.Contains(cols, "status") && attempt.Started.IsZero() && attempt.Status.IsRunning() {
|
||||
attempt.Started = timeutil.TimeStampNow()
|
||||
|
||||
@@ -724,6 +724,20 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
||||
return cancelledJobs, nil
|
||||
}
|
||||
|
||||
// GetAncestorCallerIDs returns the IDs of the reusable workflow callers the job is nested in.
|
||||
func GetAncestorCallerIDs(ctx context.Context, job *ActionRunJob) (container.Set[int64], error) {
|
||||
ids := make(container.Set[int64])
|
||||
for parentID := job.ParentJobID; parentID != 0; {
|
||||
parent, err := GetRunJobByRunAndID(ctx, job.RunID, parentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load caller %d: %w", parentID, err)
|
||||
}
|
||||
ids.Add(parent.ID)
|
||||
parentID = parent.ParentJobID
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob) (jobsToCancel []*ActionRunJob, _ error) {
|
||||
if job.RawConcurrency == "" {
|
||||
return nil, nil
|
||||
@@ -735,16 +749,15 @@ func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
statusFindOption := []Status{StatusWaiting, StatusBlocked}
|
||||
if job.ConcurrencyCancel {
|
||||
statusFindOption = append(statusFindOption, StatusRunning)
|
||||
statusFindOption = append(statusFindOption, StatusCancelling)
|
||||
}
|
||||
attempts, jobs, err := GetConcurrentRunAttemptsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, statusFindOption)
|
||||
attempts, jobs, err := getConcurrencyEntriesToReplace(ctx, job.RepoID, job.ConcurrencyGroup, job.ConcurrencyCancel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
||||
}
|
||||
jobs = slices.DeleteFunc(jobs, func(j *ActionRunJob) bool { return j.ID == job.ID })
|
||||
callerIDs, err := GetAncestorCallerIDs(ctx, job)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobs = slices.DeleteFunc(jobs, func(j *ActionRunJob) bool { return j.ID == job.ID || callerIDs.Contains(j.ID) })
|
||||
jobsToCancel = append(jobsToCancel, jobs...)
|
||||
|
||||
// cancel runs in the same concurrency group
|
||||
|
||||
@@ -91,15 +91,14 @@ func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) err
|
||||
|
||||
type FindRunJobOptions struct {
|
||||
db.ListOptions
|
||||
RunID int64
|
||||
RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy jobs have run_attempt_id=0)
|
||||
RepoID int64
|
||||
OwnerID int64
|
||||
CommitSHA string
|
||||
Statuses []Status
|
||||
UpdatedBefore timeutil.TimeStamp
|
||||
ConcurrencyGroup string
|
||||
OrderBy db.SearchOrderBy
|
||||
RunID int64
|
||||
RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy jobs have run_attempt_id=0)
|
||||
RepoID int64
|
||||
OwnerID int64
|
||||
CommitSHA string
|
||||
Statuses []Status
|
||||
UpdatedBefore timeutil.TimeStamp
|
||||
OrderBy db.SearchOrderBy
|
||||
// AccessibleRepoIDsSubQuery, when non-nil, restricts results to the repo IDs selected by the
|
||||
// subquery (the caller's accessible repos). A nil value means no restriction. Using a subquery
|
||||
// instead of a materialized ID slice avoids exceeding DB parameter limits for large owners.
|
||||
@@ -131,12 +130,6 @@ func (opts FindRunJobOptions) ToConds() builder.Cond {
|
||||
if opts.UpdatedBefore > 0 {
|
||||
cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore})
|
||||
}
|
||||
if opts.ConcurrencyGroup != "" {
|
||||
if opts.RepoID == 0 {
|
||||
panic("Invalid FindRunJobOptions: repo_id is required")
|
||||
}
|
||||
cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup})
|
||||
}
|
||||
if opts.AccessibleRepoIDsSubQuery != nil {
|
||||
cond = cond.And(builder.In("`action_run_job`.repo_id", opts.AccessibleRepoIDsSubQuery))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user