Merge pull request 'Apply a binding that differs, and never repeat a generation a node passed (hq issue 243)' (#45) from fix/a-reset-generation-silences-no-node into main
This commit was merged in pull request #45.
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -49,6 +49,8 @@ type Holdings struct {
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
} `json:"refresh"`
|
||||
ChangedAt string `json:"changedAt"`
|
||||
// Generation is the binding generation the node last applied (novox/hq issue 243).
|
||||
Generation int64 `json:"generation"`
|
||||
}
|
||||
|
||||
// BindingState is what `bindings` holds for one consumer: no secret, only what it should hold and which
|
||||
@@ -189,6 +191,17 @@ func (m *Manager) CandidatesIn(ctx context.Context, reports []Holdings) (map[str
|
||||
// newest first; the first that refreshes is adopted and the rest are settled as skipped without being
|
||||
// exchanged. Answers what was adopted.
|
||||
func (m *Manager) Consider(ctx context.Context, reports []Holdings) ([]string, error) {
|
||||
// **Never a generation a node has already passed** (novox/hq issue 243). A store rebuilt after a loss
|
||||
// counts from one again, while every node still holds the number it last applied. The nodes no longer
|
||||
// wait for a higher number, but a binding numbered exactly as the one a node applied would still be
|
||||
// skipped. So the count is moved past every number reported, before anything here binds.
|
||||
var highest int64
|
||||
for _, r := range reports {
|
||||
highest = max(highest, r.Generation)
|
||||
}
|
||||
if err := m.Store.GenerationsAbove(ctx, highest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byAccount, err := m.CandidatesIn(ctx, reports)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -423,3 +423,18 @@ func TestAnAPIKeySealedByANodeIsAdopted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A store rebuilt after a loss counts generations from one again (novox/hq issue 243): the first look at the
|
||||
// reports moves the count past every generation a node says it applied, so no binding repeats one.
|
||||
func TestARebuiltStoreNeverGivesAGenerationANodeHasPassed(t *testing.T) {
|
||||
mm := newMesh(t)
|
||||
ctx := context.Background()
|
||||
laptop := mm.login("laptop", "rt-a", true, t0)
|
||||
laptop.Generation = 16
|
||||
if _, err := mm.m.Consider(ctx, []Holdings{laptop}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g := mm.state["laptop"].Generation; g <= 16 {
|
||||
t.Fatalf("the laptop applied generation 16 and was given %d", g)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +225,16 @@ func (s *PgStore) Unbind(ctx context.Context, consumer string) (bool, error) {
|
||||
return err == nil && tag.RowsAffected() == 1, err
|
||||
}
|
||||
|
||||
func (s *PgStore) GenerationsAbove(ctx context.Context, generation int64) error {
|
||||
if generation <= 0 {
|
||||
return nil
|
||||
}
|
||||
// setval with is_called, so the next nextval is generation+1; only ever forward.
|
||||
_, err := s.pool.Exec(ctx, `select setval('binding_generation', $1, true) from binding_generation
|
||||
where not is_called or last_value < $1`, generation)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *PgStore) Advance(ctx context.Context, licence string) ([]Binding, error) {
|
||||
return s.bindingsWhere(ctx, `update binding set generation = nextval('binding_generation') where licence = $1
|
||||
returning consumer, licence, generation`, licence)
|
||||
|
||||
@@ -70,6 +70,8 @@ type Store interface {
|
||||
Unbind(ctx context.Context, consumer string) (bool, error)
|
||||
// Advance gives every consumer of a licence a new generation: what a rotation is to them.
|
||||
Advance(ctx context.Context, licence string) ([]Binding, error)
|
||||
// GenerationsAbove makes every generation given from now on greater than this one.
|
||||
GenerationsAbove(ctx context.Context, generation int64) error
|
||||
Outcome(ctx context.Context, fingerprint string) (Outcome, error)
|
||||
RecordOutcome(ctx context.Context, fingerprint, node, account string, o Outcome, why string) error
|
||||
RecordUsage(ctx context.Context, licence string, at int64, r UsageReading, raw map[string]any) error
|
||||
@@ -200,6 +202,13 @@ func (m *MemoryStore) Unbind(_ context.Context, consumer string) (bool, error) {
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (m *MemoryStore) GenerationsAbove(_ context.Context, generation int64) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.generation = max(m.generation, generation)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MemoryStore) Advance(_ context.Context, licence string) ([]Binding, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user