diff --git a/cmd/mesh-controller/asker.go b/cmd/mesh-controller/asker.go index 3b28ba5e..e589d2a8 100644 --- a/cmd/mesh-controller/asker.go +++ b/cmd/mesh-controller/asker.go @@ -397,11 +397,24 @@ func (a *asker) reconcile(ctx context.Context) error { openNow++ } } + stillOpen := map[string]conditions.Condition{} + for _, c := range open { + stillOpen[c.Key] = c + } for key, r := range byCondition { - if !wanted[key] { - if err := a.cancel(ctx, r, "the condition ended, was silenced or needs nothing now"); err != nil { - return err - } + if wanted[key] { + continue + } + // **A silence never takes an approval back** (the confirmation review of 2026-10-09, M1). Silence is an + // acknowledgement — anyone at the desk may give it — so a condition silenced while its approval is asked + // keeps that ask open, unchanged, until it is answered on a channel that proves who answered, or expires. + // It is not asked again once it ends, while the silence lasts. + if c, open := stillOpen[r.Condition]; open && c.SilencedAt(now) && r.Ask.Highest() != asks.Acknowledge && + now.Before(r.Ask.Expires) && keepsItsAnswers(c, r) { + continue + } + if err := a.cancel(ctx, r, "the condition ended, was silenced or needs nothing now"); err != nil { + return err } } why := "the router refused the ask" @@ -411,6 +424,16 @@ func (a *asker) reconcile(ctx context.Context) error { return a.sayUnasked(ctx, unasked, why) } +// keepsItsAnswers says a condition still offers the answers an ask kept was asked with. +func keepsItsAnswers(c conditions.Condition, r asked) bool { + for _, p := range partsOf(c) { + if partKey(c.Key, p.name) == partKey(r.Condition, r.Part) { + return sameAsked(r.Actions, p.actions) + } + } + return false +} + // sourceAsker raises the asker's own condition. const sourceAsker = "asker" diff --git a/cmd/mesh-controller/asker_test.go b/cmd/mesh-controller/asker_test.go index 024fd1fd..225c38e3 100644 --- a/cmd/mesh-controller/asker_test.go +++ b/cmd/mesh-controller/asker_test.go @@ -83,6 +83,13 @@ func newAskerRig(t *testing.T) *askerRig { open: func(context.Context) ([]conditions.Condition, error) { return r.open, nil }, silence: func(_ context.Context, key string, d time.Duration, by, why string) error { r.silenced = append(r.silenced, key+" for "+d.String()+" by "+by+" because "+why) + // As the controller's conditions do (the confirmation review of 2026-10-09, M1): the condition is + // silenced from now on, so what is asked next sees it silenced. + for i := range r.open { + if r.open[i].Key == key { + r.open[i].Silenced = &conditions.Silence{Until: r.now.Add(d), By: by, Why: why, Since: r.now} + } + } return nil }, store: r.store, @@ -641,4 +648,9 @@ func TestAnAcknowledgementNeverSharesAnAskWithAnApproval(t *testing.T) { if open != 1 || len(r.asksSent(t)) != 2 { t.Errorf("after the silence: %d open, %d asked", open, len(r.asksSent(t))) } + // And the approval still answers: Restart chosen on a channel that proves who answered is performed. + answerWith(t, r, r.warrantFor(t, key, "Restart")) + if len(r.called) != 1 || !strings.HasPrefix(r.called[0], "node-service-manager.restart@shanks") { + t.Errorf("the approval kept through a silence was not performed: %v", r.called) + } }