Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Expand the admin status page into an operational dashboard

4506a4d3d279556435fdd414265e7499c3dada16 · Keenan Nemetz · 14d ago · parent 2e024cc9

server/web/runners.go +40−52

@@ -1,20 +1,15 @@
1 1 package web
2 2
3 // The runners page.
3 // Runner presence and queue depth for the admin status page.
4 4 //
5 5 // A runner is not an entity the forge stores: it is an SSH channel that leases jobs,
6 // authenticated by a key. So this page is built from two derived facts in the queue —
7 // the presence rows a session writes while it is connected, and the work it is
8 // holding — plus the queue depth per repository. Nothing here is authoritative; it
9 // answers "is my build waiting because nobody is listening", which is otherwise a
10 // question about a process on somebody else's machine.
11 //
12 // Visibility follows the clone rule, as every page does: a session building a private
13 // repository is not shown at all, because naming it would confirm that the repository
14 // exists, and a private repository's queue depth is not counted.
6 // authenticated by a key. So this is built from two derived facts in the queue — the
7 // presence rows a session writes while it is connected, and the work it is holding —
8 // plus the queue depth per repository. Nothing here is authoritative; it answers "is
9 // my build waiting because nobody is listening", which is otherwise a question about
10 // a process on somebody else's machine.
15 11
16 12 import (
17 "net/http"
18 13 "sort"
19 14 "strings"
20 15 "time"
@@ -50,29 +45,30 @@ type queueView struct {
50 45 Cap int
51 46 }
52 47
53 // RunnersPage serves GET /runners: who is connected, what they are building, and how
54 // much work is waiting per repository.
55 func (w *Web) RunnersPage(rw http.ResponseWriter, r *http.Request) {
48 // runnerStatus reads the queue for the status page. The status page is an
49 // instance-admin surface, so every repository is visible, including private ones.
50 // Problems are returned alongside whatever could still be read.
51 func (w *Web) runnerStatus() (sessions []runnerView, queue []queueView, problems []string) {
52 if w.Runs == nil {
53 return nil, nil, nil
54 }
56 55 now := time.Now().UTC()
57 var sessions []runnerView
58 var queue []queueView
59 56
60 if w.Runs != nil {
61 rows, err := w.Runs.Sessions(now.Add(-presenceWindow))
62 if err != nil {
63 w.Logger("runners page: sessions: %v", err)
64 w.serverError(rw, r, "cannot read the queue: "+err.Error())
65 return
66 }
57 rows, err := w.Runs.Sessions(now.Add(-presenceWindow))
58 if err != nil {
59 problems = append(problems, "sessions: "+err.Error())
60 } else {
67 61 for _, s := range rows {
68 62 v := runnerView{
69 63 Account: s.Name, Repo: s.Repo, RunID: s.RunID, Seen: s.LastSeen,
70 64 Idle: s.RunID == "", Labels: strings.Join(s.Labels, ", "),
71 65 }
72 66 if s.Repo != "" {
73 m, err := w.Repos.Open(s.Repo)
74 if err != nil || !w.canRead(r, m) {
75 continue // a private repository's build is not an anonymous reader's business
67 if w.Repos == nil {
68 continue
69 }
70 if _, err := w.Repos.Open(s.Repo); err != nil {
71 continue // a stale row for a deleted repository
76 72 }
77 73 if run, err := w.Runs.Get(s.RunID); err == nil && run != nil {
78 74 v.Job = run.Job
@@ -80,34 +76,26 @@ func (w *Web) RunnersPage(rw http.ResponseWriter, r *http.Request) {
80 76 }
81 77 sessions = append(sessions, v)
82 78 }
79 }
83 80
84 depths, err := w.Runs.QueueByRepo()
85 if err != nil {
86 w.Logger("runners page: queue depth: %v", err)
81 depths, err := w.Runs.QueueByRepo()
82 if err != nil {
83 problems = append(problems, "queue: "+err.Error())
84 return sessions, nil, problems
85 }
86 perRepoCap := 0
87 if w.Cfg != nil {
88 perRepoCap = w.Cfg.MaxJobsPerRepo
89 }
90 for name, d := range depths {
91 if w.Repos == nil {
92 continue
87 93 }
88 perRepoCap := 0
89 if w.Cfg != nil {
90 perRepoCap = w.Cfg.MaxJobsPerRepo
94 if _, err := w.Repos.Open(name); err != nil {
95 continue // a queue row whose repository is gone
91 96 }
92 for name, d := range depths {
93 m, err := w.Repos.Open(name)
94 if err != nil || !w.canRead(r, m) {
95 continue
96 }
97 queue = append(queue, queueView{Name: name, Queued: d.Queued, Running: d.Running, Cap: perRepoCap})
98 }
99 sort.Slice(queue, func(i, j int) bool { return queue[i].Name < queue[j].Name })
97 queue = append(queue, queueView{Name: name, Queued: d.Queued, Running: d.Running, Cap: perRepoCap})
100 98 }
101
102 w.render(rw, "runners", struct {
103 Page
104 Sessions []runnerView
105 Queue []queueView
106 Window string
107 }{
108 Page: w.page(r, "Runners"),
109 Sessions: sessions,
110 Queue: queue,
111 Window: presenceWindow.String(),
112 }, 0)
99 sort.Slice(queue, func(i, j int) bool { return queue[i].Name < queue[j].Name })
100 return sessions, queue, problems
113 101 }