From fc92202e040b726a5cc8b496299f969923fa08f5 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 27 Oct 2026 02:29:20 +0100 Subject: [PATCH] =?UTF-8?q?Take=20a=20later=20composition=20into=20a=20ver?= =?UTF-8?q?dict=20that=20failed=20only=20because=20placed=20nodes=20had=20?= =?UTF-8?q?not=20answered,=20so=20a=20sleeping=20node's=20answer=20passes?= =?UTF-8?q?=20the=20check=20on=20its=20event=20with=20no=20new=20check=20(?= =?UTF-8?q?issue=201126,=20ADR=200559=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 2 ++ .../internal/check/awaiting.go | 25 ++++++++++++++++--- .../internal/check/awaiting_test.go | 21 ++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index d202053..f8e0c4a 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026233910-a2759ec92ec9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012259-a37d2622ed56 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 918a889..76d1b92 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026233910-a2759ec92ec9 h1:MS7i4NVNQHMAMqRRFMdzbto31BHZY+BuEZY1ql3p4Rs= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026233910-a2759ec92ec9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012259-a37d2622ed56 h1:PUc5s3oaWEg0uyVmXhJSSK3x0IXZBXWGqq30U/CSx30= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012259-a37d2622ed56/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/internal/check/awaiting.go b/modules/mesh-delivery-checker/internal/check/awaiting.go index 74abfda..c617e31 100644 --- a/modules/mesh-delivery-checker/internal/check/awaiting.go +++ b/modules/mesh-delivery-checker/internal/check/awaiting.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -96,9 +97,19 @@ func notJudged(v delivery.Verdict) bool { return v.Composition != nil && v.Composition.State == delivery.CompositionNotRun && v.Composition.Words == delivery.AssignerNotRunning } +// unanswered says whether a verdict failed only on its composition, and that only because nodes the change places did +// not answer its placement in time (delivery.CompositionUnanswered, ADR 0559 §5): its tests passed (a composition is read +// only into a passing verdict), so a later composition of the same heads is taken in on its event, with no new check +// (ADR 0548 §1: a part of the mesh not heard is judged again when it is heard). +func unanswered(v delivery.Verdict) bool { + return v.State == delivery.VerdictFail && v.Composition != nil && v.Composition.State == delivery.CompositionFail && + strings.Contains(v.Composition.Words, delivery.CompositionUnanswered) && v.Failed.Words == v.Composition.Words +} + // Composed takes in a composition the assigner wrote after the check's verdict said it was not judged because the -// assigner was not running (the assigner started since): the verdict and its status are written again with it. A -// composition of heads no candidate holds now, or of a verdict that judged its composition, is left. +// assigner was not running (the assigner started since), or after it failed only because placed nodes had not answered +// (unanswered): the verdict and its status are written again with it. A composition of heads no candidate holds now, or +// of a verdict that judged its composition otherwise, is left. func (ch *Checker) Composed(ctx context.Context, comp delivery.CompositionVerdict) { ch.init() if comp.State == delivery.CompositionWaits { @@ -123,7 +134,15 @@ func (ch *Checker) Composed(ctx context.Context, comp delivery.CompositionVerdic return } v, err := delivery.DecodeVerdict(cur) - if err != nil || v.State != delivery.VerdictPass || !notJudged(v) { + if err != nil { + return + } + switch { + case v.State == delivery.VerdictPass && notJudged(v): + case unanswered(v) && comp.Words != v.Composition.Words: + // The nodes answered since (or another failure stands now): the verdict is its tests' pass with this composition. + v.State, v.Failed = delivery.VerdictPass, delivery.FailedFirst{} + default: return } withComposition(&v, comp) diff --git a/modules/mesh-delivery-checker/internal/check/awaiting_test.go b/modules/mesh-delivery-checker/internal/check/awaiting_test.go index c775c49..c373d0a 100644 --- a/modules/mesh-delivery-checker/internal/check/awaiting_test.go +++ b/modules/mesh-delivery-checker/internal/check/awaiting_test.go @@ -113,6 +113,27 @@ func TestTheAssignerStartingBringsTheComposition(t *testing.T) { t.Errorf("the status's line says %q", o.Text) } } + + // ADR 0559 §5: a verdict that failed only because a placed node had not answered takes the same heads' later + // composition in on its event, a pass making it pass with no new check; a composition failing the same way is left. + ch, cs := checkerWithNotJudged(t) + silent := delivery.CompositionVerdict{Group: cs.Group, Heads: cs.HeadsHash, State: delivery.CompositionFail, SDK: "x", Machine: "g14", + Words: "g14 (offline) " + delivery.CompositionUnanswered} + ch.Composed(context.Background(), silent) + for _, row := range []struct { + name string + comp delivery.CompositionVerdict + state string + }{ + {"the same failure again", silent, delivery.VerdictFail}, + {"the node's answer", delivery.CompositionVerdict{Group: cs.Group, Heads: cs.HeadsHash, State: delivery.CompositionPass, SDK: "x"}, delivery.VerdictPass}, + } { + ch.Composed(context.Background(), row.comp) + raw, _, _, _ := ch.Verdicts.Get(context.Background(), cs.Key) + if v, err := delivery.DecodeVerdict(raw); err != nil || v.State != row.state || v.Composition == nil || v.Composition.State != row.comp.State { + t.Errorf("%s: the verdict is %+v, %v", row.name, v, err) + } + } } // checkerWithNotJudged is a checker holding a candidate whose passing verdict said its composition was not judged.