mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-10 08:40:07 +09:00
state: serialise IP backfill with node persistence
A persist between the backfill's database write and its NodeStore reload wrote the old addresses back, silently undoing the backfill.
This commit is contained in:
committed by
Kristoffer Dalby
parent
84cf38ce24
commit
f7c8b4d93f
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -1090,3 +1091,61 @@ func TestNodeWriteChangeWhenPolicyRefreshFails(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBackfillNodeIPsSurvivesConcurrentPersist guards the backfill against a
|
||||||
|
// node persist landing between its database write and the NodeStore reload:
|
||||||
|
// that persist writes the old addresses back and the backfill is lost.
|
||||||
|
func TestBackfillNodeIPsSurvivesConcurrentPersist(t *testing.T) {
|
||||||
|
dbPath, s, nodeID := persistTestSetup(t)
|
||||||
|
require.NoError(t, s.Close())
|
||||||
|
|
||||||
|
// Dropping the IPv6 prefix gives the backfill work to do.
|
||||||
|
cfg := persistTestConfig(dbPath)
|
||||||
|
cfg.PrefixV6 = nil
|
||||||
|
|
||||||
|
s, err := NewState(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = s.Close() })
|
||||||
|
|
||||||
|
nv, ok := s.GetNodeByID(nodeID)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.True(t, nv.IPv6().Valid(), "precondition: node has an IPv6 address")
|
||||||
|
|
||||||
|
// The backfill reads the nodes table twice: inside its write, then to
|
||||||
|
// reload NodeStore. Persist the node just before the reload.
|
||||||
|
var reads atomic.Int32
|
||||||
|
|
||||||
|
persisted := make(chan struct{})
|
||||||
|
|
||||||
|
require.NoError(t, s.db.DB.Callback().Query().Before("gorm:query").
|
||||||
|
Register("persist_before_reload", func(tx *gorm.DB) {
|
||||||
|
if tx.Statement.Table != "nodes" || reads.Add(1) != 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, _ = s.persistNode(nv)
|
||||||
|
|
||||||
|
close(persisted)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Give an unserialised persist time to land before the reload.
|
||||||
|
select {
|
||||||
|
case <-persisted:
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.Cleanup(func() { _ = s.db.DB.Callback().Query().Remove("persist_before_reload") })
|
||||||
|
|
||||||
|
_, _, err = s.BackfillNodeIPs()
|
||||||
|
require.NoError(t, err)
|
||||||
|
<-persisted
|
||||||
|
|
||||||
|
nv, ok = s.GetNodeByID(nodeID)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.False(t, nv.IPv6().Valid(), "NodeStore kept the removed IPv6 address")
|
||||||
|
|
||||||
|
dbNode, err := s.db.GetNodeByID(nodeID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, dbNode.IPv6, "database kept the removed IPv6 address")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1142,8 +1142,14 @@ func (s *State) RenameNode(nodeID types.NodeID, newName string) (types.NodeView,
|
|||||||
func (s *State) BackfillNodeIPs() ([]string, []change.Change, error) {
|
func (s *State) BackfillNodeIPs() ([]string, []change.Change, error) {
|
||||||
genBefore := s.polMan.NodesGeneration()
|
genBefore := s.polMan.NodesGeneration()
|
||||||
|
|
||||||
|
// Hold persistMu until NodeStore has the new addresses, or a concurrent
|
||||||
|
// persist writes the old ones back from NodeStore in between.
|
||||||
|
s.persistMu.Lock()
|
||||||
|
|
||||||
changes, err := s.db.BackfillNodeIPs(s.ipAlloc)
|
changes, err := s.db.BackfillNodeIPs(s.ipAlloc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.persistMu.Unlock()
|
||||||
|
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1154,6 +1160,8 @@ func (s *State) BackfillNodeIPs() ([]string, []change.Change, error) {
|
|||||||
if len(changes) > 0 {
|
if len(changes) > 0 {
|
||||||
nodes, err := s.db.ListNodes()
|
nodes, err := s.db.ListNodes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.persistMu.Unlock()
|
||||||
|
|
||||||
return changes, nil, fmt.Errorf("refreshing NodeStore after IP backfill: %w", err)
|
return changes, nil, fmt.Errorf("refreshing NodeStore after IP backfill: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1173,6 +1181,8 @@ func (s *State) BackfillNodeIPs() ([]string, []change.Change, error) {
|
|||||||
s.nodeStore.UpdateNodes(updates)
|
s.nodeStore.UpdateNodes(updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.persistMu.Unlock()
|
||||||
|
|
||||||
// IPs are policy inputs: without this, clients only learned the new
|
// IPs are policy inputs: without this, clients only learned the new
|
||||||
// addresses from whichever unrelated write next refreshed the policy.
|
// addresses from whichever unrelated write next refreshed the policy.
|
||||||
c, err := s.updatePolicyManagerNodes(genBefore)
|
c, err := s.updatePolicyManagerNodes(genBefore)
|
||||||
|
|||||||
Reference in New Issue
Block a user