The waiter moved to mesh-sdk buildrun (fix/734-lost-ask, d96b9fd6), where every asker of the build seats shares it. The builder hears DEAD_LETTERS for the go, node and image seats and asks a given-up part of a build again at once, with the seat's words as why, instead of waiting for the check bound. The checker keeps what it does with a lost ask (a try ends as error, a check is asked again) and its verdict words.
171 lines
6.0 KiB
Go
171 lines
6.0 KiB
Go
package check
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery/buildrun"
|
|
)
|
|
|
|
// losingAsks is the build seat over a bus that gives up the first `lose` asks put (DEAD_LETTERS keeps them, as on
|
|
// 2026-10-26 during a bus reload: try-1793017340003993788) and answers the rest; with never set it neither answers nor
|
|
// gives up (an ask no node takes).
|
|
type losingAsks struct {
|
|
*buildrun.FakeAsks
|
|
answer func(delivery.StepAsk) delivery.Step
|
|
dead *buildrun.DeadLetters
|
|
never bool
|
|
|
|
mu sync.Mutex
|
|
lose int
|
|
}
|
|
|
|
func (l *losingAsks) PublishAsk(ctx context.Context, subject, id string, data []byte) error {
|
|
if err := l.FakeAsks.PublishAsk(ctx, subject, id, data); err != nil {
|
|
return err
|
|
}
|
|
l.mu.Lock()
|
|
lost := l.lose > 0
|
|
if lost {
|
|
l.lose--
|
|
}
|
|
l.mu.Unlock()
|
|
switch {
|
|
case l.never:
|
|
case lost:
|
|
h := map[string][]string{buildrun.DeadHeaderMsgID: {id}, buildrun.DeadHeaderDeliveries: {"5"}, buildrun.DeadHeaderStream: {"SEAT_NODE_BUILD_GO"},
|
|
buildrun.DeadHeaderGaveUp: {"2026-10-26T12:26:40Z"}}
|
|
g, ok := buildrun.GivenUpOf(h)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
go l.dead.Heard(g)
|
|
default:
|
|
a, err := delivery.DecodeStepAsk(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l.Write(l.answer(a))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func losing(b *bench, lose int) *losingAsks {
|
|
l := &losingAsks{FakeAsks: &buildrun.FakeAsks{}, answer: b.answer, dead: &buildrun.DeadLetters{}, lose: lose}
|
|
b.ex.Asks = buildrun.Asker{Publisher: l, Waiter: buildrun.LostAskWaiter{Inner: l.FakeAsks, Dead: l.dead}, Bound: 5 * time.Second}
|
|
return l
|
|
}
|
|
|
|
// endable is the tries state as the end tool sees it.
|
|
type endable struct{ kv *memKV }
|
|
|
|
func (e endable) Entry(key string) (json.RawMessage, uint64, error) {
|
|
v, rev, _, err := e.kv.Get(context.Background(), key)
|
|
return v, rev, err
|
|
}
|
|
func (e endable) Update(key string, value []byte, revision uint64) error {
|
|
_, err := e.kv.Update(context.Background(), key, value, revision)
|
|
return err
|
|
}
|
|
|
|
// Issue 734: a try whose ask the build seat gave up on ends as error saying so, and the next ask of its head is a new try;
|
|
// one no node takes ends at the step bound; one ended by hand frees its place and stops being waited for.
|
|
func TestAnAskGivenUpOrNeverTakenEndsItsTryAsErrorAndItsHeadIsAskedAnew(t *testing.T) {
|
|
ctx := context.Background()
|
|
for _, row := range []struct {
|
|
name string
|
|
lose int
|
|
never bool
|
|
want string
|
|
}{
|
|
{"given up by the build seat", 1, false, buildrun.LostWords + " after 5 deliveries at 2026-10-26T12:26:40Z"},
|
|
{"taken by no build node within the bound", 0, true, buildrun.NotTakenWords + " within 200ms"},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
kv := newMemKV()
|
|
at := time.Date(2026, 10, 26, 12, 22, 20, 0, time.UTC)
|
|
id, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1}, at)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := newBench(t)
|
|
l := losing(b, row.lose)
|
|
l.never = row.never
|
|
tr := &Tryer{Tries: kv, Judge: b.ex, NotTaken: 200 * time.Millisecond}
|
|
raw, _, _, _ := kv.Get(ctx, id)
|
|
tr.Put(ctx, id, raw)
|
|
tr.Wait()
|
|
got := readTry(t, kv, id)
|
|
if got.State != TryError || len(got.Failed) != 1 || !strings.Contains(got.Failed[0], row.want) || !strings.Contains(got.Said, row.want) {
|
|
t.Fatalf("try = %+v, want error saying %q", got, row.want)
|
|
}
|
|
again, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1}, at.Add(time.Minute))
|
|
if err != nil || again == id {
|
|
t.Errorf("the head asked again answered %q, %v, want a new try", again, err)
|
|
}
|
|
})
|
|
}
|
|
t.Run("ended by hand", func(t *testing.T) {
|
|
kv := newMemKV()
|
|
id, _ := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1}, time.Now())
|
|
b := newBench(t)
|
|
losing(b, 0).never = true
|
|
tr := &Tryer{Tries: kv, Judge: b.ex} // no bound: only the hand ends it
|
|
raw, _, _, _ := kv.Get(ctx, id)
|
|
tr.Put(ctx, id, raw)
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for readTry(t, kv, id).State != TryRunning && time.Now().Before(deadline) {
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
if _, err := EndTry(endable{kv}, id, "", time.Now()); err == nil {
|
|
t.Error("an end without a why was taken")
|
|
}
|
|
ended, err := EndTry(endable{kv}, id, "its ask was lost in the bus reload", time.Now())
|
|
if err != nil || ended.State != TryError || !strings.HasPrefix(ended.Said, "ended by hand ("+TryEndTool+"): its ask was lost") {
|
|
t.Fatalf("ended = %+v, %v", ended, err)
|
|
}
|
|
raw, _, _, _ = kv.Get(ctx, id)
|
|
tr.Put(ctx, id, raw) // the watch of the tries hands the end over
|
|
tr.Wait() // returns: the try's judging stopped
|
|
if got := readTry(t, kv, id); got.State != TryError || got.Said != ended.Said {
|
|
t.Errorf("try = %+v, want the hand's end kept", got)
|
|
}
|
|
if _, err := EndTry(endable{kv}, id, "again", time.Now()); err == nil || !strings.Contains(err.Error(), "ended already") {
|
|
t.Errorf("a second end answered %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Issue 734: a pull request's check whose ask the build seat gave up on is asked again under its claim, its status
|
|
// pending, and judged; one given up every time fails as not heard and is judged again when the nodes change.
|
|
func TestACheckWhoseAskWasGivenUpIsAskedAgain(t *testing.T) {
|
|
ctx := context.Background()
|
|
for _, row := range []struct {
|
|
lose int
|
|
state string
|
|
asks int
|
|
}{
|
|
{1, delivery.VerdictPass, 2},
|
|
{LostAskTries, delivery.VerdictFail, LostAskTries},
|
|
} {
|
|
b := newBench(t)
|
|
l := losing(b, row.lose)
|
|
r := newRig(t, b.ex)
|
|
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
r.ch.Wait()
|
|
v := r.verdict(t, keyOf(t, "feat/x", cand))
|
|
if v.State != row.state || len(l.Asks()) != row.asks {
|
|
t.Errorf("lose %d: verdict %s (%s) after %d asks, want %s after %d", row.lose, v.State, v.Failed.Words, len(l.Asks()), row.state, row.asks)
|
|
}
|
|
if row.state == delivery.VerdictFail && (!IsLostAsk(v) || !IsNodesFailure(v)) {
|
|
t.Errorf("a check given up every time = %q, want lost and judged again when the nodes change", v.Failed.Words)
|
|
}
|
|
}
|
|
}
|