Skip to content

Commit 2f7821f

Browse files
committed
fix(zookeeper): restore and clean up watches on reconnect
1 parent b4d9662 commit 2f7821f

6 files changed

Lines changed: 241 additions & 41 deletions

File tree

config_center/zookeeper/config_cache.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
import (
2626
"github.com/dubbogo/go-zookeeper/zk"
27+
2728
"github.com/hashicorp/golang-lru"
2829
)
2930

@@ -90,7 +91,11 @@ func (c *configCache) enabled() bool {
9091
return c.ttl > 0
9192
}
9293

93-
func (c *configCache) load(path string, loader func(*zk.Watcher, bool) (configCacheEntry, *zk.Watcher, error)) (configCacheEntry, error) {
94+
func (c *configCache) load(
95+
path string,
96+
loader func(*zk.Watcher, bool) (configCacheEntry, *zk.Watcher, error),
97+
removeWatcher func(*zk.Watcher),
98+
) (configCacheEntry, error) {
9499
if !c.enabled() {
95100
entry, _, err := loader(nil, false)
96101
return entry, err
@@ -119,11 +124,17 @@ func (c *configCache) load(path string, loader func(*zk.Watcher, bool) (configCa
119124
}
120125
if err != nil {
121126
if !c.storeWatchState(path, generation, nextWatchState) {
127+
if registerWatch {
128+
removeRegisteredWatcher(removeWatcher, watcher)
129+
}
122130
continue
123131
}
124132
return configCacheEntry{}, err
125133
}
126134
if !c.storeLoad(path, generation, entry, nextWatchState) {
135+
if registerWatch {
136+
removeRegisteredWatcher(removeWatcher, watcher)
137+
}
127138
continue
128139
}
129140
return entry, nil
@@ -278,7 +289,11 @@ func (c *configCache) setWatchStateLocked(path string, watchState configWatchSta
278289
return true
279290
}
280291

281-
func (c *configCache) ensureBusinessWatch(path string, register func() (*zk.Watcher, error)) error {
292+
func (c *configCache) ensureBusinessWatch(
293+
path string,
294+
register func() (*zk.Watcher, error),
295+
removeWatcher func(*zk.Watcher),
296+
) error {
282297
if !c.enabled() {
283298
_, err := register()
284299
return err
@@ -311,6 +326,7 @@ func (c *configCache) ensureBusinessWatch(path string, register func() (*zk.Watc
311326
return nil
312327
}
313328
c.stateLock.Unlock()
329+
removeRegisteredWatcher(removeWatcher, watcher)
314330
}
315331
}
316332

@@ -383,14 +399,28 @@ func (c *configCache) cancelWatchRenewal(path string, generation uint64) {
383399
}
384400
}
385401

386-
func (c *configCache) reset() {
402+
func (c *configCache) reset() []*zk.Watcher {
387403
c.stateLock.Lock()
388404
defer c.stateLock.Unlock()
405+
406+
watchers := make([]*zk.Watcher, 0, len(c.watches))
407+
for _, watchState := range c.watches {
408+
if watchState.watcher != nil {
409+
watchers = append(watchers, watchState.watcher)
410+
}
411+
}
389412
c.generation++
390413
c.entries.Purge()
391414
c.watches = make(map[string]configWatchState)
392415
c.autoWatchCount = 0
393416
c.autoWatchReservations = 0
417+
return watchers
418+
}
419+
420+
func removeRegisteredWatcher(removeWatcher func(*zk.Watcher), watcher *zk.Watcher) {
421+
if watcher != nil && removeWatcher != nil {
422+
removeWatcher(watcher)
423+
}
394424
}
395425

396426
func (c *configCache) pathLock(path string) *sync.Mutex {

config_center/zookeeper/config_cache_test.go

Lines changed: 82 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
import (
2929
"github.com/dubbogo/go-zookeeper/zk"
30+
3031
"github.com/stretchr/testify/require"
3132
)
3233

@@ -38,15 +39,15 @@ func TestConfigCacheLoadAndExpiry(t *testing.T) {
3839
return configCacheEntry{content: string(rune('0' + count)), exists: true}, nil, nil
3940
}
4041

41-
first, err := cache.load("/path", loader)
42+
first, err := cache.load("/path", loader, nil)
4243
require.NoError(t, err)
43-
second, err := cache.load("/path", loader)
44+
second, err := cache.load("/path", loader, nil)
4445
require.NoError(t, err)
4546
require.Equal(t, first.content, second.content)
4647
require.Equal(t, int32(1), loads.Load())
4748

4849
require.Eventually(t, func() bool {
49-
entry, loadErr := cache.load("/path", loader)
50+
entry, loadErr := cache.load("/path", loader, nil)
5051
return loadErr == nil && entry.content == "2"
5152
}, time.Second, 5*time.Millisecond)
5253
require.Equal(t, int32(2), loads.Load())
@@ -57,19 +58,18 @@ func TestConfigCacheUsesFixedPathLockShards(t *testing.T) {
5758
locks := make(map[*sync.Mutex]struct{})
5859
pathLock := cache.pathLock("/path")
5960

60-
for i := 0; i < 4096; i++ {
61+
for i := range 4096 {
6162
locks[cache.pathLock(fmt.Sprintf("/path/%d", i))] = struct{}{}
6263
}
6364

64-
require.Equal(t, pathLockShardCount, len(cache.pathLocks))
6565
require.Same(t, pathLock, cache.pathLock("/path"))
6666
require.LessOrEqual(t, len(locks), pathLockShardCount)
6767
}
6868

6969
func TestConfigCacheBoundsEntriesUnderKeyChurn(t *testing.T) {
7070
cache := newConfigCache(time.Minute)
7171

72-
for i := 0; i < 4096; i++ {
72+
for i := range 4096 {
7373
cache.store(fmt.Sprintf("/path/%d", i), configCacheEntry{
7474
content: fmt.Sprintf("value-%d", i),
7575
exists: true,
@@ -91,7 +91,7 @@ func TestConfigCacheStoresMissingEntry(t *testing.T) {
9191

9292
func TestConfigCacheEvictsLeastRecentlyUsed(t *testing.T) {
9393
cache := newConfigCache(time.Minute)
94-
for i := 0; i < maxCacheEntries; i++ {
94+
for i := range maxCacheEntries {
9595
cache.store(fmt.Sprintf("/path/%d", i), configCacheEntry{exists: true})
9696
}
9797

@@ -122,16 +122,14 @@ func TestConfigCacheConcurrentLoadsRemainBounded(t *testing.T) {
122122
var wg sync.WaitGroup
123123
errs := make(chan error, 4096)
124124

125-
for i := 0; i < 4096; i++ {
125+
for i := range 4096 {
126126
path := fmt.Sprintf("/path/%d", i)
127-
wg.Add(1)
128-
go func() {
129-
defer wg.Done()
127+
wg.Go(func() {
130128
_, err := cache.load(path, func(*zk.Watcher, bool) (configCacheEntry, *zk.Watcher, error) {
131129
return configCacheEntry{exists: true}, nil, nil
132-
})
130+
}, nil)
133131
errs <- err
134-
}()
132+
})
135133
}
136134
wg.Wait()
137135
close(errs)
@@ -148,20 +146,18 @@ func TestConfigCacheConcurrentAutoWatchRegistrationsRemainBounded(t *testing.T)
148146
var wg sync.WaitGroup
149147
errs := make(chan error, 4096)
150148

151-
for i := 0; i < 4096; i++ {
149+
for i := range 4096 {
152150
path := fmt.Sprintf("/watch/%d", i)
153-
wg.Add(1)
154-
go func() {
155-
defer wg.Done()
151+
wg.Go(func() {
156152
_, err := cache.load(path, func(_ *zk.Watcher, registerWatch bool) (configCacheEntry, *zk.Watcher, error) {
157153
if registerWatch {
158154
registrations.Add(1)
159155
return configCacheEntry{exists: true}, &zk.Watcher{}, nil
160156
}
161157
return configCacheEntry{exists: true}, nil, nil
162-
})
158+
}, nil)
163159
errs <- err
164-
}()
160+
})
165161
}
166162
wg.Wait()
167163
close(errs)
@@ -185,7 +181,7 @@ func TestConfigCacheWatchUpdateWinsOverLoad(t *testing.T) {
185181
close(loadStarted)
186182
<-releaseLoad
187183
return configCacheEntry{content: "old", exists: true}, watcher, nil
188-
})
184+
}, nil)
189185
}()
190186

191187
<-loadStarted
@@ -205,35 +201,93 @@ func TestConfigCacheWatchUpdateWinsOverLoad(t *testing.T) {
205201

206202
func TestConfigCacheResetDiscardsInFlightLoad(t *testing.T) {
207203
cache := newConfigCache(time.Minute)
208-
cache.setWatch("/path", configWatchState{watcher: &zk.Watcher{}, auto: true})
209204
loadStarted := make(chan struct{})
210205
releaseLoad := make(chan struct{})
211-
result := make(chan configCacheEntry, 1)
206+
removed := make(chan *zk.Watcher, 1)
207+
type loadResult struct {
208+
entry configCacheEntry
209+
err error
210+
}
211+
result := make(chan loadResult, 1)
212212
var loads atomic.Int32
213+
staleWatcher := &zk.Watcher{}
214+
currentWatcher := &zk.Watcher{}
213215

214216
go func() {
215-
entry, _ := cache.load("/path", func(watcher *zk.Watcher, _ bool) (configCacheEntry, *zk.Watcher, error) {
217+
entry, err := cache.load("/path", func(_ *zk.Watcher, _ bool) (configCacheEntry, *zk.Watcher, error) {
216218
if loads.Add(1) == 1 {
217219
close(loadStarted)
218220
<-releaseLoad
219-
return configCacheEntry{content: "old", exists: true}, watcher, nil
221+
return configCacheEntry{content: "old", exists: true}, staleWatcher, nil
220222
}
221-
return configCacheEntry{content: "new", exists: true}, watcher, nil
223+
return configCacheEntry{content: "new", exists: true}, currentWatcher, nil
224+
}, func(watcher *zk.Watcher) {
225+
removed <- watcher
222226
})
223-
result <- entry
227+
result <- loadResult{entry: entry, err: err}
224228
}()
225229

226230
<-loadStarted
227-
cache.reset()
231+
require.Empty(t, cache.reset())
228232
_, ok := cache.getFresh("/path")
229233
require.False(t, ok)
230234
_, watchState := cache.snapshot("/path")
231235
require.Nil(t, watchState.watcher)
232236
close(releaseLoad)
233237

234-
require.Equal(t, "new", (<-result).content)
238+
load := <-result
239+
require.NoError(t, load.err)
240+
require.Equal(t, "new", load.entry.content)
235241
require.Equal(t, int32(2), loads.Load())
242+
select {
243+
case watcher := <-removed:
244+
require.Same(t, staleWatcher, watcher)
245+
case <-time.After(time.Second):
246+
t.Fatal("stale watcher was not removed")
247+
}
236248
entry, ok := cache.getFresh("/path")
237249
require.True(t, ok)
238250
require.Equal(t, "new", entry.content)
251+
_, watchState = cache.snapshot("/path")
252+
require.Same(t, currentWatcher, watchState.watcher)
253+
}
254+
255+
func TestConfigCacheResetDiscardsInFlightBusinessWatch(t *testing.T) {
256+
cache := newConfigCache(time.Minute)
257+
registerStarted := make(chan struct{})
258+
releaseRegister := make(chan struct{})
259+
removed := make(chan *zk.Watcher, 1)
260+
result := make(chan error, 1)
261+
var registrations atomic.Int32
262+
staleWatcher := &zk.Watcher{}
263+
currentWatcher := &zk.Watcher{}
264+
265+
go func() {
266+
result <- cache.ensureBusinessWatch("/path", func() (*zk.Watcher, error) {
267+
if registrations.Add(1) == 1 {
268+
close(registerStarted)
269+
<-releaseRegister
270+
return staleWatcher, nil
271+
}
272+
return currentWatcher, nil
273+
}, func(watcher *zk.Watcher) {
274+
removed <- watcher
275+
})
276+
}()
277+
278+
<-registerStarted
279+
require.Empty(t, cache.reset())
280+
close(releaseRegister)
281+
282+
require.NoError(t, <-result)
283+
require.Equal(t, int32(2), registrations.Load())
284+
select {
285+
case watcher := <-removed:
286+
require.Same(t, staleWatcher, watcher)
287+
case <-time.After(time.Second):
288+
t.Fatal("stale business watcher was not removed")
289+
}
290+
_, watchState := cache.snapshot("/path")
291+
require.Same(t, currentWatcher, watchState.watcher)
292+
require.False(t, watchState.auto)
239293
}

config_center/zookeeper/impl.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (c *zookeeperDynamicConfiguration) GetProperties(key string, opts ...config
134134
path := c.getPropertiesPath(key, opts...)
135135
entry, err := c.cache.load(path, func(watcher *zk.Watcher, registerWatch bool) (configCacheEntry, *zk.Watcher, error) {
136136
return c.loadProperties(path, watcher, registerWatch)
137-
})
137+
}, c.removeWatcher)
138138
if err != nil {
139139
return "", err
140140
}
@@ -343,10 +343,22 @@ func (c *zookeeperDynamicConfiguration) closeConfigs() {
343343
}
344344

345345
func (c *zookeeperDynamicConfiguration) RestartCallBack() bool {
346-
c.cache.reset()
346+
for _, watcher := range c.cache.reset() {
347+
c.removeWatcher(watcher)
348+
}
349+
if c.cacheListener != nil {
350+
c.cacheListener.restoreBusinessWatches()
351+
}
347352
return true
348353
}
349354

355+
func (c *zookeeperDynamicConfiguration) removeWatcher(watcher *zk.Watcher) {
356+
if watcher == nil || c.client == nil || c.client.Conn == nil {
357+
return
358+
}
359+
c.client.Conn.RemoveWatcher(watcher)
360+
}
361+
350362
func (c *zookeeperDynamicConfiguration) getPath(key string, group string) string {
351363
if len(key) == 0 {
352364
return c.buildPath(group)

0 commit comments

Comments
 (0)