mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-31 21:05:16 +00:00
Backport https://github.com/go-gitea/gitea/pull/40106 Repository-level and instance-level runners still picked up waiting jobs of repositories with Actions disabled. https://github.com/go-gitea/gitea/pull/30331 only covered owner-level runners, by filtering them out in SQL. Pickup now checks the Actions unit of each job it is about to claim, for every runner scope, and cancels the job instead when Actions is disabled, also honoring `DISABLED_REPO_UNITS`. Disabling Actions therefore cancels its queued jobs once a matching runner polls, instead of leaving them waiting until `ABANDONED_JOB_TIMEOUT`. Co-authored-by: silverwind <me@silverwind.io>
81 lines
3.0 KiB
Go
81 lines
3.0 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"gitea.dev/models/db"
|
|
|
|
"xorm.io/builder"
|
|
"xorm.io/xorm"
|
|
)
|
|
|
|
// JobQueueOptions scopes the job queue to a repo, an owner or, when both are zero, the instance.
|
|
type JobQueueOptions struct {
|
|
RepoID int64
|
|
OwnerID int64
|
|
Status Status
|
|
}
|
|
|
|
func (opts JobQueueOptions) session(ctx context.Context) *xorm.Session {
|
|
// a reusable-workflow caller only tracks its children, it never occupies a runner itself
|
|
sess := db.GetEngine(ctx).Table("action_run_job").Where(builder.Eq{"`action_run_job`.is_reusable_caller": false})
|
|
if opts.RepoID > 0 {
|
|
sess = sess.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID})
|
|
}
|
|
if opts.OwnerID > 0 {
|
|
sess = sess.And(builder.In("`action_run_job`.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.OwnerID})))
|
|
}
|
|
return sess.And(opts.statusCond())
|
|
}
|
|
|
|
var (
|
|
queuedJobsCond = builder.Eq{"`action_run_job`.status": StatusWaiting, "`action_run_job`.task_id": 0}
|
|
// a cancelling job still occupies its runner
|
|
runningJobsCond = builder.In("`action_run_job`.status", StatusRunning, StatusCancelling)
|
|
)
|
|
|
|
func (opts JobQueueOptions) statusCond() builder.Cond {
|
|
switch opts.Status {
|
|
case StatusRunning:
|
|
return runningJobsCond
|
|
case StatusWaiting:
|
|
return queuedJobsCond
|
|
default:
|
|
return builder.Or(runningJobsCond, queuedJobsCond)
|
|
}
|
|
}
|
|
|
|
// active jobs first by start time, then queued jobs in pickup order
|
|
var jobQueueOrderBy = fmt.Sprintf(
|
|
"CASE WHEN `action_run_job`.status IN (%d, %d) THEN 0 ELSE 1 END ASC, CASE WHEN `action_run_job`.status IN (%d, %d) THEN `action_run_job`.started ELSE `action_run_job`.updated END ASC, `action_run_job`.id ASC",
|
|
StatusRunning, StatusCancelling, StatusRunning, StatusCancelling)
|
|
|
|
// FindJobQueueJobs returns one page of the job queue and its total count.
|
|
func FindJobQueueJobs(ctx context.Context, opts JobQueueOptions, page, pageSize int) ([]*ActionRunJob, int64, error) {
|
|
total, err := opts.session(ctx).Count(new(ActionRunJob))
|
|
if err != nil || total == 0 {
|
|
return nil, total, err
|
|
}
|
|
|
|
// Auto-refresh can shrink the queue under a user still on page 2; show the last page instead of empty.
|
|
page = min(page, int((total+int64(pageSize)-1)/int64(pageSize)))
|
|
|
|
jobs := make([]*ActionRunJob, 0, pageSize)
|
|
return jobs, total, opts.session(ctx).
|
|
Cols("`action_run_job`.id", "`action_run_job`.repo_id", "`action_run_job`.name", "`action_run_job`.status", // skip the payload columns
|
|
"`action_run_job`.run_id", "`action_run_job`.runs_on", "`action_run_job`.updated", "`action_run_job`.started", "`action_run_job`.task_id").
|
|
OrderBy(jobQueueOrderBy).
|
|
Limit(pageSize, (page-1)*pageSize).
|
|
Find(&jobs)
|
|
}
|
|
|
|
// JobQueueFilterRepoIDs returns up to limit ids of the repositories with queued or running jobs.
|
|
func JobQueueFilterRepoIDs(ctx context.Context, limit int) ([]int64, error) {
|
|
var ids []int64
|
|
return ids, JobQueueOptions{}.session(ctx).Distinct("`action_run_job`.repo_id").Limit(limit).Find(&ids)
|
|
}
|