issue 974: the builders say the checks they run in a bucket that ages, and a builder drops a waiting request for a head and trunk commit another is running
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -168,3 +169,122 @@ const (
|
||||
checkTakeWait = 30 * time.Millisecond
|
||||
takeIdleWait = time.Second
|
||||
)
|
||||
|
||||
// CheckRunning is what a builder says of the check it is running, in the bucket of the checks running (novox/hq ADR 0406
|
||||
// §3): the head and trunk commit it checks, the builder and its build. The key is the pull request's (RunningKeyOf).
|
||||
type CheckRunning struct {
|
||||
Head string `json:"head"`
|
||||
Trunk string `json:"trunk"`
|
||||
Builder string `json:"builder"`
|
||||
Build string `json:"build"`
|
||||
}
|
||||
|
||||
// RunningKeyOf is the key a check is said running under: its pull request's tokens, or `group.<name>` for a group's
|
||||
// composed check; none for a try, which no other request replaces.
|
||||
func RunningKeyOf(r BuildRequest) (string, bool) {
|
||||
c := r.Check
|
||||
switch {
|
||||
case c == nil, c.Try:
|
||||
return "", false
|
||||
case c.Group != "":
|
||||
return "group." + tokenOf(c.Group), true
|
||||
case c.Number <= 0:
|
||||
return "", false
|
||||
}
|
||||
return PullKeyOf(c.Owner, c.Repo, c.Number), true
|
||||
}
|
||||
|
||||
// runningBucket opens the bucket of the checks running.
|
||||
func runningBucket(ctx context.Context, conn *nats.Conn) (jetstream.KeyValue, error) {
|
||||
api, err := jetstream.New(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kv, err := api.KeyValue(ctx, broker.CheckRunningBucket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the checks running: %w", err)
|
||||
}
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// ReadRunning reads the key of the check running for a request's pull request: false when there is none.
|
||||
func ReadRunning(ctx context.Context, conn *nats.Conn, r BuildRequest) (CheckRunning, bool, error) {
|
||||
key, ok := RunningKeyOf(r)
|
||||
if !ok {
|
||||
return CheckRunning{}, false, nil
|
||||
}
|
||||
kv, err := runningBucket(ctx, conn)
|
||||
if err != nil {
|
||||
return CheckRunning{}, false, err
|
||||
}
|
||||
e, err := kv.Get(ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return CheckRunning{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return CheckRunning{}, false, err
|
||||
}
|
||||
var running CheckRunning
|
||||
if err := json.Unmarshal(e.Value(), &running); err != nil {
|
||||
return CheckRunning{}, false, nil // a key of a builder from before this means no match
|
||||
}
|
||||
return running, true, nil
|
||||
}
|
||||
|
||||
// Same says a running check is for this request's head and trunk commit: both said, and the same. A key that names neither
|
||||
// (an older builder's) is no match.
|
||||
func (c CheckRunning) Same(r BuildRequest) bool {
|
||||
return r.Check != nil && c.Head != "" && c.Trunk != "" && r.Check.Trunk != "" && sameCommit(c.Head, r.Ref) &&
|
||||
sameCommit(c.Trunk, r.Check.Trunk)
|
||||
}
|
||||
|
||||
// keepRunning says a check is running: its key put now, put again at each heartbeat, and deleted by the stop it returns,
|
||||
// whatever the outcome. Said to nobody that cannot be told: a bucket that is not there is no reason not to run the check.
|
||||
func keepRunning(ctx context.Context, conn *nats.Conn, r BuildRequest, builder string, every time.Duration) (stop func()) {
|
||||
key, ok := RunningKeyOf(r)
|
||||
if !ok {
|
||||
return func() {}
|
||||
}
|
||||
kv, err := runningBucket(ctx, conn)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s runs without saying so: %v\n", r.ID, err)
|
||||
return func() {}
|
||||
}
|
||||
trunk := ""
|
||||
if r.Check != nil {
|
||||
trunk = r.Check.Trunk
|
||||
}
|
||||
body, _ := json.Marshal(CheckRunning{Head: r.Ref, Trunk: trunk, Builder: builder, Build: r.ID})
|
||||
put := func() {
|
||||
putting, end := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
||||
defer end()
|
||||
if _, err := kv.Put(putting, key, body); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s could not say it is running: %v\n", r.ID, err)
|
||||
}
|
||||
}
|
||||
put()
|
||||
done := make(chan struct{})
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
defer close(finished)
|
||||
tick := time.NewTicker(every)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-tick.C:
|
||||
put()
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() {
|
||||
close(done)
|
||||
<-finished
|
||||
deleting, end := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
||||
defer end()
|
||||
if err := kv.Delete(deleting, key); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s could not take its running key away; it ages out: %v\n", r.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user