fix(zfc): NamePool.InUse is transient, not persisted (hq-lng09)
ZFC violation: InUse was being persisted to JSON and loaded from disk, but Reconcile() immediately overwrites it with filesystem-derived state. Changes: - Mark InUse with json:"-" to exclude from serialization - Load() now initializes InUse as empty (derived via Reconcile) - Updated test to verify OverflowNext persists but InUse does not Per ZFC "Discover, Don't Track", InUse should always be derived from existing polecat directories, not tracked as separate state. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,7 +84,9 @@ type NamePool struct {
|
|||||||
|
|
||||||
// InUse tracks which pool names are currently in use.
|
// InUse tracks which pool names are currently in use.
|
||||||
// Key is the name itself, value is true if in use.
|
// Key is the name itself, value is true if in use.
|
||||||
InUse map[string]bool `json:"in_use"`
|
// ZFC: This is transient state derived from filesystem via Reconcile().
|
||||||
|
// Never persist - always discover from existing polecat directories.
|
||||||
|
InUse map[string]bool `json:"-"`
|
||||||
|
|
||||||
// OverflowNext is the next overflow sequence number.
|
// OverflowNext is the next overflow sequence number.
|
||||||
// Starts at MaxSize+1 and increments.
|
// Starts at MaxSize+1 and increments.
|
||||||
@@ -168,12 +170,12 @@ func (p *NamePool) Load() error {
|
|||||||
|
|
||||||
// Note: Theme and CustomNames are NOT loaded from state file.
|
// Note: Theme and CustomNames are NOT loaded from state file.
|
||||||
// They are configuration (from settings/config.json), not runtime state.
|
// They are configuration (from settings/config.json), not runtime state.
|
||||||
// The state file only persists InUse, OverflowNext, and MaxSize.
|
// The state file only persists OverflowNext and MaxSize.
|
||||||
|
//
|
||||||
|
// ZFC: InUse is NEVER loaded from disk - it's transient state derived
|
||||||
|
// from filesystem via Reconcile(). Always start with empty map.
|
||||||
|
p.InUse = make(map[string]bool)
|
||||||
|
|
||||||
p.InUse = loaded.InUse
|
|
||||||
if p.InUse == nil {
|
|
||||||
p.InUse = make(map[string]bool)
|
|
||||||
}
|
|
||||||
p.OverflowNext = loaded.OverflowNext
|
p.OverflowNext = loaded.OverflowNext
|
||||||
if p.OverflowNext < p.MaxSize+1 {
|
if p.OverflowNext < p.MaxSize+1 {
|
||||||
p.OverflowNext = p.MaxSize + 1
|
p.OverflowNext = p.MaxSize + 1
|
||||||
|
|||||||
@@ -160,13 +160,18 @@ func TestNamePool_SaveLoad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
// Use config to set MaxSize from the start (affects OverflowNext initialization)
|
||||||
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, 3)
|
||||||
|
|
||||||
// Allocate some names
|
// Exhaust the pool to trigger overflow, which increments OverflowNext
|
||||||
pool.Allocate() // furiosa
|
pool.Allocate() // furiosa
|
||||||
pool.Allocate() // nux
|
pool.Allocate() // nux
|
||||||
pool.Allocate() // slit
|
pool.Allocate() // slit
|
||||||
pool.Release("nux")
|
overflowName, _ := pool.Allocate() // testrig-4 (overflow)
|
||||||
|
|
||||||
|
if overflowName != "testrig-4" {
|
||||||
|
t.Errorf("expected testrig-4 for first overflow, got %s", overflowName)
|
||||||
|
}
|
||||||
|
|
||||||
// Save state
|
// Save state
|
||||||
if err := pool.Save(); err != nil {
|
if err := pool.Save(); err != nil {
|
||||||
@@ -174,20 +179,26 @@ func TestNamePool_SaveLoad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new pool and load
|
// Create new pool and load
|
||||||
pool2 := NewNamePool(tmpDir, "testrig")
|
pool2 := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, 3)
|
||||||
if err := pool2.Load(); err != nil {
|
if err := pool2.Load(); err != nil {
|
||||||
t.Fatalf("Load error: %v", err)
|
t.Fatalf("Load error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should have furiosa and slit in use
|
// ZFC: InUse is NOT persisted - it's transient state derived from filesystem.
|
||||||
if pool2.ActiveCount() != 2 {
|
// After Load(), InUse should be empty (0 active).
|
||||||
t.Errorf("expected 2 active, got %d", pool2.ActiveCount())
|
if pool2.ActiveCount() != 0 {
|
||||||
|
t.Errorf("expected 0 active after Load (ZFC: InUse is transient), got %d", pool2.ActiveCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next allocation should be nux (released slot)
|
// OverflowNext SHOULD persist - it's the one piece of state that can't be derived.
|
||||||
name, _ := pool2.Allocate()
|
// Next overflow should be testrig-5, not testrig-4.
|
||||||
if name != "nux" {
|
pool2.Allocate() // furiosa (InUse empty, so starts from beginning)
|
||||||
t.Errorf("expected nux, got %s", name)
|
pool2.Allocate() // nux
|
||||||
|
pool2.Allocate() // slit
|
||||||
|
overflowName2, _ := pool2.Allocate() // Should be testrig-5
|
||||||
|
|
||||||
|
if overflowName2 != "testrig-5" {
|
||||||
|
t.Errorf("expected testrig-5 (OverflowNext persisted), got %s", overflowName2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user