Skip to content

Commit 8cab32c

Browse files
fix(addressbook): lock remove (#5573)
1 parent caa01b6 commit 8cab32c

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

pkg/addressbook/addressbook.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,9 @@ type store struct {
9696
store storage.StateStorer
9797
now func() time.Time
9898

99-
// mu serializes the read-modify-write in Seen against Put, so that a
100-
// concurrent Put is not rolled back by a stale copy of the entry.
99+
// mu serializes the read-modify-write in Seen against Put and Remove, so
100+
// that a concurrent Put is not rolled back and a concurrent Remove is not
101+
// undone by a stale copy of the entry.
101102
mu sync.Mutex
102103
}
103104

@@ -184,6 +185,9 @@ func (s *store) Seen(overlays ...swarm.Address) error {
184185
}
185186

186187
func (s *store) Remove(overlay swarm.Address) error {
188+
s.mu.Lock()
189+
defer s.mu.Unlock()
190+
187191
return s.store.Delete(keyPrefix + overlay.String())
188192
}
189193

pkg/addressbook/addressbook_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,55 @@ func TestSeenKeepsConcurrentPut(t *testing.T) {
253253
}
254254
}
255255

256+
// TestSeenRaceWithRemove pins Seen's read-modify-write against a concurrent
257+
// Remove of the same overlay. Remove takes the same lock as Put and Seen, so
258+
// it cannot run while Seen holds the entry it has just read: the removal
259+
// always wins over Seen's stale write-back.
260+
func TestSeenRaceWithRemove(t *testing.T) {
261+
t.Parallel()
262+
263+
now := time.Unix(1_000_000, 0)
264+
hooked := &hookStore{StateStorer: mock.NewStateStore()}
265+
book := addressbook.NewWithClock(hooked, func() time.Time { return now })
266+
267+
overlay := swarm.NewAddress([]byte{0, 1, 2, 3})
268+
addr := newTestAddr(t, overlay)
269+
270+
if err := book.Put(overlay, addr, true); err != nil {
271+
t.Fatal(err)
272+
}
273+
274+
// move past the throttle window, so that Seen takes its write path.
275+
now = now.Add(25 * time.Hour)
276+
277+
// While Seen holds the entry it has just read, kademlia decides the peer is
278+
// bad (too many failed connection attempts, overlay mismatch, light node)
279+
// and removes it.
280+
started, finished := make(chan struct{}), make(chan struct{})
281+
hooked.onGet = func() {
282+
go func() {
283+
defer close(finished)
284+
close(started)
285+
if err := book.Remove(overlay); err != nil {
286+
t.Error(err)
287+
}
288+
}()
289+
<-started
290+
// Give Remove a chance to run before Seen writes back. Serialized on
291+
// the addressbook lock, it blocks here until Seen returns.
292+
time.Sleep(100 * time.Millisecond)
293+
}
294+
295+
if err := book.Seen(overlay); err != nil {
296+
t.Fatal(err)
297+
}
298+
<-finished
299+
300+
if _, _, err := book.Get(overlay); !errors.Is(err, addressbook.ErrNotFound) {
301+
t.Fatalf("concurrent Remove was undone by Seen's stale write-back: err=%v", err)
302+
}
303+
}
304+
256305
// hookStore fires onGet once, immediately after a Get returns, to interleave a
257306
// concurrent writer inside Seen's read-modify-write.
258307
type hookStore struct {

0 commit comments

Comments
 (0)