Files
Gitea/models/actions/run_job_queue.go
T
bircniandsilverwind 6c8eaaf9e7 fix(actions): don't pick up jobs of repos with Actions disabled (#40106) (#40122)
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>
2026-10-31 15:56:26 +00:00

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