diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go index 6906ffd..de366a4 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go @@ -18,7 +18,6 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" - "mesh-delivery-watchman/internal/watch" ) // busConf is the composed accounts file's shape (mesh-controller internal/broker): the account MESH imports the system @@ -100,7 +99,7 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 {Bucket: delivery.PresenceBucket, History: 20}, {Bucket: delivery.StuckBucket, History: 20}, {Bucket: delivery.PulseBucket, LimitMarkerTTL: time.Second}, - {Bucket: watch.ChecksBucket, History: 5}, + {Bucket: delivery.StepsBucket("mesh-build-go"), History: 5}, } { kv, err := js.CreateKeyValue(ctx, c) if err != nil { @@ -167,19 +166,25 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 n1.Close() until("node n1 offline", func() bool { on, ok := online("n1"); return ok && !on }) - // A check running gets a pulse, as its builder puts the key (mesh-controller CheckRunning); a builder that stops - // heartbeating lets the pulse expire, and that writes a stuck key. - running := func() []byte { - b, _ := json.Marshal(map[string]any{"head": "abc", "trunk": "def", "builder": "n1", "build": "b9", "started": time.Now().UTC()}) - return b + // An ask put on a build seat gets a pulse; one no node takes lets the pulse expire, and that writes a stuck key. + askStream, _ := delivery.AskStream(delivery.AskGo) + askSubject, _ := delivery.AskSubject(delivery.AskGo, delivery.AskCheck) + putAsk := func(id string) { + t.Helper() + body, err := json.Marshal(delivery.StepAsk{V: delivery.StepAskVersion, ID: id, Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: "mesh-delivery-checker", At: time.Now().UTC()}) + if err != nil { + t.Fatal(err) + } + if err := ctl.Publish(askSubject, body); err != nil { + t.Fatal(err) + } } - if _, err := kvs[watch.ChecksBucket].Put(ctx, "o.r.7", running()); err != nil { - t.Fatal(err) - } - until("a pulse for the check running", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(watch.ChecksBucket, "o.r.7")); return ok }) + const ask1, ask2 = "m0000001aaaaaa", "m0000002bbbbbb" + putAsk(ask1) + until("a pulse for the ask waiting", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(askStream, ask1)); return ok }) var stuck delivery.Stuck - until("a stuck key for the check whose builder stopped", func() bool { - e, ok := has(delivery.StuckBucket, stuckOf(watch.ChecksBucket, "o.r.7")) + until("a stuck key for the ask no node took", func() bool { + e, ok := has(delivery.StuckBucket, stuckOf(askStream, ask1)) if !ok { return false } @@ -187,7 +192,7 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 stuck, err = delivery.DecodeStuck(e.Value()) return err == nil }) - if stuck.Moved || stuck.Waited != "the check of o.r.7 to end" || stuck.Bucket != watch.ChecksBucket || stuck.Key != "o.r.7" { + if stuck.Moved || stuck.Bucket != askStream || stuck.Key != ask1 { t.Fatalf("stuck = %+v", stuck) } // The node n1, gone offline and never back, is stuck as well. @@ -204,17 +209,15 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 connect("node.n2") until("node n2 online again", func() bool { on, ok := online("n2"); return ok && on }) until("the pulse of node n2 deleted", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(delivery.PresenceBucket, "n2")); return !ok }) - // A check that ends at once is the same: no stuck key, however long the bound passes. - if _, err := kvs[watch.ChecksBucket].Put(ctx, "o.r.8", running()); err != nil { + // An ask a node takes at once is the same: no stuck key, however long the bound passes. + putAsk(ask2) + until("a pulse for the second ask", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(askStream, ask2)); return ok }) + if _, err := kvs[delivery.StepsBucket("mesh-build-go")].Put(ctx, "n1."+ask2, []byte(`{}`)); err != nil { t.Fatal(err) } - until("a pulse for the second check", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(watch.ChecksBucket, "o.r.8")); return ok }) - if err := kvs[watch.ChecksBucket].Delete(ctx, "o.r.8"); err != nil { - t.Fatal(err) - } - until("the pulse of the second check deleted", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(watch.ChecksBucket, "o.r.8")); return !ok }) + until("the pulse of the second ask deleted", func() bool { _, ok := has(delivery.PulseBucket, pulseOf(askStream, ask2)); return !ok }) time.Sleep(6 * time.Second) // twice the bound and the marker time - for _, k := range []string{stuckOf(delivery.PresenceBucket, "n2"), stuckOf(watch.ChecksBucket, "o.r.8")} { + for _, k := range []string{stuckOf(delivery.PresenceBucket, "n2"), stuckOf(askStream, ask2)} { if _, ok := has(delivery.StuckBucket, k); ok { t.Errorf("a stuck key %s was written for a move that happened before the bound", k) } diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go index ba95465..ac5ec78 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go @@ -1,5 +1,5 @@ // Command mesh-delivery-watchman is delivery's watchman (novox/hq ADR 0429 §1, ADR 0431, ADR 0433, issue 1135): it -// hears the bus's connect and disconnect events and writes presence, mirrors every move a machine owes (a check running, +// hears the bus's connect and disconnect events and writes presence, mirrors every move a machine owes (a build ask waiting for a node, // a node or part gone offline) into the pulse bucket with a time to live, and writes // a stuck key when the server expires one. It holds no timer: every reaction is an event, a watch or a request at // start or at a reconnect, and the server's expiry is the only clock. @@ -179,6 +179,17 @@ func follow(ctx context.Context, w jetstream.KeyWatcher, each func(jetstream.Key } } +// stepTaken is a change of a build module's steps: a step key written for an ask is that ask taken by a node. The node's +// `paused.` key and a key that is no step key are left. +func stepTaken(w *watch.Watchman, language string, e jetstream.KeyValueEntry) { + if opOf(e) != watch.Put { + return + } + if _, id, err := delivery.ParseStepKey(e.Key()); err == nil { + w.Taken(language, id) + } +} + // run is the program: it returns when ctx ends or the bus cannot be used. func run(ctx context.Context, getenv func(string) string, logOut io.Writer) error { logger := log.New(logOut, "mesh-delivery-watchman: ", log.LstdFlags) @@ -223,12 +234,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } return kv, nil } - var kvs struct{ presence, pulse, stuck, checks jetstream.KeyValue } + var kvs struct{ presence, pulse, stuck jetstream.KeyValue } for _, b := range []struct { name string into *jetstream.KeyValue - }{{delivery.PresenceBucket, &kvs.presence}, {delivery.PulseBucket, &kvs.pulse}, {delivery.StuckBucket, &kvs.stuck}, - {watch.ChecksBucket, &kvs.checks}} { + }{{delivery.PresenceBucket, &kvs.presence}, {delivery.PulseBucket, &kvs.pulse}, {delivery.StuckBucket, &kvs.stuck}} { if *b.into, err = bucket(b.name); err != nil { return err } @@ -275,11 +285,22 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err != nil { return err } - checks, err := settle(ctx, kvs.checks, func(e jetstream.KeyValueEntry) { - w.Check(e.Key(), e.Value(), e.Created(), opOf(e)) - }) - if err != nil { - return err + // The build seats' steps: a step key `.` written is the ask taken (issue 1887 A8b). A seat whose build module + // keeps no steps bucket (yet) is said and left; the asks of its stream are mirrored all the same. + taken := map[string]jetstream.KeyWatcher{} + for _, lang := range delivery.AskLanguages { + module, _ := delivery.BuildModuleOf(lang) + kv, err := js.KeyValue(ctx, delivery.StepsBucket(module)) + if err != nil { + logger.Printf("the steps of %s cannot be read (%v): its asks are mirrored and never seen taken", module, err) + continue + } + lang := lang + sw, err := settle(ctx, kv, func(e jetstream.KeyValueEntry) { stepTaken(w, lang, e) }) + if err != nil { + return err + } + taken[lang] = sw } // The events first, then the one request of the open connections, so no connection falls between them. @@ -297,6 +318,23 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } } } + // The asks put on the build seats' streams: a plain subscription hears each as it is published, as the stream captures it. + for _, lang := range delivery.AskLanguages { + for _, kind := range delivery.AskKinds { + subject, _ := delivery.AskSubject(lang, kind) + lang := lang + if _, err := conn.Subscribe(subject, func(m *nats.Msg) { + a, err := delivery.DecodeStepAsk(m.Data) + if err != nil { + logger.Printf("an ask on %s is not readable: %v", m.Subject, err) + return + } + w.Asked(lang, a.ID, a.At) + }); err != nil { + return err + } + } + } for subject, open := range map[string]bool{connectSubject: true, disconnectSubject: false} { if _, err := conn.Subscribe(subject, onEvent(open)); err != nil { return err @@ -317,13 +355,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } w.Reconcile() - errs := make(chan error, 4) + errs := make(chan error, 8) go func() { errs <- follow(ctx, pulses, func(e jetstream.KeyValueEntry) { w.Pulse(e.Key(), opOf(e), e.Created()) }) }() - go func() { - errs <- follow(ctx, checks, func(e jetstream.KeyValueEntry) { w.Check(e.Key(), e.Value(), e.Created(), opOf(e)) }) - }() + for lang, sw := range taken { + lang, sw := lang, sw + go func() { errs <- follow(ctx, sw, func(e jetstream.KeyValueEntry) { stepTaken(w, lang, e) }) }() + } go func() { for { select { diff --git a/modules/mesh-delivery-watchman/internal/watch/mirror.go b/modules/mesh-delivery-watchman/internal/watch/mirror.go index 9e6a767..8297755 100644 --- a/modules/mesh-delivery-watchman/internal/watch/mirror.go +++ b/modules/mesh-delivery-watchman/internal/watch/mirror.go @@ -1,42 +1,42 @@ package watch import ( - "encoding/json" + "git.novox.be/novox/mesh-sdk/go/delivery" "time" ) -// ChecksBucket is the bucket of the checks the builders are running (mesh-controller internal/broker -// CheckRunningBucket). -const ChecksBucket = "mesh-controller_checks-running" +// A waiting ask is a move (novox/hq ADR 0525 §4, issue 1887 A8b): a message on a build seat's stream (mesh-sdk go/delivery +// AskStream) that a node of the seat must pull. Its pulse is the pulse of the bucket named as the stream, under the ask's id; +// the ask put on the seat is the move beginning at its time, and the ask taken (its step key `.` written in +// the build module's steps, delivery.StepsBucket) is the move done. An ask no node takes within the bound is stuck, as any +// move. The watchman reads no bucket of the controller for it. -// checkRecord is the part of a running check's key the watchman reads, as the controller's builder writes it -// (mesh-controller internal/link/checks_nats.go CheckRunning): the head being checked. -type checkRecord struct { - Head string `json:"head"` -} +// AskedKey names a waiting ask for the pulse and stuck keys: the seat's stream and the ask's id. +func askedBucket(language string) (string, error) { return delivery.AskStream(language) } -// Check is a change in the checks-running bucket. The builder puts a check's key when the check starts and puts it -// again at every heartbeat while the check lives, and deletes it when the check ends. Each new revision of the key is -// the check moving: the pulse is deleted and written again with the full bound from that revision's time, so only a -// check whose builder stops heartbeating for the whole bound is stuck; the key deleted is the check ended. An aged-out -// key is the bucket's own expiry of a builder that died: the pulse is left to run out. -func (w *Watchman) Check(key string, value []byte, at time.Time, op Op) { - w.mu.Lock() - defer w.mu.Unlock() - switch op { - case Expired: - return - case Delete: - w.moved(ChecksBucket, key) +// Asked is an ask of a build seat of language put on its stream at time at (the ask's own time). +func (w *Watchman) Asked(language, id string, at time.Time) { + bucket, err := askedBucket(language) + if err != nil { + w.say("no pulse for the ask %s: %v", id, err) return } - var rec checkRecord - _ = json.Unmarshal(value, &rec) + w.mu.Lock() + defer w.mu.Unlock() if at.IsZero() { at = w.now() } - if _, stalled := w.ttl(w.now().Sub(at)); !stalled { - w.stuckMoved(ChecksBucket, key) + w.mirror(bucket, id, describe("the ask %s to be taken by a node", id), "", at, 0) +} + +// Taken is the ask taken by a node: its step key written. The pulse is deleted, so it never expires, and a stuck key it left is +// written as moved. +func (w *Watchman) Taken(language, id string) { + bucket, err := askedBucket(language) + if err != nil { + return } - w.mirror(ChecksBucket, key, describe("the check of %s to end", key), rec.Head, at, 0) + w.mu.Lock() + defer w.mu.Unlock() + w.moved(bucket, id) } diff --git a/modules/mesh-delivery-watchman/internal/watch/watch_test.go b/modules/mesh-delivery-watchman/internal/watch/watch_test.go index 4dd5d3f..1c8a6be 100644 --- a/modules/mesh-delivery-watchman/internal/watch/watch_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/watch_test.go @@ -74,7 +74,7 @@ func checkValue(started time.Time, head string) []byte { } const ( - checksKey = "mesh-controller_checks-running" + checksKey = "SEAT_NODE_BUILD_GO" presenceNS = delivery.PresenceBucket ) @@ -207,13 +207,13 @@ func TestRebuildTakesThePresenceOfWhoIsGone(t *testing.T) { // 1135/AC2 func TestRebuildPulsesFromCurrentKeys(t *testing.T) { // 1135/AC2 w, o, c := setup() c.t = t0.Add(30 * time.Second) - w.Check("o.r.7", checkValue(t0, "abc"), t0.Add(10*time.Second), Put) + w.Asked("go", "askid1", t0.Add(10*time.Second)) w.SeedPresence("ace", false, t0) w.Reconcile() if err := w.Rebuild(func([]byte) ([]byte, error) { return connzAnswer(), nil }); err != nil { t.Fatal(err) } - for _, k := range []string{pulseKeyOf(t, checksKey, "o.r.7"), pulseKeyOf(t, presenceNS, "ace")} { + for _, k := range []string{pulseKeyOf(t, checksKey, "askid1"), pulseKeyOf(t, presenceNS, "ace")} { if _, ok := o.pulses[k]; !ok { t.Errorf("no pulse %s; have %v", k, o.pulses) } @@ -222,11 +222,11 @@ func TestRebuildPulsesFromCurrentKeys(t *testing.T) { // 1135/AC2 func TestReconcileDropsPulsesNothingMirrorsAnyMore(t *testing.T) { // 1135/AC2 w, o, _ := setup() - stale := pulseKeyOf(t, checksKey, "o.r.gone") - live := pulseKeyOf(t, checksKey, "o.r.7") + stale := pulseKeyOf(t, checksKey, "gone") + live := pulseKeyOf(t, checksKey, "askid1") w.SeedPulse(stale) w.SeedPulse(live) - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) + w.Asked("go", "askid1", t0) w.Reconcile() if !slices.Equal(o.pulseDeletes, []string{stale}) { t.Fatalf("deleted = %v, want only %s", o.pulseDeletes, stale) @@ -239,12 +239,12 @@ func TestReconcileDropsPulsesNothingMirrorsAnyMore(t *testing.T) { // 1135/AC2 func TestPulseTimeToLiveIsTheBoundMinusTheAge(t *testing.T) { // 1135/AC3 w, o, c := setup() c.t = t0.Add(30 * time.Second) - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) // the key found at start, last renewed 30s ago - k := pulseKeyOf(t, checksKey, "o.r.7") + w.Asked("go", "askid1", t0) // the key found at start, last renewed 30s ago + k := pulseKeyOf(t, checksKey, "askid1") if o.ttls[k] != 90*time.Second { t.Fatalf("ttl = %v, want 90s (last renewed 30s ago)", o.ttls[k]) } - if p := o.pulses[k]; p.Bucket != checksKey || p.Key != "o.r.7" { + if p := o.pulses[k]; p.Bucket != checksKey || p.Key != "askid1" { t.Fatalf("pulse = %+v", p) } w.Connect("node.ace") @@ -257,15 +257,15 @@ func TestPulseTimeToLiveIsTheBoundMinusTheAge(t *testing.T) { // 1135/AC3 func TestAMoveAtTheBoundIsStuckAtOnce(t *testing.T) { // 1135/AC3 w, o, c := setup() c.t = t0.Add(120 * time.Second) - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) + w.Asked("go", "askid1", t0) if len(o.pulses) != 0 { t.Fatalf("a pulse was written for a heartbeat 120s old: %v", o.pulses) } - s, ok := o.stuck[stuckKeyOf(t, checksKey, "o.r.7")] + s, ok := o.stuck[stuckKeyOf(t, checksKey, "askid1")] if !ok { t.Fatalf("no stuck key; have %v", o.stuck) } - if s.Moved || !s.Since.Equal(t0) || s.Waited != "the check of o.r.7 to end" { + if s.Moved || !s.Since.Equal(t0) || s.Waited != "the ask askid1 to be taken by a node" { t.Fatalf("stuck = %+v", s) } } @@ -273,13 +273,13 @@ func TestAMoveAtTheBoundIsStuckAtOnce(t *testing.T) { // 1135/AC3 func TestWaitedInWords(t *testing.T) { // 1135/AC3 w, o, c := setup() c.t = t0.Add(200 * time.Second) - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) + w.Asked("go", "askid1", t0) w.SeedPresence("ace", false, t0) w.SeedPresence("part.mesh-delivery-view.ace", false, t0) w.Reconcile() _ = w.Rebuild(func([]byte) ([]byte, error) { return connzAnswer(), nil }) want := map[string]string{ - stuckKeyOf(t, checksKey, "o.r.7"): "the check of o.r.7 to end", + stuckKeyOf(t, checksKey, "askid1"): "the ask askid1 to be taken by a node", stuckKeyOf(t, presenceNS, "ace"): "ace to come back online", stuckKeyOf(t, presenceNS, "part.mesh-delivery-view.ace"): "mesh-delivery-view on ace to come back online", } @@ -292,14 +292,14 @@ func TestWaitedInWords(t *testing.T) { // 1135/AC3 func TestEveryHeartbeatRenewsThePulse(t *testing.T) { // 1135/AC3 w, o, c := setup() - k := pulseKeyOf(t, checksKey, "o.r.7") - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) + k := pulseKeyOf(t, checksKey, "askid1") + w.Asked("go", "askid1", t0) if o.ttls[k] != 120*time.Second { t.Fatalf("ttl = %v, want the full 120s", o.ttls[k]) } o.ttls[k] = 0 c.t = t0.Add(100 * time.Second) - w.Check("o.r.7", checkValue(t0, "abc"), c.t, Put) // the builder re-puts the key: same check, started long ago + w.Asked("go", "askid1", c.t) // the builder re-puts the key: same check, started long ago if o.ttls[k] != 120*time.Second { t.Fatalf("a heartbeat left the pulse as it was: ttl = %v, want the full 120s again", o.ttls[k]) } @@ -308,7 +308,7 @@ func TestEveryHeartbeatRenewsThePulse(t *testing.T) { // 1135/AC3 } // The same revision heard twice changes nothing. o.ttls[k] = 0 - w.Check("o.r.7", checkValue(t0, "abc"), c.t, Put) + w.Asked("go", "askid1", c.t) if o.ttls[k] != 0 { t.Fatal("one revision rewrote the pulse twice") } @@ -317,10 +317,10 @@ func TestEveryHeartbeatRenewsThePulse(t *testing.T) { // 1135/AC3 func TestMirroredMovesDeleteThePulse(t *testing.T) { // 1135/AC4 w, o, c := setup() c.t = t0.Add(10 * time.Second) - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) + w.Asked("go", "askid1", t0) w.Connect("node.ace") w.Disconnect("node.ace") - w.Check("o.r.7", nil, c.t, Delete) // the check ended + w.Taken("go", "askid1") // the check ended w.Connect("node.ace") if len(o.pulses) != 0 { t.Fatalf("pulses left: %v", o.pulses) @@ -335,12 +335,12 @@ func TestMirroredMovesDeleteThePulse(t *testing.T) { // 1135/AC4 func TestACheckWhoseBuilderStopsIsStuckFromItsLastHeartbeat(t *testing.T) { // 1135/AC4 w, o, c := setup() - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) + w.Asked("go", "askid1", t0) c.t = t0.Add(50 * time.Second) - w.Check("o.r.7", checkValue(t0, "abc"), c.t, Put) // the last heartbeat + w.Asked("go", "askid1", c.t) // the last heartbeat c.t = t0.Add(170 * time.Second) - w.Pulse(pulseKeyOf(t, checksKey, "o.r.7"), Expired, c.t) - s := o.stuck[stuckKeyOf(t, checksKey, "o.r.7")] + w.Pulse(pulseKeyOf(t, checksKey, "askid1"), Expired, c.t) + s := o.stuck[stuckKeyOf(t, checksKey, "askid1")] if !s.Since.Equal(t0.Add(50 * time.Second)) { t.Fatalf("stuck = %+v, want since the last heartbeat", s) } @@ -348,35 +348,35 @@ func TestACheckWhoseBuilderStopsIsStuckFromItsLastHeartbeat(t *testing.T) { // 1 func TestAStuckKeyIsWrittenMovedNeverDeleted(t *testing.T) { // 1135/AC4 w, o, c := setup() - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) - pk := pulseKeyOf(t, checksKey, "o.r.7") + w.Asked("go", "askid1", t0) + pk := pulseKeyOf(t, checksKey, "askid1") c.t = t0.Add(120 * time.Second) w.Pulse(pk, Expired, c.t) - sk := stuckKeyOf(t, checksKey, "o.r.7") + sk := stuckKeyOf(t, checksKey, "askid1") if o.stuck[sk].Moved { t.Fatal("stuck written as moved at expiry") } c.t = t0.Add(300 * time.Second) - w.Check("o.r.7", nil, c.t, Delete) + w.Taken("go", "askid1") s := o.stuck[sk] - if !s.Moved || !s.Since.Equal(t0) || s.Waited != "the check of o.r.7 to end" { + if !s.Moved || !s.Since.Equal(t0) || s.Waited != "the ask askid1 to be taken by a node" { t.Fatalf("stuck after the move = %+v, want the same key moved", s) } n := o.stuckWrites - w.Check("o.r.7", nil, c.t, Delete) + w.Taken("go", "askid1") if o.stuckWrites != n { t.Fatal("a stuck key already moved was written again") } } -func TestAHeartbeatAfterStuckMovesIt(t *testing.T) { // 1135/AC4 +func TestAnAskTakenAfterStuckMovesIt(t *testing.T) { // 1135/AC4 w, o, c := setup() - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) + w.Asked("go", "askid1", t0) c.t = t0.Add(120 * time.Second) - w.Pulse(pulseKeyOf(t, checksKey, "o.r.7"), Expired, c.t) - w.Check("o.r.7", checkValue(t0, "abc"), c.t, Put) // the builder is back - if !o.stuck[stuckKeyOf(t, checksKey, "o.r.7")].Moved { - t.Fatal("a check heartbeating again is still stuck") + w.Pulse(pulseKeyOf(t, checksKey, "askid1"), Expired, c.t) + w.Taken("go", "askid1") // a node took it at last + if !o.stuck[stuckKeyOf(t, checksKey, "askid1")].Moved { + t.Fatal("an ask taken after it was stuck is still stuck") } } @@ -398,23 +398,23 @@ func TestPresenceStuckThenBackOnline(t *testing.T) { // 1135/AC4 func TestExpiryMarkerWritesStuck(t *testing.T) { // 1135/AC5 w, o, c := setup() - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) + w.Asked("go", "askid1", t0) c.t = t0.Add(120 * time.Second) - w.Pulse(pulseKeyOf(t, checksKey, "o.r.7"), Expired, c.t) - s, ok := o.stuck[stuckKeyOf(t, checksKey, "o.r.7")] + w.Pulse(pulseKeyOf(t, checksKey, "askid1"), Expired, c.t) + s, ok := o.stuck[stuckKeyOf(t, checksKey, "askid1")] if !ok { t.Fatalf("no stuck key; have %v", o.stuck) } - if s.Bucket != checksKey || s.Key != "o.r.7" || s.Waited != "the check of o.r.7 to end" || !s.Since.Equal(t0) || s.Moved { + if s.Bucket != checksKey || s.Key != "askid1" || s.Waited != "the ask askid1 to be taken by a node" || !s.Since.Equal(t0) || s.Moved { t.Fatalf("stuck = %+v", s) } } func TestOwnDeleteOfAPulseWritesNothing(t *testing.T) { // 1135/AC5 w, o, c := setup() - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) - pk := pulseKeyOf(t, checksKey, "o.r.7") - w.Check("o.r.7", nil, c.t, Delete) + w.Asked("go", "askid1", t0) + pk := pulseKeyOf(t, checksKey, "askid1") + w.Taken("go", "askid1") w.Pulse(pk, Delete, c.t) // the delete the watchman made, heard on its own watch w.Pulse(pk, Expired, c.t) w.Pulse("mesh-delivery-watchman_pulse.unknown.key", Expired, c.t) @@ -427,13 +427,13 @@ func TestTheBoundCanBeShortenedForATest(t *testing.T) { // 1135/AC8 w, o, c := setup() w.SetBound(2 * time.Second) c.t = t0.Add(500 * time.Millisecond) - w.Check("o.r.7", checkValue(t0, "abc"), t0, Put) - if got := o.ttls[pulseKeyOf(t, checksKey, "o.r.7")]; got != 1500*time.Millisecond { + w.Asked("go", "askid1", t0) + if got := o.ttls[pulseKeyOf(t, checksKey, "askid1")]; got != 1500*time.Millisecond { t.Fatalf("ttl = %v, want 1.5s", got) } c.t = t0.Add(3 * time.Second) - w.Check("o.r.8", checkValue(t0, "abc"), t0, Put) - if _, ok := o.stuck[stuckKeyOf(t, checksKey, "o.r.8")]; !ok { + w.Asked("go", "askid2", t0) + if _, ok := o.stuck[stuckKeyOf(t, checksKey, "askid2")]; !ok { t.Fatalf("a heartbeat older than the bound is stuck at once; have %v", o.stuck) } } diff --git a/modules/mesh-delivery-watchman/manifest_test.go b/modules/mesh-delivery-watchman/manifest_test.go index 4c2968d..d0344b9 100644 --- a/modules/mesh-delivery-watchman/manifest_test.go +++ b/modules/mesh-delivery-watchman/manifest_test.go @@ -81,7 +81,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6 } reads := slices.Clone(m.Reads) slices.Sort(reads) - if !reflect.DeepEqual(reads, []string{"mesh-controller.checks-running"}) { + if !reflect.DeepEqual(reads, []string{"mesh-build-go.steps", "mesh-build-image.steps", "mesh-build-node.steps"}) { t.Errorf("reads = %v", reads) } for _, k := range []string{"emits", "context", "items", "consumes", "provides", "claims"} { diff --git a/modules/mesh-delivery-watchman/module.json b/modules/mesh-delivery-watchman/module.json index 2b723d2..8c4c713 100644 --- a/modules/mesh-delivery-watchman/module.json +++ b/modules/mesh-delivery-watchman/module.json @@ -3,7 +3,9 @@ "version": "1", "slug": "watchman", "reads": [ - "mesh-controller.checks-running" + "mesh-build-go.steps", + "mesh-build-image.steps", + "mesh-build-node.steps" ], "state": [ { "name": "presence", "history": 20 },