update
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s

This commit is contained in:
2026-07-03 17:45:51 +02:00
parent f339b60351
commit cecf4abe76
12 changed files with 1818 additions and 112 deletions
+127 -4
View File
@@ -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")