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.

Stop a running job at its runner's next heartbeat

0c14f0789af1ab0c43f99043acd7507eec4956e4 · Keenan Nemetz · 6d ago · parent 7e0b5950

Stop a running job at its runner's next heartbeat

A maintainer could cancel a queued run and nothing else: CancelRun only retired
waiting work, so a build that was clearly wrong or expensive held its runner
until its budget ran out, or until somebody killed the container by hand. The
queue cannot kill a process it does not own, but it can ask.

Canceling a running run now marks the queue row and leaves the lease alone,
because the runner still holds it and its partial attempt has to be acceptable.
The runner sees the request on the heartbeat it already sends while a step or an
image build runs, stops the container, writes one line to the log, and reports
what it did. RecordAttempt and Finish then keep the canceled status instead of
mapping the nonzero exit code to failed, so the partial log and step timings
survive and the run is not a red build.

A stop request whose runner dies first becomes canceled on the expiry sweep
rather than being leased again, and a rerun clears the request: it is a new
decision. The run page says who asked, distinguishes a run stopped while running
from one canceled in the queue, and shows the interrupted step as stopped rather
than failed.

Closes #162.

server/web/run_actions.go +21−4

@@ -1,7 +1,9 @@
1 1 package web
2 2
3 // CI actions on one run: cancel a queued job, or enqueue the same pinned job
4 // again. A running job is not interrupted; the queue does not own the process.
3 // CI actions on one run: cancel it while it waits, ask a running job to stop, or
4 // enqueue the same pinned job again. A running job is stopped by its runner at the
5 // next heartbeat; the queue cannot kill a process it does not own, but it can say
6 // that a maintainer asked.
5 7
6 8 import (
7 9 "net/http"
@@ -38,13 +40,28 @@ func (w *Web) CancelRun(rw http.ResponseWriter, r *http.Request) {
38 40 if !ok {
39 41 return
40 42 }
41 canceled, err := w.Runs.CancelQueued(run.ID, m.Name, time.Now().UTC())
43 now := time.Now().UTC()
44 var canceled bool
45 var err error
46 switch run.Status {
47 case runs.StatusQueued:
48 // Waiting work is retired outright: no runner has it, so nothing needs to be
49 // asked to stop.
50 canceled, err = w.Runs.CancelQueued(run.ID, m.Name, accountID, now)
51 case runs.StatusRunning:
52 // The runner holds the lease and learns about this on its next heartbeat. The
53 // run is not canceled yet: it becomes canceled when the attempt comes back.
54 canceled, err = w.Runs.CancelRunning(run.ID, m.Name, accountID, now)
55 default:
56 http.Error(rw, "only a queued or running run can be canceled", http.StatusBadRequest)
57 return
58 }
42 59 if err != nil {
43 60 w.serverError(rw, r, "cannot cancel the run: "+err.Error())
44 61 return
45 62 }
46 63 if !canceled {
47 http.Error(rw, "only a queued run can be canceled", http.StatusBadRequest)
64 http.Error(rw, "this run has already finished; nothing was canceled", http.StatusBadRequest)
48 65 return
49 66 }
50 67 w.audit("web.run.cancel", accountID, m.Name, run.ID)