feat(actions): add waiting jobs view filtered by runner label
Adds a new page to view all waiting/blocked jobs for a specific runner label. This helps administrators identify which jobs are queued for particular runner labels and diagnose runner capacity issues.
This commit is contained in:
@@ -315,3 +315,24 @@ func GetQueueDepthByLabels(ctx context.Context) ([]QueueDepthByLabel, error) {
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetWaitingJobsByLabel returns all waiting/blocked jobs that have the specified label in their RunsOn
|
||||
func GetWaitingJobsByLabel(ctx context.Context, label string) ([]*ActionRunJob, error) {
|
||||
var jobs []*ActionRunJob
|
||||
err := db.GetEngine(ctx).Where("status IN (?, ?)", StatusWaiting, StatusBlocked).Find(&jobs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter by label
|
||||
var filtered []*ActionRunJob
|
||||
for _, job := range jobs {
|
||||
for _, l := range job.RunsOn {
|
||||
if l == label {
|
||||
filtered = append(filtered, job)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
@@ -4219,6 +4219,9 @@
|
||||
"actions.runners.current_task": "Current Task",
|
||||
"actions.runners.cleanup_requested": "Cleanup request sent to runner",
|
||||
"actions.runners.cleanup_request_failed": "Failed to send cleanup request",
|
||||
"actions.runners.waiting_jobs": "Waiting Jobs",
|
||||
"actions.runners.back_to_runners": "Back to Runners",
|
||||
"actions.runners.no_waiting_jobs": "No jobs waiting for this label",
|
||||
"admin.ai_learning": "AI Learning",
|
||||
"admin.ai_learning.edit": "Edit Pattern",
|
||||
"admin.ai_learning.total_patterns": "Total Patterns",
|
||||
|
||||
@@ -29,14 +29,15 @@ import (
|
||||
|
||||
const (
|
||||
// TODO: Separate secrets from runners when layout is ready
|
||||
tplRepoRunners templates.TplName = "repo/settings/actions"
|
||||
tplOrgRunners templates.TplName = "org/settings/actions"
|
||||
tplAdminRunners templates.TplName = "admin/actions"
|
||||
tplUserRunners templates.TplName = "user/settings/actions"
|
||||
tplRepoRunnerEdit templates.TplName = "repo/settings/runner_edit"
|
||||
tplOrgRunnerEdit templates.TplName = "org/settings/runners_edit"
|
||||
tplAdminRunnerEdit templates.TplName = "admin/runners/edit"
|
||||
tplUserRunnerEdit templates.TplName = "user/settings/runner_edit"
|
||||
tplRepoRunners templates.TplName = "repo/settings/actions"
|
||||
tplOrgRunners templates.TplName = "org/settings/actions"
|
||||
tplAdminRunners templates.TplName = "admin/actions"
|
||||
tplUserRunners templates.TplName = "user/settings/actions"
|
||||
tplRepoRunnerEdit templates.TplName = "repo/settings/runner_edit"
|
||||
tplOrgRunnerEdit templates.TplName = "org/settings/runners_edit"
|
||||
tplAdminRunnerEdit templates.TplName = "admin/runners/edit"
|
||||
tplUserRunnerEdit templates.TplName = "user/settings/runner_edit"
|
||||
tplAdminWaitingJobs templates.TplName = "admin/runners/waiting_jobs"
|
||||
)
|
||||
|
||||
type runnersCtx struct {
|
||||
@@ -809,3 +810,33 @@ func QueueDepthJSON(ctx *context.Context) {
|
||||
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// WaitingJobs shows waiting jobs filtered by label
|
||||
func WaitingJobs(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("actions.runners.waiting_jobs")
|
||||
ctx.Data["PageIsAdminRunners"] = true
|
||||
|
||||
label := ctx.FormString("label")
|
||||
ctx.Data["Label"] = label
|
||||
|
||||
jobs, err := actions_model.GetWaitingJobsByLabel(ctx, label)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWaitingJobsByLabel", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Load repo and run info for each job
|
||||
for _, job := range jobs {
|
||||
if err := job.LoadRepo(ctx); err != nil {
|
||||
log.Error("LoadRepo for job %d: %v", job.ID, err)
|
||||
}
|
||||
if err := job.LoadRun(ctx); err != nil {
|
||||
log.Error("LoadRun for job %d: %v", job.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Jobs"] = jobs
|
||||
ctx.Data["Link"] = setting.AppSubURL + "/-/admin/actions/runners"
|
||||
|
||||
ctx.HTML(http.StatusOK, tplAdminWaitingJobs)
|
||||
}
|
||||
|
||||
@@ -517,6 +517,7 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/{runnerid}/use-suggested-labels", shared_actions.RunnerUseSuggestedLabels)
|
||||
m.Get("/status", shared_actions.RunnersStatusJSON)
|
||||
m.Get("/queue-depth", shared_actions.QueueDepthJSON)
|
||||
m.Get("/queue", shared_actions.WaitingJobs)
|
||||
m.Get("/{runnerid}/status", shared_actions.RunnerStatusJSON)
|
||||
m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken)
|
||||
})
|
||||
|
||||
72
templates/admin/runners/waiting_jobs.tmpl
Normal file
72
templates/admin/runners/waiting_jobs.tmpl
Normal file
@@ -0,0 +1,72 @@
|
||||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
|
||||
<div class="admin-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "actions.runners.waiting_jobs"}}
|
||||
{{if .Label}}
|
||||
<span class="ui blue label">{{.Label}}</span>
|
||||
{{end}}
|
||||
<a href="{{.Link}}" class="ui right">
|
||||
<button class="ui tiny button">{{svg "octicon-arrow-left" 14}} {{ctx.Locale.Tr "actions.runners.back_to_runners"}}</button>
|
||||
</a>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .Jobs}}
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.run"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.repository"}}</th>
|
||||
<th>{{ctx.Locale.Tr "admin.packages.name"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.labels"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.status"}}</th>
|
||||
<th>{{ctx.Locale.Tr "created"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Jobs}}
|
||||
<tr>
|
||||
<td>
|
||||
{{if and .Run .Repo}}
|
||||
<a href="{{.Repo.Link}}/actions/runs/{{.Run.Index}}/jobs/{{.ID}}" target="_blank">
|
||||
{{.Run.Index}}#{{.ID}}
|
||||
</a>
|
||||
{{else}}
|
||||
{{.ID}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .Repo}}
|
||||
<a href="{{.Repo.Link}}" target="_blank">{{.Repo.FullName}}</a>
|
||||
{{else}}
|
||||
-
|
||||
{{end}}
|
||||
</td>
|
||||
<td>{{.Name}}</td>
|
||||
<td>
|
||||
{{range .RunsOn}}
|
||||
<span class="ui small label">{{.}}</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<span class="ui label {{if eq .Status.String "waiting"}}yellow{{else if eq .Status.String "blocked"}}orange{{else}}grey{{end}}">
|
||||
{{.Status.LocaleString ctx.Locale}}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{DateUtils.TimeSince .Created}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="ui placeholder segment">
|
||||
<div class="ui icon header">
|
||||
{{svg "octicon-check-circle" 48}}
|
||||
<div class="content">
|
||||
{{ctx.Locale.Tr "actions.runners.no_waiting_jobs"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
@@ -134,8 +134,9 @@
|
||||
for (const item of data) {
|
||||
const labelClass = item.stuck_jobs > 0 ? 'red' : (item.job_count > 5 ? 'yellow' : 'blue');
|
||||
let waitInfo = item.oldest_wait ? ' (oldest: ' + item.oldest_wait + ')' : '';
|
||||
html += '<span class="ui ' + labelClass + ' label" title="' + item.job_count + ' jobs waiting' + waitInfo + '">' +
|
||||
item.label + ': ' + item.job_count + (item.oldest_wait ? ' (' + item.oldest_wait + ')' : '') + '</span>';
|
||||
const queueUrl = '{{$.Link}}/queue?label=' + encodeURIComponent(item.label);
|
||||
html += '<a href="' + queueUrl + '" class="ui ' + labelClass + ' label" title="' + item.job_count + ' jobs waiting' + waitInfo + ' - Click to view">' +
|
||||
item.label + ': ' + item.job_count + (item.oldest_wait ? ' (' + item.oldest_wait + ')' : '') + '</a>';
|
||||
|
||||
if (item.stuck_jobs > 0) {
|
||||
hasStuck = true;
|
||||
|
||||
Reference in New Issue
Block a user