update
This commit is contained in:
@@ -45,7 +45,7 @@ func newMockHost(name string) *mockHost {
|
||||
return &mockHost{name: name, healthy: true}
|
||||
}
|
||||
|
||||
func (m *mockHost) AnnounceExpiry(ids []uint64) {
|
||||
func (m *mockHost) AnnounceExpiry(ids []uint64, _ []int64) {
|
||||
m.mu.Lock()
|
||||
m.expiry = append(m.expiry, ids...)
|
||||
m.mu.Unlock()
|
||||
@@ -84,7 +84,7 @@ func (m *mockHost) Ping() bool { return m.healthy }
|
||||
|
||||
func (m *mockHost) IsHealthy() bool { return m.healthy }
|
||||
|
||||
func (m *mockHost) AnnounceOwnership(_ string, ids []uint64) {
|
||||
func (m *mockHost) AnnounceOwnership(_ string, ids []uint64, _ []int64) {
|
||||
m.mu.Lock()
|
||||
m.ownership = append(m.ownership, ids...)
|
||||
m.mu.Unlock()
|
||||
@@ -247,11 +247,19 @@ func TestSimpleGrainPoolHandleOwnershipChange(t *testing.T) {
|
||||
host := newMockHost("peer-d")
|
||||
pool := newTestPool(t, func(string) (Host[testState], error) { return host, nil })
|
||||
|
||||
// First-spawn-wins arbitration: a remote broadcast wins only when its
|
||||
// lastChange stamp is strictly earlier than the local grain's stamp
|
||||
// (or the local grain is absent). Capture the local spawn time once
|
||||
// and pass a stamp 1ns earlier to the remote so the test continues to
|
||||
// assert "external owner takes over given an earlier remote spawn"
|
||||
// under the new arbitration rule.
|
||||
spawnTime := time.Now()
|
||||
pool.localMu.Lock()
|
||||
pool.grains[7] = &testGrain{id: 7, accessed: time.Now()}
|
||||
pool.grains[7] = &testGrain{id: 7, accessed: spawnTime}
|
||||
pool.localMu.Unlock()
|
||||
|
||||
if err := pool.HandleOwnershipChange(host.name, []uint64{7}); err != nil {
|
||||
remoteStamp := spawnTime.UnixNano() - 1
|
||||
if err := pool.HandleOwnershipChange(host.name, []uint64{7}, []int64{remoteStamp}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -268,6 +276,121 @@ func TestSimpleGrainPoolHandleOwnershipChange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSimpleGrainPoolHandleOwnershipChangeArbitration verifies the
|
||||
// first-spawn-wins arbitration rules for HandleOwnershipChange:
|
||||
//
|
||||
// - Remote with earlier lastChange than local → accept remote.
|
||||
// - Remote with later lastChange than local → keep local.
|
||||
// - Equal lastChange → lower hostname wins.
|
||||
// - Remote stamp == -1 (legacy path) → accept remote (pre-arbitration).
|
||||
// - Local grain absent → accept remote.
|
||||
func TestSimpleGrainPoolHandleOwnershipChangeArbitration(t *testing.T) {
|
||||
host := newMockHost("peer-arbitrate")
|
||||
// Each table case constructs a fresh pool inside t.Run — we don't
|
||||
// share state across cases because per-case hostname differs
|
||||
// (hostname is the tie-break oracle on equal lastChange stamps).
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
name_local string // pool hostname for ordering check
|
||||
id uint64
|
||||
localStamp int64 // UnixNano of the local grain; -1 = no local grain
|
||||
remoteStamp int64 // UnixNano stamped on the broadcast
|
||||
expectLocal bool // true → local grain still resident
|
||||
expectRemote bool // true → remoteOwners[id] = host
|
||||
}{
|
||||
{
|
||||
name: "remote older stamp wins",
|
||||
name_local: "zzz", // pool above "peer-arbitrate" lex
|
||||
id: 10,
|
||||
localStamp: now + 100,
|
||||
remoteStamp: now,
|
||||
expectLocal: false,
|
||||
expectRemote: true,
|
||||
},
|
||||
{
|
||||
name: "local earlier stamp keeps",
|
||||
name_local: "aaa", // pool below "peer-arbitrate" lex but stamp decides
|
||||
id: 11,
|
||||
localStamp: now,
|
||||
remoteStamp: now + 100,
|
||||
expectLocal: true,
|
||||
expectRemote: false,
|
||||
},
|
||||
{
|
||||
name: "equal stamp + lower local hostname keeps",
|
||||
name_local: "a-peer", // below host
|
||||
id: 12,
|
||||
localStamp: now,
|
||||
remoteStamp: now,
|
||||
expectLocal: true,
|
||||
expectRemote: false,
|
||||
},
|
||||
{
|
||||
name: "equal stamp + higher local hostname defers",
|
||||
name_local: "zzz-peer", // above host
|
||||
id: 13,
|
||||
localStamp: now,
|
||||
remoteStamp: now,
|
||||
expectLocal: false,
|
||||
expectRemote: true,
|
||||
},
|
||||
{
|
||||
name: "legacy (no remote stamp) accepts remote",
|
||||
name_local: "zzz", // we'd otherwise keep local
|
||||
id: 14,
|
||||
localStamp: now + 100,
|
||||
remoteStamp: -1,
|
||||
expectLocal: false,
|
||||
expectRemote: true,
|
||||
},
|
||||
{
|
||||
name: "no local grain accepts remote",
|
||||
name_local: "aaa",
|
||||
id: 15,
|
||||
localStamp: -1,
|
||||
remoteStamp: now,
|
||||
expectLocal: false,
|
||||
expectRemote: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Fresh pool per case so hostname differences don't leak.
|
||||
p := newTestPool(t, func(string) (Host[testState], error) { return host, nil })
|
||||
p.hostname = tc.name_local
|
||||
|
||||
p.localMu.Lock()
|
||||
if tc.localStamp != -1 {
|
||||
p.grains[tc.id] = &testGrain{id: tc.id, accessed: time.Unix(0, tc.localStamp)}
|
||||
}
|
||||
p.localMu.Unlock()
|
||||
|
||||
if err := p.HandleOwnershipChange(host.name, []uint64{tc.id}, []int64{tc.remoteStamp}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.localMu.RLock()
|
||||
_, hasLocal := p.grains[tc.id]
|
||||
p.localMu.RUnlock()
|
||||
if hasLocal != tc.expectLocal {
|
||||
t.Fatalf("local grain presence = %v, want %v", hasLocal, tc.expectLocal)
|
||||
}
|
||||
|
||||
owner, ok := p.OwnerHost(tc.id)
|
||||
if ok != tc.expectRemote {
|
||||
t.Fatalf("OwnerHost ok = %v, want %v", ok, tc.expectRemote)
|
||||
}
|
||||
if tc.expectRemote && (owner == nil || owner.Name() != host.name) {
|
||||
t.Fatalf("Owner = %v, want host %q", owner, host.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleGrainPoolPurgeEvictsStaleGrains(t *testing.T) {
|
||||
pool := newTestPool(t, func(string) (Host[testState], error) {
|
||||
return nil, fmt.Errorf("no remotes")
|
||||
|
||||
Reference in New Issue
Block a user