Apply a binding that differs, and never repeat a generation a node passed

A licence store rebuilt after issue 241 counted generations from one again,
and nodes that apply only a higher number discarded the login move and the
rotations unseen. Nodes now apply any binding other than the one applied;
the manager moves its sequence past every generation a node reports. hq
issue 243.
This commit is contained in:
jochen
2026-10-05 09:59:19 +02:00
parent 316576f1da
commit b91b4c427d
6 changed files with 83 additions and 3 deletions
@@ -433,3 +433,31 @@ func TestAnAPIKeyIsHandedOverSealedAndItsFileRemoved(t *testing.T) {
t.Fatalf("the key crossed in the clear: %v", sent)
}
}
// A manager whose store was rebuilt counts generations from one again (novox/hq issue 243): a binding with a
// lower generation than the one applied is still a binding to apply, and only the one applied is skipped.
func TestALowerGenerationAfterTheManagerWasRebuiltIsStillApplied(t *testing.T) {
p, w := node(t, "laptop")
var asked []string
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 16},
seat(t, "personal", "at-old", 16, &asked), writer(w)); err != nil {
t.Fatal(err)
}
// The rotation that came with it: a later token, as a refresh hands one over.
later := func(address string, args any) (json.RawMessage, error) {
asked = append(asked, address)
g, _ := json.Marshal(Grant{AccessToken: "at-new", ExpiresAt: now + 7_200_000})
box, err := Seal(string(g), args.(map[string]any)["public_key"].(string))
if err != nil {
t.Fatal(err)
}
return json.Marshal(Current{Licence: "personal", Kind: "subscription", Generation: 3, Sealed: &box})
}
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3},
later, writer(w)); err != nil {
t.Fatal(err)
}
if len(asked) != 2 || creds(t, p)["accessToken"] != "at-new" || HoldingsOf(p).Generation != 3 {
t.Fatalf("asked %v, credentials %v: a lower generation was ignored", asked, creds(t, p))
}
}
+8 -3
View File
@@ -255,14 +255,19 @@ func Pull(p Paths, ask Ask, write WriteManaged) (map[string]any, error) {
}
// OnBinding takes a change to this node's key in the manager's `bindings` state (ADR 0206): the token is
// fetched when the generation is newer than the one applied. A released binding keeps the last token,
// which lives hours, and says so.
// fetched when the binding differs from the one applied. A released binding keeps the last token, which
// lives hours, and says so.
//
// **Differs, not "is newer"** (novox/hq issue 243). The state keeps only the latest value per node, so
// nothing older can arrive. A manager whose store was rebuilt counts generations from one again, and
// a node that waited for a number above its own ignored every binding it was sent, its login and the
// licence's rotations included, until the count caught up. Only the binding already applied is skipped.
func OnBinding(p Paths, b *BindingState, ask Ask, write WriteManaged) (string, error) {
if b == nil {
return "this node's binding was released; it keeps its last token until it expires", nil
}
var applied Binding
if readJSON(p.binding(), &applied) && applied.Generation >= b.Generation {
if readJSON(p.binding(), &applied) && applied.Generation == b.Generation && applied.Licence == b.Licence {
return "", nil
}
out, err := Pull(p, ask, write)