Skip to content

fix(config_center/nacos): remove unused context.WithCancel and call CancelListenConfig on last listener removal#3441

Open
Aias00 wants to merge 4 commits into
apache:developfrom
Aias00:worktree-fix-nacos-cancel-leak
Open

fix(config_center/nacos): remove unused context.WithCancel and call CancelListenConfig on last listener removal#3441
Aias00 wants to merge 4 commits into
apache:developfrom
Aias00:worktree-fix-nacos-cancel-leak

Conversation

@Aias00

@Aias00 Aias00 commented Jun 17, 2026

Copy link
Copy Markdown

Problem

addListener() in config_center/nacos/listener.go created context.WithCancel(context.Background()) but the returned cancel function was never called, causing a goroutine and context leak. Additionally, removeListener() never called CancelListenConfig to unsubscribe from the nacos server, leaving stale config change listeners active indefinitely even after all listeners were removed.

Changes

config_center/nacos/listener.go

  • Remove unused context.WithCancel calls in addListener() — the cancel functions were stored but never invoked, leaking contexts and goroutines
  • Replace context.CancelFunc value type with struct{} in the inner sync.Map since the cancel function served no purpose
  • Remove unused "context" import
  • In removeListener(), call CancelListenConfig when the last listener for a key is removed, matching the behavior of Apollo (RemoveChangeListener) and File (watch.Remove) implementations
  • Clean up keyListeners entry when no listeners remain for a key
  • Add nil client guard for test compatibility

config_center/nacos/impl.go

  • Update keyListeners type comment from context.CancelFunc to struct{}

Before vs After

Scenario Before After
addListener creates context context.WithCancel called but cancel never used No context creation, struct{}{} stored
removeListener removes last listener Only deletes map entry, nacos subscription leaks Deletes map entry and calls CancelListenConfig
Listener goroutine cleanup Never cleaned up Cleaned up when all listeners removed

Testing

  • All existing tests pass (go test ./config_center/nacos/...)
  • Project compiles cleanly (go build ./...)

🤖 Generated with Claude Code

…ancelListenConfig on last listener removal

The addListener() function created context.WithCancel but the cancel
function was never called, causing a goroutine and context leak.
Additionally, removeListener() never called CancelListenConfig to
unsubscribe from the nacos server, leaving stale config change
listeners active indefinitely.

Changes:
- Remove unused context.WithCancel calls in addListener()
- Replace context.CancelFunc value type with struct{} in keyListeners
  map (the cancel function was never used)
- In removeListener(), call CancelListenConfig when the last listener
  for a key is removed (matching Apollo and File implementations)
- Clean up keyListeners entry when no listeners remain
- Add nil client guard for test compatibility

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 17, 2026 07:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR refactors Nacos listener bookkeeping by removing per-listener context.CancelFunc storage and adds logic to cancel the underlying Nacos subscription when the last listener for a key is removed.

Changes:

  • Stop creating/storing context.WithCancel cancel funcs for each listener and store a struct{} placeholder instead.
  • Enhance removeListener to delete the key entry and call CancelListenConfig when no listeners remain.
  • Update keyListeners type comment to reflect the new stored value type.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
config_center/nacos/listener.go Removes context usage, stores listeners as set entries, and cancels Nacos subscription when a key has no listeners.
config_center/nacos/impl.go Updates keyListeners comment to reflect struct{} placeholder values.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 20 to 22
import (
"context"
"sync"
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! These imports are already present in the file — the diff only showed the first import block, but the file has three import blocks (lines 20-22, 24-29, 31-37). The code compiles and all tests pass. No change needed.

Comment on lines +93 to +96
err := n.client.Client().CancelListenConfig(vo.ConfigParam{
DataId: key,
Group: n.resolvedGroup(n.url.GetParam(constant.NacosGroupKey, constant2.DEFAULT_GROUP)),
})
Comment thread config_center/nacos/listener.go Outdated
Comment on lines 81 to 101
listenersMap := rawListenersMap.(*sync.Map)
listenersMap.Delete(listener)

// If no listeners remain for this key, cancel the nacos config subscription
isEmpty := true
listenersMap.Range(func(_, _ any) bool {
isEmpty = false
return false
})
if isEmpty {
n.keyListeners.Delete(key)
if n.client != nil {
err := n.client.Client().CancelListenConfig(vo.ConfigParam{
DataId: key,
Group: n.resolvedGroup(n.url.GetParam(constant.NacosGroupKey, constant2.DEFAULT_GROUP)),
})
if err != nil {
logger.Errorf("[ConfigCenter][Nacos] cancel listen config fail, key=%s, err=%v", key, err)
}
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Fixed in the latest commit. I replaced the bare sync.Map with a keyListenerSet struct that uses sync.Mutex to make add/remove/check-empty atomic. This prevents the race where a concurrent addListener could add a listener between the emptiness check and the CancelListenConfig call. Additionally, I now delete from keyListeners before calling CancelListenConfig so that a new addListener won't find the stale set.

Comment on lines +93 to +96
err := n.client.Client().CancelListenConfig(vo.ConfigParam{
DataId: key,
Group: n.resolvedGroup(n.url.GetParam(constant.NacosGroupKey, constant2.DEFAULT_GROUP)),
})

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern. Fixed in the latest commit — the resolved group string is now stored in the keyListenerSet struct at registration time, and CancelListenConfig uses set.group directly instead of re-deriving it. This guarantees the same (DataId, Group) pair is used for both ListenConfig and CancelListenConfig.

@codecov-commenter

codecov-commenter commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.70213% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.58%. Comparing base (60d1c2a) to head (8787a89).
⚠️ Report is 850 commits behind head on develop.

Files with missing lines Patch % Lines
config_center/nacos/listener.go 61.70% 17 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3441      +/-   ##
===========================================
+ Coverage    46.76%   53.58%   +6.81%     
===========================================
  Files          295      493     +198     
  Lines        17172    38418   +21246     
===========================================
+ Hits          8031    20586   +12555     
- Misses        8287    16180    +7893     
- Partials       854     1652     +798     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Aias00 added 2 commits June 17, 2026 15:42
…fe listener management

Address Copilot review feedback:
1. Race condition: Replace bare sync.Map with keyListenerSet struct that
   uses sync.Mutex to make add/remove/check-empty atomic, preventing
   concurrent addListener from racing with removeListener's emptiness
   check and CancelListenConfig call.
2. DataId/Group consistency: Store the resolved group string in
   keyListenerSet so that CancelListenConfig uses the exact same group
   that was passed to ListenConfig, avoiding mismatch from re-derivation.
3. Delete keyListeners entry before CancelListenConfig to prevent new
   addListener from finding a stale set after cancellation.
4. Use snapshot() for callback iteration to safely iterate while
   mutations may occur.

Also add test for multi-listener removal behavior.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants