From 42c3db1d5baec02e825dc4bb14670863740db938 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 09:24:04 +0100 Subject: [PATCH] B2h AS17 (2a): the assigner's ask of a held revert has its kind and key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The asks state gains the kind revert, keyed revert... after the reverted build's fingerprint; like the backup ask it needs no proof of who answered, so the view may answer it, and an asking that ends unanswered while the hold stands is written again. Builds: design 47 G §710, §300 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- modules/mesh-delivery-assigner/asks/asks.go | 27 ++++++++++++++----- .../mesh-delivery-assigner/asks/asks_test.go | 12 ++++++--- .../internal/assign/asks_test.go | 4 +++ .../internal/assign/ended.go | 3 ++- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/modules/mesh-delivery-assigner/asks/asks.go b/modules/mesh-delivery-assigner/asks/asks.go index 4542253a..bc29ad14 100644 --- a/modules/mesh-delivery-assigner/asks/asks.go +++ b/modules/mesh-delivery-assigner/asks/asks.go @@ -41,10 +41,13 @@ const ( // AskKindOutside is the kind of the assigner's ask about a moved module whose failing health check has waited past its bound // on an outside cause (design 47 G §706): go on, or count it as failed. `outside...`. AskKindOutside = "outside" + // AskKindRevert is the kind of the assigner's ask about a held revert, one with no restore point (design 47 G §710): keep it + // held, or start the reverted build on the live data. `revert...`, the reverted build's. + AskKindRevert = "revert" ) // AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's Module -// and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest Composed of the +// and fingerprint FP, a restore's, a backup's, an outside's and a revert's Node, Module and FP, a record's Node, Module and the digest Composed of the // composition it would allow, the assignments' Proposal. type AskKey struct { Proposal string @@ -63,11 +66,11 @@ func AskNeedsProof(kind string) (bool, error) { switch kind { case meshdeliveryclient.AskKindMerge: return true, nil - case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside: + case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside, AskKindRevert: return false, nil } - return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold, - AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside) + return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold, + AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside, AskKindRevert) } // HoldAskKey is the key of the ask about a module's held fingerprint: `hold..`. @@ -119,6 +122,16 @@ func BackupAskKey(node, module, fingerprint string) (string, error) { return AskKindBackup + "." + key, nil } +// RevertAskKey is the key of the ask about a held revert of a module on a node to the reverted build's fingerprint: +// `revert...` (design 47 G §710). +func RevertAskKey(node, module, fingerprint string) (string, error) { + key, err := BackupAskKey(node, module, fingerprint) + if err != nil { + return "", err + } + return AskKindRevert + strings.TrimPrefix(key, AskKindBackup), nil +} + // AssignmentsAskKey is the key of the authorising ask about a proposal of assignments: `assignments.` (ADR 0567 §5). func AssignmentsAskKey(proposal string) (string, error) { if err := meshmoduleassignerclient.CheckProposalID(proposal); err != nil { @@ -140,7 +153,7 @@ func RecordAskKey(node, module, composed string) (string, error) { return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil } -// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record or assignments ask); it refuses an unknown +// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record, assignments, outside or revert ask); it refuses an unknown // kind and a key whose parts do not fit its kind, naming the key. func ParseAnyAskKey(key string) (AskKey, error) { kind, rest, _ := strings.Cut(key, ".") @@ -180,10 +193,10 @@ func ParseAnyAskKey(key string) (AskKey, error) { } k.Kind = kind return k, nil - case AskKindBackup: + case AskKindBackup, AskKindRevert: node, module, part, isNode, err := nodeengineclient.ParseComposedKey(rest) if err != nil || isNode { - return refused(fmt.Errorf("a backup ask is backup...")) + return refused(fmt.Errorf("a %s ask is %s...", kind, kind)) } fp, err := catalogkey.ParseKeyPart(part) if err != nil { diff --git a/modules/mesh-delivery-assigner/asks/asks_test.go b/modules/mesh-delivery-assigner/asks/asks_test.go index c522584c..d97babc9 100644 --- a/modules/mesh-delivery-assigner/asks/asks_test.go +++ b/modules/mesh-delivery-assigner/asks/asks_test.go @@ -19,6 +19,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { backup, _ := BackupAskKey("n1", "novox.be", fp) assignments, _ := AssignmentsAskKey("p-20261028t183000-1a2b3c") record, _ := RecordAskKey("n1", "gitea", digest) + revert, _ := RevertAskKey("n1", "nats", fp) for key, want := range map[string]struct { got string back AskKey @@ -28,6 +29,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { "backup": {backup, AskKey{Kind: "backup", Node: "n1", Module: "novox.be", FP: fp}}, "assignments": {assignments, AskKey{Kind: "assignments", Proposal: "p-20261028t183000-1a2b3c"}}, "record": {record, AskKey{Kind: "record", Node: "n1", Module: "gitea", Composed: digest}}, + "revert": {revert, AskKey{Kind: "revert", Node: "n1", Module: "nats", FP: fp}}, } { back, err := ParseAnyAskKey(want.got) if err != nil || back != want.back { @@ -35,7 +37,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { } } for got, want := range map[string]string{hold: "hold.gitea." + hex, restore: "restore.n1.gitea." + hex, backup: "backup.n1.novox=2ebe." + hex, - assignments: "assignments.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32)} { + assignments: "assignments.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32), revert: "revert.n1.nats." + hex} { if got != want { t.Errorf("key %q, want %q", got, want) } @@ -45,7 +47,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { t.Errorf("a merge ask key: %+v, %v", merge, err) } for _, bad := range []string{"", "other.x", "hold.gitea", "hold.gitea.src2:" + hex, "restore.n1", "backup.n1.node", "backup.n1.gitea." + hex[:60], - "assignment.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g"} { + "assignment.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g", "revert.n1.node", "revert.n1.nats." + hex[:60]} { if _, err := ParseAnyAskKey(bad); err == nil { t.Errorf("ParseAnyAskKey(%q) reads a wrong key", bad) } @@ -53,8 +55,10 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { if proof, err := AskNeedsProof("merge"); err != nil || !proof { t.Error("a merge ask needs proof") } - if proof, err := AskNeedsProof("hold"); err != nil || proof { - t.Error("a hold ask needs no proof") + for _, kind := range []string{"hold", "revert"} { + if proof, err := AskNeedsProof(kind); err != nil || proof { + t.Errorf("a %s ask needs no proof", kind) + } } if _, err := AskNeedsProof("view"); err == nil { t.Error("an unknown kind of ask is taken") diff --git a/modules/mesh-delivery-assigner/internal/assign/asks_test.go b/modules/mesh-delivery-assigner/internal/assign/asks_test.go index 3e23b1db..dd3ecd79 100644 --- a/modules/mesh-delivery-assigner/internal/assign/asks_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/asks_test.go @@ -42,6 +42,7 @@ func anAskEndedAtTheChannelIsWrittenAgain(t *testing.T) { holdKey, _ := asks.HoldAskKey("nats", fNew) restoreKey, _ := asks.RestoreAskKey("novox", "nats", fNew) backupKey, _ := asks.BackupAskKey("novox", "nats", fNew) + revertKey, _ := asks.RevertAskKey("novox", "nats", fNew) recordKey, _ := asks.RecordAskKey("novox", "nats", digest) assignmentsKey, _ := asks.AssignmentsAskKey("p-20261028t183000-1a2b3c") need := RecordNeed{Node: "novox", Digest: digest, Judgement: JudgedMovesData} @@ -64,6 +65,9 @@ func anAskEndedAtTheChannelIsWrittenAgain(t *testing.T) { {name: "hold", key: holdKey, a: ptr(HoldAsk(holdKey, "nats", fNew, "novox", "w", AskingSeconds(asked), asked)), outcome: occ.OutcomeExpired}, {name: "restore", key: restoreKey, a: ptr(RestoreAsk(restoreKey, "novox", "nats", fNew, "w", AskingSeconds(asked), asked)), outcome: occ.OutcomeCancelled}, {name: "backup", key: backupKey, a: ptr(BackupAsk(backupKey, "novox", "nats", fNew, digest, "w", 0, asked)), outcome: occ.OutcomeCancelled}, + {name: "revert", key: revertKey, a: ptr(assignerAsk(AskingSeconds(asked), oc.Ask{ID: meshdeliveryclient.AskingID(revertKey, AskingSeconds(asked)), + Headline: "h", Explanation: "e", Who: occ.Operator, Expires: occ.FormatTime(asked.Add(AskLasts)), OnExpiry: "o", + Options: []oc.Option{{ID: "hold", Label: "Hold", Does: "d", Level: occ.Acknowledge}}})), outcome: occ.OutcomeExpired}, {name: "record", key: recordKey, a: &record, outcome: occ.OutcomeCancelled}, {name: "record of another put", key: recordKey, a: &record, outcome: occ.OutcomeExpired, put: &other}, {name: "record a put asked", key: recordKey, a: &byPut, outcome: occ.OutcomeExpired, put: &put, why: "put"}, diff --git a/modules/mesh-delivery-assigner/internal/assign/ended.go b/modules/mesh-delivery-assigner/internal/assign/ended.go index 98985d51..cc05a714 100644 --- a/modules/mesh-delivery-assigner/internal/assign/ended.go +++ b/modules/mesh-delivery-assigner/internal/assign/ended.go @@ -18,7 +18,7 @@ import ( ) // AskLasts is how long an ask of the assigner's rules stands at the operator channel before it lapses unanswered: the hold, -// restore, record and backup asks. +// restore, record, backup, outside and revert asks. const AskLasts = 7 * 24 * time.Hour // writtenAgainOnEnd is, per kind of the assigner's asks, whether an asking that ended at the channel without an answer while @@ -31,6 +31,7 @@ var writtenAgainOnEnd = map[string]string{ asks.AskKindBackup: "", asks.AskKindOutside: "", // raised again at once while the wait stands (design 47 G §706); the wait's end takes the key away asks.AskKindRecord: "", + asks.AskKindRevert: "", // raised again at once while the hold stands (design 47 G §710); one answered hold is not asked again asks.AskKindAssignments: "a proposal's authorising ask that lapsed ends the proposal (ADR 0567 §5); the verb again asks it again (§11)", }