fix(actions): preserve admitted jobs and runs in their concurrency group (#39461)

This commit is contained in:
Stochastic
2026-09-29 12:36:33 +08:00
committed by GitHub
parent fba8d7eace
commit 6bb74ed65d
13 changed files with 255 additions and 103 deletions
+30 -18
View File
@@ -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)
}
-11
View File
@@ -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()
+20 -7
View File
@@ -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
+8 -15
View File
@@ -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))
}