From 5eda1c287ba9d4e043ce2581917a6a1e4dc5ad07 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 13:56:09 +0200 Subject: [PATCH] Keep an approval asked through a silence of its condition (hq ADR 0259, the confirmation review's M1) Choosing Silence silenced the condition, the condition was no longer wanted, and the next reconcile cancelled the Restart or Release ask beside it: an acknowledgement, which any desk click may give, took an approval back. An open approval ask now stays until it is answered or expires while its condition is open and silenced with the same answers. The test silences as the controller does; it failed before (0 open) and passes, and the kept Restart is performed on its warrant. --- cmd/mesh-controller/asker.go | 31 +++++++++++++++++++++++++++---- cmd/mesh-controller/asker_test.go | 12 ++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) 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) + } }