mirror of
https://github.com/hanzoai/ha.git
synced 2026-09-30 22:38:49 +00:00
It was Fencer, which named the wrong actor. Fencing is what the STORE does when it refuses a stale round; an interface that hands back a lease fences nothing. The value it yields is a Lease, so it is Leases -- the same shape as db.Namespaces resolving a namespace to a handle. Recorded in the doc why it is not the other candidates, because each is tempting and each is wrong in an instructive way: Writer has no write method and already means something you write TO; Consensus names one possible source when StaticLeases returns Round 1 from a single process and CAS consults a single register, neither reaching agreement; Leader implies one leader when Owner is per-KEY, so a thousand keys have a thousand owners. Prose still says fencing where it describes the store refusing a stale round. That is the accurate use, and keeping it is what makes the rename legible. Identifiers only -- no behaviour change. Breaking for importers, so this is a minor bump and cloud moves on its own commit. Co-authored-by: Hanzo Dev <[email protected]>
61 lines
2.4 KiB
Go
61 lines
2.4 KiB
Go
// Copyright 2025 Hanzo AI Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
package ha
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
// TestStaticFencerHoldsEveryKeyAtRoundOne proves the single-process Leases is
|
|
// exactly-once by construction: self holds every key at the fixed round 1, never
|
|
// an error — the sole process is the sole writer.
|
|
func TestStaticFencerHoldsEveryKeyAtRoundOne(t *testing.T) {
|
|
f := StaticLeases("solo")
|
|
for _, key := range []string{"acme", "globex", "orgs/acme/iam", ""} {
|
|
l, err := f.Acquire(context.Background(), key)
|
|
if err != nil {
|
|
t.Fatalf("Acquire(%q): unexpected error %v", key, err)
|
|
}
|
|
if l.Owner.ID != "solo" || l.Round != 1 || l.Key != key {
|
|
t.Fatalf("Acquire(%q) = %+v, want {Key:%q Owner:solo Round:1}", key, l, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestStaticFencerIsDeterministic proves repeated Acquire of a key returns a
|
|
// STABLE lease (round does not drift for a stable owner) — the renewal contract
|
|
// every Leases must honor.
|
|
func TestStaticFencerIsDeterministic(t *testing.T) {
|
|
f := StaticLeases("solo")
|
|
a, _ := f.Acquire(context.Background(), "acme")
|
|
b, _ := f.Acquire(context.Background(), "acme")
|
|
if a != b {
|
|
t.Fatalf("Acquire not stable for a fixed owner: %+v vs %+v", a, b)
|
|
}
|
|
}
|
|
|
|
// TestLeaseRoundIsTheFencingBoundary documents the invariant the whole design
|
|
// leans on: a takeover lease STRICTLY outranks the prior holder's, so the store
|
|
// can fence the old writer purely by comparing rounds — the store needs to know
|
|
// nothing about members or elections, only that a lower round is stale.
|
|
func TestLeaseRoundIsTheFencingBoundary(t *testing.T) {
|
|
prior := Lease{Key: "acme", Owner: Member{ID: "old"}, Round: 4}
|
|
takeover := Lease{Key: "acme", Owner: Member{ID: "new"}, Round: 5}
|
|
if takeover.Round <= prior.Round {
|
|
t.Fatalf("takeover round %d must strictly exceed prior round %d", takeover.Round, prior.Round)
|
|
}
|
|
// The elected owner and the fenced round are independent axes: the SAME key
|
|
// can change owner while the round advances, and the value carries both.
|
|
if takeover.Key != prior.Key {
|
|
t.Fatal("a lease handoff keeps the key and changes owner+round")
|
|
}
|
|
}
|
|
|
|
// staticLeases must satisfy the Leases seam — the compile-time proof the value
|
|
// this package ships is usable wherever a Leases is required.
|
|
var _ Leases = StaticLeases("self")
|