The checks running are readable through reads and carry when each started (issue 1155)

This commit is contained in:
2026-10-18 20:55:18 +02:00
parent 3b49d7044f
commit 0273609090
6 changed files with 159 additions and 4 deletions
+16 -1
View File
@@ -180,6 +180,9 @@ type CheckRunning struct {
Trunk string `json:"trunk"`
Builder string `json:"builder"`
Build string `json:"build"`
// Started is when the check started, kept on every heartbeat: the entry's own time is the last heartbeat's, and a
// reader that asks how long a check has run reads this (novox/hq ADR 0433 §6).
Started time.Time `json:"started"`
}
// RunningKeyOf is the key a check is said running under: its pull request's tokens, or `group.<name>` for a group's
@@ -253,11 +256,23 @@ func keepRunning(ctx context.Context, conn *nats.Conn, r BuildRequest, builder s
fmt.Fprintf(os.Stderr, "%s runs without saying so: %v\n", r.ID, err)
return func() {}
}
return keepRunningIn(ctx, kv, key, r, builder, every, time.Now())
}
// runningStore is what keepRunning needs of the bucket: a put and a delete.
type runningStore interface {
Put(ctx context.Context, key string, value []byte) (uint64, error)
Delete(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt) error
}
// keepRunningIn is keepRunning on a bucket in hand, the check having started at started: the value is made once, so every
// heartbeat puts the same started.
func keepRunningIn(ctx context.Context, kv runningStore, key string, r BuildRequest, builder string, every time.Duration, started time.Time) (stop func()) {
trunk := ""
if r.Check != nil {
trunk = r.Check.Trunk
}
body, _ := json.Marshal(CheckRunning{Head: r.Ref, Trunk: trunk, Builder: builder, Build: r.ID})
body, _ := json.Marshal(CheckRunning{Head: r.Ref, Trunk: trunk, Builder: builder, Build: r.ID, Started: started.UTC()})
put := func() {
putting, end := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer end()
@@ -0,0 +1,61 @@
package link
import (
"context"
"encoding/json"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
)
type recordedRunning struct {
mu sync.Mutex
bodies [][]byte
}
func (r *recordedRunning) Put(_ context.Context, _ string, value []byte) (uint64, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.bodies = append(r.bodies, value)
return uint64(len(r.bodies)), nil
}
func (r *recordedRunning) Delete(context.Context, string, ...jetstream.KVDeleteOpt) error { return nil }
func (r *recordedRunning) puts() [][]byte {
r.mu.Lock()
defer r.mu.Unlock()
return append([][]byte(nil), r.bodies...)
}
// 1155/AC2: a check starting puts its key, and puts it again at each heartbeat, and every value carries the same Started,
// the time the check started.
func TestACheckRunningKeepsItsStartAcrossHeartbeats(t *testing.T) { // 1155/AC2
store := &recordedRunning{}
started := time.Date(2026, 10, 18, 19, 0, 0, 0, time.UTC)
stop := keepRunningIn(context.Background(), store, "novox.a.7", aPullCheck("b1", 7, "head-7-000000000", "trunk-1-000000", false),
"builder-1", 5*time.Millisecond, started)
deadline := time.Now().Add(5 * time.Second)
for len(store.puts()) < 3 && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond)
}
stop()
puts := store.puts()
if len(puts) < 3 {
t.Fatalf("the key was put %d times, want the first and two heartbeats", len(puts))
}
for i, body := range puts {
var got CheckRunning
if err := json.Unmarshal(body, &got); err != nil {
t.Fatal(err)
}
if !got.Started.Equal(started) {
t.Fatalf("put %d carries Started %v, want %v", i, got.Started, started)
}
if got.Builder != "builder-1" || got.Build != "b1" {
t.Fatalf("put %d lost the rest of the value: %+v", i, got)
}
}
}