ci: a schedule tick queues nothing while the last build is pending !360
merged
merged by cmc on 2026-09-10 02:10 UTC
· krz/gitbay:sched-dedupe into main
5 files changed, +64 −7
Layout: unified · split
.gitbay/wiki/Admin.org
+7 −5
| @@ -402,11 +402,13 @@ stacks from one repository. A runner attached to one repository cannot |
| 402 | be starved. That is the rule, decided in krz/gitbay#207: a runner |
402 | be starved. That is the rule, decided in krz/gitbay#207: a runner |
| 403 | serving several repositories takes them oldest-first, and an operator |
403 | serving several repositories takes them oldest-first, and an operator |
| 404 | who wants one repository never to wait on another runs a second |
404 | who wants one repository never to wait on another runs a second |
| 405 | runner attached to it alone. Nothing caps what an account queues: builds pending at |
405 | runner attached to it alone. Nothing caps what an account queues, |
| 406 | once and schedule intervals down to a minute are unbounded, and a |
406 | and nothing needs to (krz/gitbay#206): a schedule tick queues nothing |
| 407 | build nothing claims stays pending. Since a build runs only on a |
407 | while the job's last build is pending or running, so a repository |
| 408 | runner its owner attaches, the cost is rows and the queue numbers on |
408 | with no runner holds one row per scheduled job rather than one per |
| 409 | =admin runners=, not compute; a per-account cap is krz/gitbay#206. |
409 | tick, and a build runs only on a runner its owner attaches, so a busy |
| |
410 | schedule spends the owner's compute. Pushes are bounded by what an |
| |
411 | account can push. |
| 410 | |
412 | |
| 411 | #+begin_src sh |
413 | #+begin_src sh |
| 412 | gitbay-runner -remote git@gitbay.org -repos krz/site,krz/docs \ |
414 | gitbay-runner -remote git@gitbay.org -repos krz/site,krz/docs \ |
.gitbay/wiki/CI.org
+6 −2
| @@ -28,8 +28,11 @@ nothing claims. See the Users page. Among what a runner may claim it |
| 28 | takes the oldest pending build, across every repository it serves; |
28 | takes the oldest pending build, across every repository it serves; |
| 29 | a repository that must never wait on another gets a runner of its own |
29 | a repository that must never wait on another gets a runner of its own |
| 30 | (decided in krz/gitbay#207). There is no cap on how many |
30 | (decided in krz/gitbay#207). There is no cap on how many |
| 31 | builds an account queues or how often a schedule fires; that is |
31 | builds an account queues or how often a schedule fires, and none is |
| 32 | krz/gitbay#206. |
32 | planned (krz/gitbay#206): a tick queues nothing while the job's last |
| |
33 | build is pending or running, so a repository with no runner holds one |
| |
34 | row per scheduled job, and a schedule with a runner attached spends |
| |
35 | its owner's compute, not the instance's. |
| 33 | |
36 | |
| 34 | Scheduled jobs run on their cron against the default branch, never on |
37 | Scheduled jobs run on their cron against the default branch, never on |
| 35 | push; a default-branch push registers or updates them. Tag jobs run on |
38 | push; a default-branch push registers or updates them. Tag jobs run on |
| @@ -84,6 +87,7 @@ push produced for that job; the commit's =ci/<job>= status follows: |
| 84 | | merge request head from a fork | queued, untrusted | queued, untrusted | queued, untrusted | — | — | — | |
87 | | merge request head from a fork | queued, untrusted | queued, untrusted | queued, untrusted | — | — | — | |
| 85 | | tag push | — | — | — | — | queued | — | |
88 | | tag push | — | — | — | — | queued | — | |
| 86 | | schedule tick on the default branch | — | — | — | queued | — | — | |
89 | | schedule tick on the default branch | — | — | — | queued | — | — | |
| |
90 | | schedule tick while the last scheduled build is still pending | — | — | — | — | — | — | |
| 87 | | claimed builds whose runner vanished | abandoned | abandoned | — | — | — | — | |
91 | | claimed builds whose runner vanished | abandoned | abandoned | — | — | — | — | |
| 88 | | push to the default branch with an old sha that cannot be diffed | queued | queued | queued | registered | — | — | |
92 | | push to the default branch with an old sha that cannot be diffed | queued | queued | queued | registered | — | — | |
| 89 | | push with a broken ci.yml | — | — | — | — | — | failure | |
93 | | push with a broken ci.yml | — | — | — | — | — | failure | |
internal/ci/sched.go
+10
| @@ -100,6 +100,16 @@ func (s *Scheduler) RunDue(now time.Time) { |
| 100 | s.St.RemoveSchedule(e.RepoID, e.Job) |
100 | s.St.RemoveSchedule(e.RepoID, e.Job) |
| 101 | continue |
101 | continue |
| 102 | } |
102 | } |
| |
103 | // The push path skips a job whose build for the commit is still |
| |
104 | // pending or running; so does a tick. Without this a repository |
| |
105 | // no runner serves gained one row per tick forever (#206). A |
| |
106 | // finished build does not suppress the tick: a schedule re-runs |
| |
107 | // an unchanged commit on purpose. |
| |
108 | if built, err := s.St.BuildsForCommit(repo.ID, sha); err == nil { |
| |
109 | if b, ok := built[job.Name]; ok && (b.Status == "pending" || b.Status == "running") { |
| |
110 | continue |
| |
111 | } |
| |
112 | } |
| 103 | steps, _ := json.Marshal(job.Steps) |
113 | steps, _ := json.Marshal(job.Steps) |
| 104 | n, err := s.St.CreateBuild(repo.ID, job.Name, sha, repo.DefaultBranch, string(steps), job.Image, "", true) |
114 | n, err := s.St.CreateBuild(repo.ID, job.Name, sha, repo.DefaultBranch, string(steps), job.Image, "", true) |
| 105 | if err != nil { |
115 | if err != nil { |
internal/ci/sched_test.go
+34
| @@ -101,4 +101,38 @@ func TestSchedulerRunDue(t *testing.T) { |
| 101 | if builds, _ = st.ListBuilds(repoID, 10); len(builds) != 1 { |
101 | if builds, _ = st.ListBuilds(repoID, 10); len(builds) != 1 { |
| 102 | t.Fatalf("second pass queued extra builds: %+v", builds) |
102 | t.Fatalf("second pass queued extra builds: %+v", builds) |
| 103 | } |
103 | } |
| |
104 | |
| |
105 | // Due again while the first build is still pending: nothing is |
| |
106 | // queued, so a repository no runner serves holds one row per |
| |
107 | // scheduled job, not one per tick (#206). Once that build finishes, |
| |
108 | // the next tick queues again — a schedule re-runs an unchanged |
| |
109 | // commit on purpose. |
| |
110 | if err := st.SetScheduleNext(repoID, "nightly", past); err != nil { |
| |
111 | t.Fatal(err) |
| |
112 | } |
| |
113 | s.RunDue(now) |
| |
114 | if builds, _ = st.ListBuilds(repoID, 10); len(builds) != 1 { |
| |
115 | t.Fatalf("tick with the last build pending queued another: %+v", builds) |
| |
116 | } |
| |
117 | b, ok, err := st.ClaimBuild(nil, false) |
| |
118 | if err != nil || !ok { |
| |
119 | t.Fatalf("claim: %v ok=%v", err, ok) |
| |
120 | } |
| |
121 | if err := st.SetScheduleNext(repoID, "nightly", past); err != nil { |
| |
122 | t.Fatal(err) |
| |
123 | } |
| |
124 | s.RunDue(now) |
| |
125 | if builds, _ = st.ListBuilds(repoID, 10); len(builds) != 1 { |
| |
126 | t.Fatalf("tick with the last build running queued another: %+v", builds) |
| |
127 | } |
| |
128 | if err := st.FinishBuild(b.ID, "success"); err != nil { |
| |
129 | t.Fatal(err) |
| |
130 | } |
| |
131 | if err := st.SetScheduleNext(repoID, "nightly", past); err != nil { |
| |
132 | t.Fatal(err) |
| |
133 | } |
| |
134 | s.RunDue(now) |
| |
135 | if builds, _ = st.ListBuilds(repoID, 10); len(builds) != 2 { |
| |
136 | t.Fatalf("tick after the last build finished did not queue: %+v", builds) |
| |
137 | } |
| 104 | } |
138 | } |
internal/hookd/pushshapes_test.go
+7
| @@ -363,6 +363,13 @@ var pushShapes = []pushShape{ |
| 363 | f.sched.RunDue(time.Now().AddDate(1, 0, 0)) |
363 | f.sched.RunDue(time.Now().AddDate(1, 0, 0)) |
| 364 | }, []string{"—", "—", "—", "queued", "—", "—"}}, |
364 | }, []string{"—", "—", "—", "queued", "—", "—"}}, |
| 365 | |
365 | |
| |
366 | {"schedule tick while the last scheduled build is still pending", func(f *shapeFixture) { |
| |
367 | f.push("main", zeroSHA40, f.base) |
| |
368 | f.sched.RunDue(time.Now().AddDate(1, 0, 0)) |
| |
369 | f.mark(f.base) |
| |
370 | f.sched.RunDue(time.Now().AddDate(2, 0, 0)) |
| |
371 | }, []string{"—", "—", "—", "—", "—", "—"}}, |
| |
372 | |
| 366 | {"claimed builds whose runner vanished", func(f *shapeFixture) { |
373 | {"claimed builds whose runner vanished", func(f *shapeFixture) { |
| 367 | f.git(f.src, "checkout", "-q", "-b", "feat") |
374 | f.git(f.src, "checkout", "-q", "-b", "feat") |
| 368 | c1 := f.appCommit("more") |
375 | c1 := f.appCommit("more") |