state: test that a persist racing a delete cannot resurrect the node

DeleteNode's early persistMu unlock is safe only while node persists
stay UPDATE-only; say so where the lock is declared.
This commit is contained in:
Kristoffer Dalby committed 2026-11-02 17:46:53 +00:00
1 parent ab11fcbf32
commit 598b4c53c3
2 files changed
+64

No files matched your search

+60
View File
@@ -621,6 +621,66 @@ func TestDeleteNodeTwiceReleasesAddressOnce(t *testing.T) {
require.ErrorIs(t, err, db.ErrCouldNotAllocateIP)
}
func TestPersistDuringDeleteDoesNotResurrectNode(t *testing.T) {
_, s, nodeID := persistTestSetup(t)
t.Cleanup(func() { _ = s.Close() })
node, ok := s.GetNodeByID(nodeID)
require.True(t, ok)
s.nodeStore.Stop()
entered, release := make(chan struct{}), make(chan struct{})
var enteredOnce sync.Once
s.nodeStore = NewNodeStore(
types.Nodes{node.AsStruct()},
func(nodes []types.NodeView) map[types.NodeID][]types.NodeID {
if len(nodes) == 0 {
enteredOnce.Do(func() { close(entered) })
<-release
}
return nil
},
TestBatchSize,
TestBatchTimeout,
)
s.nodeStore.Start()
var releaseOnce sync.Once
t.Cleanup(func() { releaseOnce.Do(func() { close(release) }) })
deleteDone := make(chan error, 1)
go func() {
_, err := s.DeleteNode(node)
deleteDone <- err
}()
<-entered
persistDone := make(chan error, 1)
go func() {
_, err := s.persistNode(node)
persistDone <- err
}()
select {
case err := <-persistDone:
require.NoError(t, err)
case <-time.After(time.Second):
t.Error("persist blocked behind the NodeStore delete")
}
_, err := s.db.GetNodeByID(nodeID)
require.ErrorIs(t, err, gorm.ErrRecordNotFound)
releaseOnce.Do(func() { close(release) })
require.NoError(t, <-deleteDone)
}
func TestDeleteNodeKeepsStoreOnDBFailure(t *testing.T) {
_, s, nodeID := persistTestSetup(t)
t.Cleanup(func() { _ = s.Close() })
+4
View File
@@ -236,6 +236,10 @@ type State struct {
// persistMu serialises node-row persistence and deletion so the database
// always converges on [NodeStore] rather than being clobbered by a stale
// caller snapshot or resurrected by an update racing with deletion.
// Every node's persist takes it, so paths a node can trigger must not
// hold it across a NodeStore write. Node persists must stay UPDATE-only;
// then a persist between the row delete and the NodeStore delete changes
// nothing.
persistMu sync.Mutex
// selfRefresh holds nodes whose own DNS config inputs changed (NextDNS