fix(discov): prevent unbounded memory growth on duplicate etcd PUT events (#5580)

This commit is contained in:
Kevin Wan
2026-05-16 12:35:05 +08:00
committed by GitHub
parent 4ad4fd43b7
commit 7b5e7b1c26
4 changed files with 148 additions and 8 deletions

View File

@@ -263,14 +263,24 @@ func (c *cluster) handleWatchEvents(ctx context.Context, key watchKey, events []
for _, ev := range events {
switch ev.Type {
case clientv3.EventTypePut:
evKey := string(ev.Kv.Key)
evVal := string(ev.Kv.Value)
c.lock.Lock()
watcher.values[string(ev.Kv.Key)] = string(ev.Kv.Value)
oldVal, exists := watcher.values[evKey]
watcher.values[evKey] = evVal
c.lock.Unlock()
if exists && oldVal == evVal {
// duplicate PUT with same value, skip to prevent unbounded growth
continue
}
if exists {
// key moved to a new value, notify delete of old entry first
for _, l := range listeners {
l.OnDelete(KV{Key: evKey, Val: oldVal})
}
}
for _, l := range listeners {
l.OnAdd(KV{
Key: string(ev.Kv.Key),
Val: string(ev.Kv.Value),
})
l.OnAdd(KV{Key: evKey, Val: evVal})
}
case clientv3.EventTypeDelete:
c.lock.Lock()
@@ -433,7 +443,7 @@ func (c *cluster) setupWatch(cli EtcdClient, key watchKey, rev int64) (context.C
}
ctx, cancel := context.WithCancel(cli.Ctx())
c.lock.Lock()
if watcher, ok := c.watchers[key]; ok {
watcher.cancel = cancel

View File

@@ -517,7 +517,7 @@ func TestCluster_ConcurrentMonitor(t *testing.T) {
go func() {
defer wg.Done()
key := keys[idx%len(keys)]
if idx%2 == 0 {
// Half the goroutines add listeners (write operation)
c.addListener(key, &mockListener{})
@@ -543,6 +543,50 @@ func TestCluster_ConcurrentMonitor(t *testing.T) {
close(c.done)
}
func TestCluster_handleWatchEvents_DuplicatePut(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
listener := NewMockUpdateListener(ctrl)
// OnAdd must be called exactly once despite two PUT events with the same key+value.
listener.EXPECT().OnAdd(KV{Key: "hello", Val: "world"}).Times(1)
c := newCluster([]string{"any"})
key := watchKey{key: "any"}
c.watchers[key] = &watchValue{
listeners: []UpdateListener{listener},
values: make(map[string]string),
}
events := []*clientv3.Event{
{Type: clientv3.EventTypePut, Kv: &mvccpb.KeyValue{Key: []byte("hello"), Value: []byte("world")}},
{Type: clientv3.EventTypePut, Kv: &mvccpb.KeyValue{Key: []byte("hello"), Value: []byte("world")}},
}
c.handleWatchEvents(context.Background(), key, events)
}
func TestCluster_handleWatchEvents_ValueChange(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
listener := NewMockUpdateListener(ctrl)
gomock.InOrder(
listener.EXPECT().OnAdd(KV{Key: "hello", Val: "world1"}),
listener.EXPECT().OnDelete(KV{Key: "hello", Val: "world1"}),
listener.EXPECT().OnAdd(KV{Key: "hello", Val: "world2"}),
)
c := newCluster([]string{"any"})
key := watchKey{key: "any"}
c.watchers[key] = &watchValue{
listeners: []UpdateListener{listener},
values: make(map[string]string),
}
c.handleWatchEvents(context.Background(), key, []*clientv3.Event{
{Type: clientv3.EventTypePut, Kv: &mvccpb.KeyValue{Key: []byte("hello"), Value: []byte("world1")}},
})
c.handleWatchEvents(context.Background(), key, []*clientv3.Event{
{Type: clientv3.EventTypePut, Kv: &mvccpb.KeyValue{Key: []byte("hello"), Value: []byte("world2")}},
})
}
type mockListener struct {
}