-
-
Notifications
You must be signed in to change notification settings - Fork 84
fix: deduplicate config listeners by identity #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: deduplicate config listeners by identity #121
Conversation
📝 WalkthroughWalkthroughReplaces equality-based listener deduplication in AbstractConfig with identity-based checks so distinct listener instances with identical characteristics can be registered, removed, and notified independently; adds unit tests validating multi-instance notification and removal; updates CHANGES.md with a changelog entry. Changes
Sequence Diagram(s)sequenceDiagram
participant Initializer as ApolloApplicationContextInitializer
participant Config as AbstractConfig
participant SourceA as CachedCompositePropertySource#A
participant SourceB as CachedCompositePropertySource#B
Note over Initializer,Config: Bootstrap and application contexts create distinct CachedCompositePropertySource instances with same name
Initializer->>Config: addChangeListener(listenerA, keysA, prefixesA)
Config-->>Config: containsListenerInstance? (identity) → false
Config-->>Config: add listenerA (store interested keys/prefixes)
Initializer->>Config: addChangeListener(listenerB, keysB, prefixesB)
Config-->>Config: containsListenerInstance? (identity) → false
Config-->>Config: add listenerB
Config->>SourceA: notify changeEvent
SourceA-->>SourceA: onChange -> clear cached names
SourceA-->>Config: handler invoked (listenerA or listenerB depending on instance)
Config->>SourceB: notify changeEvent
SourceB-->>SourceB: onChange -> clear cached names
SourceB-->>Config: handler invoked (distinct listener instances both get events)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java (1)
113-123:⚠️ Potential issue | 🟠 Major
m_interestedKeys/m_interestedKeyPrefixesremoval still usesequals, inconsistent with identity-basedm_listeners.When two listeners are equal-but-not-identical (the exact scenario this PR fixes),
m_interestedKeys.remove(listener)on line 115 usesequals-based lookup on theConcurrentHashMap, so removing either listener deletes the shared map entry for both. The surviving listener then has no entry inm_interestedKeys, causingisConfigChangeListenerInterestedto treat it as interested in all keys — a silent broadening of its subscription scope.The same issue applies to
addChangeListener:m_interestedKeys.put(listener2, ...)overwrites the entry keyed bylistener1(since ConcurrentHashMap usesequals), so listener1's interested-key set is silently replaced by listener2's.Consider switching these maps to an
IdentityHashMap(wrapped withCollections.synchronizedMapfor thread safety) to match the identity semantics used form_listeners.Proposed fix sketch
- private final Map<ConfigChangeListener, Set<String>> m_interestedKeys = Maps.newConcurrentMap(); - private final Map<ConfigChangeListener, Set<String>> m_interestedKeyPrefixes = Maps.newConcurrentMap(); + private final Map<ConfigChangeListener, Set<String>> m_interestedKeys = + Collections.synchronizedMap(new IdentityHashMap<>()); + private final Map<ConfigChangeListener, Set<String>> m_interestedKeyPrefixes = + Collections.synchronizedMap(new IdentityHashMap<>());And in
removeChangeListener, move the map removals inside the identity check so only the matched instance's entries are cleaned up:public boolean removeChangeListener(ConfigChangeListener listener) { - m_interestedKeys.remove(listener); - m_interestedKeyPrefixes.remove(listener); for (ConfigChangeListener addedListener : m_listeners) { if (addedListener == listener) { + m_interestedKeys.remove(addedListener); + m_interestedKeyPrefixes.remove(addedListener); return m_listeners.remove(addedListener); } } return false; }
🧹 Nitpick comments (1)
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigTest.java (1)
128-156: Good regression test that clearly validates the fix.The precondition assertions on lines 140-141 are excellent — they explicitly verify the conditions (
assertNotSame+assertEquals) that trigger the bug.Minor robustness note:
Thread.sleep(100)on line 152 is inherently racy. The existing tests in this class useSettableFuturewith a timeout for deterministic waiting. Consider adopting the same pattern here for consistency and CI stability.
427c8d8 to
6d32daa
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR addresses a Spring Cloud bootstrap dual-context edge case where change listeners that are different instances but equals()-equal (e.g., Spring PropertySource equality-by-name) could be incorrectly de-duplicated, leading to missed notifications and stale cached property names.
Changes:
- Switch
AbstractConfigchange-listener de-duplication fromequals()to instance identity (==). - Update listener removal logic to attempt identity-based removal.
- Add a regression test covering two same-name
CachedCompositePropertySourcelisteners and updateCHANGES.md.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java |
Changes listener registration/removal logic to use identity semantics. |
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigTest.java |
Adds regression test for two equals()-equal but distinct listener instances both receiving events. |
CHANGES.md |
Documents the fix in release notes. |
Comments suppressed due to low confidence (1)
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java:109
m_interestedKeys/m_interestedKeyPrefixesare still keyed byConfigChangeListenerin aConcurrentMap, so they still useequals/hashCode. For listeners whereequalsis not identity (e.g., SpringPropertySourceequality-by-name), registering two distinct instances will collide and overwrite each other’s interested-keys/prefixes, and later lookups/removals won’t be identity-safe. To truly switch to identity semantics, store per-listener metadata with identity keys (e.g., wrap the listener in an identity-based key, or replace the separate maps with a single registration object that holds listener + filters).
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys, Set<String> interestedKeyPrefixes) {
if (!containsListenerInstance(listener)) {
m_listeners.add(listener);
if (interestedKeys != null && !interestedKeys.isEmpty()) {
m_interestedKeys.put(listener, Sets.newHashSet(interestedKeys));
}
if (interestedKeyPrefixes != null && !interestedKeyPrefixes.isEmpty()) {
m_interestedKeyPrefixes.put(listener, Sets.newHashSet(interestedKeyPrefixes));
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java
Outdated
Show resolved
Hide resolved
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigTest.java
Show resolved
Hide resolved
6d32daa to
ad4aa96
Compare
ad4aa96 to
85b3cf4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
What's the purpose of this PR
Fixes a listener registration edge case in Spring Cloud bootstrap dual-context initialization.
When two
CachedCompositePropertySourcelisteners share the same name (ApolloBootstrapPropertySources),AbstractConfig#addChangeListenerusedList.contains(equals-based) to de-duplicate listeners.Because Spring
PropertySourcedefines equality by name, the second listener instance could be skipped,which leaves the active context listener unsubscribed and
CachedCompositePropertySource#namesstale after key add/delete.This PR switches listener de-duplication/removal to instance identity (
==) semantics and adds a regression testcovering two same-name
CachedCompositePropertySourcelisteners.Which issue(s) this PR fixes:
Fixes #88
Brief changelog
AbstractConfig#addChangeListenerto deduplicate by object identity instead ofequalsAbstractConfig#removeChangeListenerto remove by object identityCachedCompositePropertySourcelisteners both receiving change eventsCHANGES.mdFollow this checklist to help us incorporate your contribution quickly and easily:
mvn clean testto make sure this pull request doesn't break anything.CHANGESlog.Test run details
mvn -pl apollo-client clean testmvn -pl apollo-client -Dtest=AbstractConfigTest,CachedCompositePropertySourceTest,ApolloApplicationContextInitializerTest testSummary by CodeRabbit
Bug Fixes
Tests