diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 716be76e..7b0aa40d 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -2444,6 +2444,16 @@ func TestGrants(t *testing.T) { if v := verdictOf(); !v.Refused() || !slices.Equal(v.Problems, []string{"p"}) || verdicts.Status().Asks != asked { t.Errorf("manifest-refused, then a revision already held: %+v, asks %d (was %d)", v, verdicts.Status().Asks, asked) } + // A revision below the picture's (ADR 0615 §5; the verdicts made again restart low): the same verdict again is a + // redelivery, passed over; another verdict asks the first view once more. + heard(delivery.ManifestRefusedEvent, delivery.ManifestRefused{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Problems: []string{"p"}, Revision: 3}) + if verdicts.Status().Asks != asked { + t.Errorf("a held verdict redelivered at a lower revision asked: asks %d (was %d)", verdicts.Status().Asks, asked) + } + heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 2}) + waitFor(t, func() bool { return verdicts.Status().Asks > asked }) + waitFor(t, func() bool { s := verdicts.Status(); return !s.Answered.Before(s.Asked) }) + asked = verdicts.Status().Asks heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j"}) waitFor(t, func() bool { return verdicts.Status().Asks > asked }) asked = verdicts.Status().Asks diff --git a/modules/mesh-grants/internal/grants/firstviews.go b/modules/mesh-grants/internal/grants/firstviews.go index 4a6d017c..e7240e04 100644 --- a/modules/mesh-grants/internal/grants/firstviews.go +++ b/modules/mesh-grants/internal/grants/firstviews.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" "strings" "sync" @@ -259,6 +260,18 @@ func (v *Views) HearVerdict(ctx context.Context, event string, data []byte, logf } return gap(fmt.Sprintf("the indexer's %s of %s at revision %d names no manifest", event, verdict.Module, revision)) } + // A revision below the picture's is a gap (ADR 0615 §5): the verdict events carry no mark of when the indexer's verdicts + // were made, so one made again shows only as revisions starting low. A redelivery of a verdict the picture already + // holds for its manifest is passed over, so a consumer's backlog after a restart asks nothing. + if revision < held.Revision { + if raw, found := f.Get(key); found { + if was, err := delivery.DecodeManifestVerdict(raw); err == nil && sameVerdict(was, verdict) { + return nil + } + } + return gap(fmt.Sprintf("the indexer's %s of %s at revision %d, below the picture's %d: its verdicts were made again, or this verdict was missed", + event, verdict.Module, revision, held.Revision)) + } value, err := json.Marshal(verdict) if err != nil { return err @@ -270,6 +283,11 @@ func (v *Views) HearVerdict(ctx context.Context, event string, data []byte, logf return nil } +// sameVerdict says two verdicts of one manifest say the same: its module, accepted or refused, the judge and the problems. +func sameVerdict(a, b delivery.ManifestVerdict) bool { + return a.Module == b.Module && a.State == b.State && a.Judge == b.Judge && slices.Equal(a.Problems, b.Problems) +} + // unansweredCondition is FirstViewUnanswered for one first view, raised or cleared. func unansweredCondition(fv FirstView, s views.Status, raised bool) asks.Condition { c := asks.Condition{Key: FirstViewUnanswered + "." + fv.Owner + "." + fv.State, Subject: asks.Scope{Scope: "module", ID: Module}}