Skip to content
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

Optimize dynamic config: integrate Zookeeper & Nacos, support interface-level dynamic config #1430

Merged
merged 9 commits into from
Oct 31, 2024

Conversation

Narzisss
Copy link
Contributor

@Narzisss Narzisss commented Jul 21, 2024

This is a Work In Progress pull request for issue #1428.

Modification:

Integrate Zookeeper as configuration center with ZookeeperDynamicConfigManager.
Integrate Nacos as configuration center with NacosDynamicConfigManager.
Support dynamic config at the interface level.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced new modules for ZooKeeper and Nacos configuration management within the SOFA RPC framework.
    • Enhanced dynamic configuration management capabilities, allowing for responsive adjustments to configuration parameters at runtime.
    • Added support for dynamic configuration updates, allowing for more flexible service management.
    • New DynamicUrl class for parsing and managing dynamic URLs.
    • Added a ConfigChangedEvent class to represent configuration change events.
    • Expanded dynamic configuration properties coverage in tests.
  • Tests

    • Added unit tests for the ZooKeeper, Nacos, and Apollo dynamic configuration managers to validate dynamic configuration updates.
    • Introduced tests for the DynamicUrl class to ensure proper URL parsing functionality.
    • Updated tests for ApolloDynamicConfigManager to enhance coverage of dynamic configuration properties.
  • Chores

    • Updated project structure to include new modules and dependencies in the main configuration.

Copy link

sofastack-cla bot commented Jul 21, 2024

Hi @Narzisss, welcome to SOFAStack community, Please sign Contributor License Agreement!

After you signed CLA, we will automatically sync the status of this pull request in 3 minutes.

@sofastack-cla sofastack-cla bot added cla:no Need sign CLA First-time contributor First-time contributor size/L labels Jul 21, 2024
Copy link
Contributor

coderabbitai bot commented Jul 21, 2024

Walkthrough

The recent updates enhance the dynamic configuration management capabilities within the SOFA RPC framework. Key changes include the introduction of new classes for handling dynamic configurations with Nacos and Zookeeper, along with modifications to existing classes for better management of configuration events. Additionally, new unit tests have been added to validate the functionality of these dynamic configurations, ensuring that changes are correctly processed and reflected in consumer services.

Changes

Files Change Summary
test/test-integration/pom.xml Updated dependency from sofa-rpc-config-apollo to sofa-rpc-config-nacos, added new dependencies for sofa-rpc-config-apollo, sofa-rpc-config-zk, and curator-test.
bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java Enhanced dynamic configuration capabilities, updated method signatures, and added new imports.
config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java Added new methods and updated the constructor for improved configuration management.
config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java Introduced a new class for managing dynamic configurations with Nacos, added multiple methods for configuration management.
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java Added new constants and configuration keys for dynamic configuration centers.
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java Updated constructor and added new methods for enhanced functionality.
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java Added a unit test class for Apollo dynamic configuration management.
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java Added a JUnit test class for Nacos dynamic configuration management.
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java Introduced a test class for validating Zookeeper dynamic configuration management.
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicUrl.java Added a new class for parsing and managing dynamic URLs.
core/api/src/test/java/com/alipay/sofa/rpc/dynamic/DynamicUrlTest.java Introduced unit tests for the DynamicUrl class to validate URL parsing functionality.
config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java Added a new class for managing dynamic configurations with Zookeeper, including methods for configuration management.
core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java Added a cache for dynamic configuration values and updated methods to utilize this cache.
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java Introduced a class to represent configuration change events with attributes for key, content, and change type.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ZookeeperDynamicConfigManager
    participant NacosDynamicConfigManager
    participant ZooKeeper
    participant Nacos

    User->>ZookeeperDynamicConfigManager: Request Config from ZK
    ZookeeperDynamicConfigManager->>ZooKeeper: Fetch Config Data
    ZooKeeper-->>ZookeeperDynamicConfigManager: Return Config Data
    ZookeeperDynamicConfigManager-->>User: Provide ZK Config

    User->>NacosDynamicConfigManager: Request Config from Nacos
    NacosDynamicConfigManager->>Nacos: Fetch Config Data
    Nacos-->>NacosDynamicConfigManager: Return Config Data
    NacosDynamicConfigManager-->>User: Provide Nacos Config
Loading

Poem

🐇 In the meadow where bunnies play,
New helpers hop in, brightening the day!
With ZooKeeper and Nacos, they gleefully sing,
Dynamic configs, what joy they bring!
Let’s cheer for the changes, a magical trance,
For smoother coding, let’s all prance! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between d163820 and 86c0c52.

Files selected for processing (7)
  • all/pom.xml (2 hunks)
  • config/config-zk/pom.xml (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager (1 hunks)
  • config/config-zk/src/test/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManagerTest.java (1 hunks)
  • config/config-zk/src/test/resources/log4j.xml (1 hunks)
  • config/pom.xml (1 hunks)
Files skipped from review due to trivial changes (2)
  • config/config-zk/src/test/resources/log4j.xml
  • config/pom.xml
Additional comments not posted (24)
config/config-zk/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager (1)

1-1: LGTM!

The service provider configuration for ZookeeperDynamicConfigManager is correctly mapped.

config/config-zk/src/test/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManagerTest.java (5)

1-16: Ensure the license header is up-to-date.

The license header is present and appears to be correct. Ensure it is up-to-date with the latest project standards.


17-17: Package declaration is correct.

The package declaration aligns with the project's structure.


19-23: Imports are appropriate.

The imported classes are relevant for the test cases.


27-28: Logger initialization is correct.

The logger is correctly initialized for the test class.


32-36: Test method getProviderServiceProperty is well-defined.

The test method checks the retrieval of provider service properties and asserts the expected result.

config/config-zk/pom.xml (6)

1-11: POM file header and parent configuration are correct.

The POM file header and parent configuration are correctly defined.


13-13: Artifact ID is correct.

The artifact ID sofa-rpc-config-zk aligns with the project structure.


15-47: Dependencies are correctly defined.

The dependencies are necessary and correctly defined for the project.


51-71: Resources configuration is correct.

The resources configuration is correctly defined for the project.


73-110: Plugins configuration is correctly defined.

The plugins configuration includes necessary Maven plugins and is correctly defined.


111-112: POM file footer is correct.

The POM file footer is correctly defined.

config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (10)

1-16: Ensure compliance with ASF license.

The file includes the Apache Software Foundation license header. Ensure that the usage complies with the ASF guidelines.


17-27: Imports look good.

The imports are relevant and necessary for the class functionality.


34-35: Class annotation and declaration are appropriate.

The @Extension annotation is correctly used to indicate that this class is an extension for Zookeeper.


37-41: Class-level variables are well-defined.

The class-level variables are correctly declared and initialized.


43-51: Constructor initializes Zookeeper client correctly.

The constructor initializes the Zookeeper client with the specified address, retry policy, and namespace.


58-67: Method correctly retrieves provider service property.

The method builds the path using DynamicConfigKeyHelper, retrieves the data from Zookeeper, and handles exceptions appropriately.


69-77: Method correctly retrieves consumer service property.

The method builds the path using DynamicConfigKeyHelper, retrieves the data from Zookeeper, and handles exceptions appropriately.


80-89: Method correctly retrieves provider method property.

The method builds the path using DynamicConfigKeyHelper, retrieves the data from Zookeeper, and handles exceptions appropriately.


91-99: Method correctly retrieves consumer method property.

The method builds the path using DynamicConfigKeyHelper, retrieves the data from Zookeeper, and handles exceptions appropriately.


109-111: Method correctly constructs prefix path.

The method constructs the prefix path by concatenating the configuration node, default group, and context separator.

all/pom.xml (2)

313-317: Dependency for sofa-rpc-config-zk is correctly added.

The dependency is correctly declared with the appropriate groupId, artifactId, and version.


573-573: Include for sofa-rpc-config-zk is correctly added.

The include is correctly declared within the Maven Shade Plugin configuration.

private final static Logger logger = LoggerFactory
.getLogger(ZookeeperDynamicConfigManager.class);

private ZookeeperDynamicConfigManager zookeeperDynamicConfigManager = new ZookeeperDynamicConfigManager("test");
Copy link
Contributor

@coderabbitai coderabbitai bot Jul 21, 2024

Choose a reason for hiding this comment

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

Consider using dependency injection for ZookeeperDynamicConfigManager.

Direct instantiation of ZookeeperDynamicConfigManager may limit test flexibility. Consider using dependency injection for better testability.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can use DynamicConfigManagerFactory.getDynamicManager instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

@EvenLjj, thank you for the suggestion. Using DynamicConfigManagerFactory.getDynamicManager is indeed a better approach and aligns with the existing code practices. I recommend updating the test code to utilize this factory method for creating instances of ZookeeperDynamicConfigManager.

private ZookeeperDynamicConfigManager zookeeperDynamicConfigManager = (ZookeeperDynamicConfigManager) DynamicConfigManagerFactory.getDynamicManager("test");

This change will enhance the testability and maintain consistency with the rest of the codebase.

Analysis chain
Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for the definition and usage of `DynamicConfigManagerFactory.getDynamicManager` in the codebase.
rg 'DynamicConfigManagerFactory.getDynamicManager'

Length of output: 627

@EvenLjj EvenLjj closed this Jul 22, 2024
@EvenLjj EvenLjj reopened this Jul 22, 2024
@sofastack-cla sofastack-cla bot added the cla:yes CLA is ok label Jul 22, 2024
public String getProviderServiceProperty(String service, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildProviderServiceProKey(service, key);
byte[] bytes = zkClient.getData().forPath(path);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This zkClient.getData().forPath method does not have cached. Here should add a cache in conjunction with the listener mentioned above.

@Override
public String getConsumerServiceProperty(String service, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildConsumerServiceProKey(service, key);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This zkClient.getData().forPath method does not have cached. Here should add a cache in conjunction with the listener mentioned above.

@Override
public String getProviderMethodProperty(String service, String method, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildProviderMethodProKey(service, method, key);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This zkClient.getData().forPath method does not have cached. Here should add a cache in conjunction with the listener mentioned above.

@Override
public String getConsumerMethodProperty(String service, String method, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildConsumerMethodProKey(service, method, key);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This zkClient.getData().forPath method does not have cached. Here should add a cache in conjunction with the listener mentioned above.

@EvenLjj
Copy link
Collaborator

EvenLjj commented Jul 30, 2024

image
Should run mvn clean compile before git commit.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 86c0c52 and 70352c2.

Files selected for processing (13)
  • all/pom.xml (2 hunks)
  • config/config-nacos/pom.xml (1 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NaocsDynamicConfigManager.java (1 hunks)
  • config/config-nacos/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager (1 hunks)
  • config/config-nacos/src/test/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManagerTest.java (1 hunks)
  • config/config-nacos/src/test/resources/log4j.xml (1 hunks)
  • config/config-zk/pom.xml (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager (1 hunks)
  • config/config-zk/src/test/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManagerTest.java (1 hunks)
  • config/config-zk/src/test/resources/log4j.xml (1 hunks)
  • config/pom.xml (1 hunks)
  • core/common/src/main/java/com/alipay/sofa/rpc/common/config/RpcConfigKeys.java (1 hunks)
Files skipped from review due to trivial changes (2)
  • config/config-nacos/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager
  • config/config-nacos/src/test/resources/log4j.xml
Additional comments not posted (24)
config/config-zk/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager (1)

1-1: LGTM!

The service provider configuration correctly maps the zookeeper key to the ZookeeperDynamicConfigManager class.

config/config-zk/src/test/resources/log4j.xml (1)

1-16: LGTM!

The Log4j configuration sets up a console appender with a pattern and sets the root logger level to INFO. This is a standard configuration for test environments.

config/config-zk/src/test/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManagerTest.java (1)

32-36: LGTM!

The test method getProviderServiceProperty is implemented correctly and verifies the expected behavior.

config/pom.xml (1)

18-19: LGTM!

The additions of config-zk and config-nacos modules are straightforward and align with the PR objectives.

config/config-nacos/pom.xml (1)

1-107: LGTM!

The dependencies and build configurations for the config-nacos module are standard for a Maven project and align with the PR objectives.

config/config-zk/pom.xml (4)

13-13: LGTM!

The artifact ID sofa-rpc-config-zk is appropriate and follows naming conventions.


51-111: LGTM!

The build configurations are appropriate and follow best practices.


1-11: Ensure the parent project version is defined.

The parent project version is defined using the ${revision} property. Ensure that this property is set correctly in the parent POM or properties file.

Verification successful

The parent project version is defined correctly.

The ${revision} property is set to 5.13.1-SNAPSHOT in the pom.xml and bom/pom.xml files. This confirms that the parent project version is defined and has a valid value.

  • pom.xml: <revision>5.13.1-SNAPSHOT</revision>
  • bom/pom.xml: <revision>5.13.1-SNAPSHOT</revision>
Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the `${revision}` property is set correctly in the parent POM or properties file.

# Test: Search for the `${revision}` property definition. Expect: The property is defined and has a valid value.
rg --type xml -A 3 $'<revision>'

Length of output: 518


15-47: Ensure dependency versions are managed.

The dependencies do not specify versions, which implies that the versions are managed in the parent POM or a dependency management section. Ensure that the versions are defined and managed correctly.

Verification successful

Dependency versions are managed.

The dependencies within the dependencyManagement sections have versions specified, often using placeholders like ${project.parent.version} or ${revision}. This indicates that the versions are managed appropriately.

  • tracer/pom.xml
  • remoting/pom.xml
  • registry/pom.xml
  • pom.xml
  • metrics/pom.xml
  • fault/pom.xml
  • core/pom.xml
  • core-impl/pom.xml
  • config/pom.xml
  • bootstrap/pom.xml
  • codec/pom.xml
  • bom/pom.xml
Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the dependency versions are managed correctly in the parent POM or a dependency management section.

# Test: Search for the dependency management section. Expect: The dependencies are defined with versions.
rg --type xml -A 3 $'<dependencyManagement>'

Length of output: 2280


Script:

#!/bin/bash
# Description: Verify that the dependencies within the dependency management sections have versions specified.

# List of POM files containing dependencyManagement sections
poms=(
  "tracer/pom.xml"
  "remoting/pom.xml"
  "registry/pom.xml"
  "pom.xml"
  "metrics/pom.xml"
  "fault/pom.xml"
  "core/pom.xml"
  "core-impl/pom.xml"
  "config/pom.xml"
  "bootstrap/pom.xml"
  "codec/pom.xml"
  "bom/pom.xml"
)

# Check for the presence of version tags within the dependencyManagement sections
for pom in "${poms[@]}"; do
  echo "Checking $pom for dependency versions..."
  rg --type xml -A 3 '<dependencyManagement>' "$pom" | grep -A 10 '<dependency>' | grep '<version>'
done

Length of output: 3140


Script:

#!/bin/bash
# Description: Extract and display the entire dependencyManagement sections from the identified POM files to verify the presence of version tags.

# List of POM files containing dependencyManagement sections
poms=(
  "tracer/pom.xml"
  "remoting/pom.xml"
  "registry/pom.xml"
  "pom.xml"
  "metrics/pom.xml"
  "fault/pom.xml"
  "core/pom.xml"
  "core-impl/pom.xml"
  "config/pom.xml"
  "bootstrap/pom.xml"
  "codec/pom.xml"
  "bom/pom.xml"
)

# Extract and display the dependencyManagement sections
for pom in "${poms[@]}"; do
  echo "Extracting dependencyManagement section from $pom..."
  rg --type xml -A 50 '<dependencyManagement>' "$pom"
done

Length of output: 24768

config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NaocsDynamicConfigManager.java (3)

1-17: LGTM!

The license header and package declaration are present and correct.


19-30: LGTM!

The import statements are appropriate and necessary.


67-104: LGTM!

The methods are implemented correctly and handle exceptions appropriately.

config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (6)

1-17: LGTM!

The license header and package declaration are present and correct.


19-30: LGTM!

The import statements are appropriate and necessary.


46-54: LGTM!

The constructor initializes the CuratorFramework with Zookeeper configurations appropriately.


56-59: Reminder: Implement the service configuration initialization.

The method initServiceConfiguration is currently a placeholder. Ensure to implement the service configuration initialization in the future.


107-110: Reminder: Implement the service authentication rule retrieval.

The method getServiceAuthRule is currently a placeholder. Ensure to implement the service authentication rule retrieval in the future.


36-45: Ensure the default address is configurable.

The default address 127.0.0.1:2181 is hardcoded. Use SofaConfigs.getOrDefault to make it configurable.

- private static final String    ADDRESS           = "127.0.0.1:2181";
+ private static final String    ADDRESS           = SofaConfigs.getOrCustomDefault(RpcConfigKeys.ZK_ADDRESS, "127.0.0.1:2181");

Likely invalid or redundant comment.

core/common/src/main/java/com/alipay/sofa/rpc/common/config/RpcConfigKeys.java (2)

145-151: LGTM!

The ZK_ADDRESS configuration key is correctly defined with appropriate parameters.


153-159: LGTM!

The NACOS_ADDRESS configuration key is correctly defined with appropriate parameters.

all/pom.xml (4)

313-317: LGTM!

The sofa-rpc-config-zk dependency is correctly added under the <dependencies> section.


578-578: LGTM!

The sofa-rpc-config-zk dependency is correctly included in the <includes> section of the configuration block.


318-322: LGTM!

The sofa-rpc-config-nacos dependency is correctly added under the <dependencies> section.


579-579: LGTM!

The sofa-rpc-config-nacos dependency is correctly included in the <includes> section of the configuration block.

Comment on lines 38 to 57
@Test
public void getConsumerServiceProperty() {
}

@Test
public void getProviderMethodProperty() {
}

@Test
public void getConsumerMethodProperty() {
}

@Test
public void getServiceAuthRule() {
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Implement the missing test methods.

The following test methods are currently empty and should be implemented to ensure full test coverage:

  • getConsumerServiceProperty
  • getProviderMethodProperty
  • getConsumerMethodProperty
  • getServiceAuthRule

Comment on lines 60 to 64
@Override
public void initServiceConfiguration(String service) {
//TODO not now

}
Copy link
Contributor

Choose a reason for hiding this comment

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

Reminder: Implement the service configuration initialization.

The method initServiceConfiguration is currently a placeholder. Ensure to implement the service configuration initialization in the future.

Do you want me to generate the implementation code or open a GitHub issue to track this task?

Comment on lines 107 to 110
public AuthRuleGroup getServiceAuthRule(String service) {
//TODO 暂不支持
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Reminder: Implement the service authentication rule retrieval.

The method getServiceAuthRule is currently a placeholder. Ensure to implement the service authentication rule retrieval in the future.

Do you want me to generate the implementation code or open a GitHub issue to track this task?

Comment on lines 62 to 148
public String getProviderServiceProperty(String service, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildProviderServiceProKey(service, key);
byte[] bytes = zkClient.getData().forPath(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}

@Override
public String getConsumerServiceProperty(String service, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildConsumerServiceProKey(service, key);
byte[] bytes = zkClient.getData().forPath(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}

@Override
public String getProviderMethodProperty(String service, String method, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildProviderMethodProKey(service, method, key);
byte[] bytes = zkClient.getData().forPath(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}

@Override
public String getConsumerMethodProperty(String service, String method, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildConsumerMethodProKey(service, method, key);
byte[] bytes = zkClient.getData().forPath(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Add caching for Zookeeper data retrieval.

The zkClient.getData().forPath method does not have caching. Consider adding a cache in conjunction with a listener to improve performance.

- byte[] bytes = zkClient.getData().forPath(path);
+ byte[] bytes = getCachedData(path);

private byte[] getCachedData(String path) {
    // Implement caching logic here
    return zkClient.getData().forPath(path);
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String getProviderServiceProperty(String service, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildProviderServiceProKey(service, key);
byte[] bytes = zkClient.getData().forPath(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}
@Override
public String getConsumerServiceProperty(String service, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildConsumerServiceProKey(service, key);
byte[] bytes = zkClient.getData().forPath(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}
@Override
public String getProviderMethodProperty(String service, String method, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildProviderMethodProKey(service, method, key);
byte[] bytes = zkClient.getData().forPath(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}
@Override
public String getConsumerMethodProperty(String service, String method, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildConsumerMethodProKey(service, method, key);
byte[] bytes = zkClient.getData().forPath(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
public String getProviderServiceProperty(String service, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildProviderServiceProKey(service, key);
byte[] bytes = getCachedData(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}
@Override
public String getConsumerServiceProperty(String service, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildConsumerServiceProKey(service, key);
byte[] bytes = getCachedData(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}
@Override
public String getProviderMethodProperty(String service, String method, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildProviderMethodProKey(service, method, key);
byte[] bytes = getCachedData(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}
@Override
public String getConsumerMethodProperty(String service, String method, String key) {
try {
String path = getPrefixPath() + DynamicConfigKeyHelper.buildConsumerMethodProKey(service, method, key);
byte[] bytes = getCachedData(path);
return bytes != null ? new String(bytes) : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}
private byte[] getCachedData(String path) {
// Implement caching logic here
return zkClient.getData().forPath(path);
}

@Narzisss Narzisss changed the title integrate Zookeeper as configuration center with ZookeeperDynamicConfigManager integrate Zookeeper and Nacos as configuration centers Aug 5, 2024
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Outside diff range, codebase verification and nitpick comments (1)
config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1)

Line range hint 151-157: Handle null case in getCachedData method.

The getCachedData method should handle the case where cache.getCurrentData(path) returns null to avoid potential NullPointerException.

- return cache.getCurrentData(path).getData();
+ return cache.getCurrentData(path) != null ? cache.getCurrentData(path).getData() : null;
Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 70352c2 and 223547e.

Files selected for processing (13)
  • all/pom.xml (2 hunks)
  • config/config-nacos/pom.xml (1 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • config/config-nacos/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager (1 hunks)
  • config/config-nacos/src/test/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManagerTest.java (1 hunks)
  • config/config-nacos/src/test/resources/log4j.xml (1 hunks)
  • config/config-zk/pom.xml (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager (1 hunks)
  • config/config-zk/src/test/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManagerTest.java (1 hunks)
  • config/config-zk/src/test/resources/log4j.xml (1 hunks)
  • config/pom.xml (1 hunks)
  • core/common/src/main/java/com/alipay/sofa/rpc/common/config/RpcConfigKeys.java (1 hunks)
Files skipped from review due to trivial changes (4)
  • config/config-nacos/pom.xml
  • config/config-nacos/src/test/resources/log4j.xml
  • config/config-zk/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager
  • config/config-zk/src/test/resources/log4j.xml
Files skipped from review as they are similar to previous changes (6)
  • all/pom.xml
  • config/config-nacos/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager
  • config/config-nacos/src/test/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManagerTest.java
  • config/config-zk/pom.xml
  • config/config-zk/src/test/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManagerTest.java
  • config/pom.xml
Additional comments not posted (7)
config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (2)

1-18: License and package declaration are correct.

The license and package declaration follow the required format.


19-33: Imports are appropriate.

The imports include necessary libraries for Nacos, logging, and configuration management.

config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (3)

1-18: License and package declaration are correct.

The license and package declaration follow the required format.


19-40: Imports are appropriate.

The imports include necessary libraries for Zookeeper, logging, and configuration management.


61-75: Improve exception handling in the constructor.

The exception handling in the constructor could be improved by providing more context in the log message.

- LOGGER.error("setupPathChildrenCache error", e);
+ LOGGER.error("setupPathChildrenCache error for appName: " + appName, e);

Likely invalid or redundant comment.

core/common/src/main/java/com/alipay/sofa/rpc/common/config/RpcConfigKeys.java (2)

Line range hint 1-11: License and package declaration are correct.

The license and package declaration follow the required format.


Line range hint 12-15: Imports are appropriate.

The imports include necessary libraries for configuration management.

Comment on lines 70 to 74
@Override
public void initServiceConfiguration(String service) {
//TODO not now

}
Copy link
Contributor

Choose a reason for hiding this comment

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

Implement service configuration initialization.

The initServiceConfiguration method is currently a placeholder. Ensure to implement the service configuration initialization in the future.

Do you want me to generate the implementation code or open a GitHub issue to track this task?

Comment on lines +117 to +127
public AuthRuleGroup getServiceAuthRule(String service) {
//TODO 暂不支持
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Implement service authentication rule retrieval.

The getServiceAuthRule method is currently a placeholder. Ensure to implement the service authentication rule retrieval in the future.

Do you want me to generate the implementation code or open a GitHub issue to track this task?

Comment on lines 41 to 51
@Extension(value = "nacos", override = true)
public class NacosDynamicConfigManager extends DynamicConfigManager {

private final static Logger LOGGER = LoggerFactory.getLogger(NacosDynamicConfigManager.class);
private static final String DEFAULT_NAMESPACE = "sofa-rpc";
private static final String ADDRESS = SofaConfigs.getOrDefault(RpcConfigKeys.NACOS_ADDRESS);
private static final String DEFAULT_GROUP = "sofa-rpc";
private static final long DEFAULT_TIMEOUT = 5000;
private ConfigService configService;
private Properties nacosConfig = new Properties();
private final String appName;
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using SofaConfigs#getOrDefault for default values.

The DEFAULT_NAMESPACE and DEFAULT_GROUP values are hardcoded. Consider using SofaConfigs#getOrDefault for these values to allow for configuration flexibility.

- private static final String DEFAULT_NAMESPACE = "sofa-rpc";
- private static final String DEFAULT_GROUP = "sofa-rpc";
+ private static final String DEFAULT_NAMESPACE = SofaConfigs.getOrDefault(RpcConfigKeys.NACOS_NAMESPACE, "sofa-rpc");
+ private static final String DEFAULT_GROUP = SofaConfigs.getOrDefault(RpcConfigKeys.NACOS_GROUP, "sofa-rpc");
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Extension(value = "nacos", override = true)
public class NacosDynamicConfigManager extends DynamicConfigManager {
private final static Logger LOGGER = LoggerFactory.getLogger(NacosDynamicConfigManager.class);
private static final String DEFAULT_NAMESPACE = "sofa-rpc";
private static final String ADDRESS = SofaConfigs.getOrDefault(RpcConfigKeys.NACOS_ADDRESS);
private static final String DEFAULT_GROUP = "sofa-rpc";
private static final long DEFAULT_TIMEOUT = 5000;
private ConfigService configService;
private Properties nacosConfig = new Properties();
private final String appName;
private static final String DEFAULT_NAMESPACE = SofaConfigs.getOrDefault(RpcConfigKeys.NACOS_NAMESPACE, "sofa-rpc");
private static final String DEFAULT_GROUP = SofaConfigs.getOrDefault(RpcConfigKeys.NACOS_GROUP, "sofa-rpc");

Comment on lines 53 to 67
protected NacosDynamicConfigManager(String appName) {
super(appName);
if (StringUtils.isEmpty(appName)) {
this.appName = DEFAULT_GROUP;
} else {
this.appName = appName;
}
try {
nacosConfig.put(PropertyKeyConst.SERVER_ADDR, ADDRESS);
nacosConfig.put(PropertyKeyConst.NAMESPACE, DEFAULT_NAMESPACE);
configService = ConfigFactory.createConfigService(nacosConfig);

} catch (NacosException e) {
LOGGER.error("Failed to create ConfigService", e);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Improve exception handling in the constructor.

The exception handling in the constructor could be improved by providing more context in the log message.

- LOGGER.error("Failed to create ConfigService", e);
+ LOGGER.error("Failed to create ConfigService for appName: " + appName, e);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected NacosDynamicConfigManager(String appName) {
super(appName);
if (StringUtils.isEmpty(appName)) {
this.appName = DEFAULT_GROUP;
} else {
this.appName = appName;
}
try {
nacosConfig.put(PropertyKeyConst.SERVER_ADDR, ADDRESS);
nacosConfig.put(PropertyKeyConst.NAMESPACE, DEFAULT_NAMESPACE);
configService = ConfigFactory.createConfigService(nacosConfig);
} catch (NacosException e) {
LOGGER.error("Failed to create ConfigService", e);
}
protected NacosDynamicConfigManager(String appName) {
super(appName);
if (StringUtils.isEmpty(appName)) {
this.appName = DEFAULT_GROUP;
} else {
this.appName = appName;
}
try {
nacosConfig.put(PropertyKeyConst.SERVER_ADDR, ADDRESS);
nacosConfig.put(PropertyKeyConst.NAMESPACE, DEFAULT_NAMESPACE);
configService = ConfigFactory.createConfigService(nacosConfig);
} catch (NacosException e) {
LOGGER.error("Failed to create ConfigService for appName: " + appName, e);
}

Comment on lines 151 to 157
private byte[] getCachedData(String path) throws Exception {
PathChildrenCache cache = cacheMap.get(rootPath);
if (cache == null || cache.getCurrentData() == null) {
setupPathChildrenCache();
cache = cacheMap.get(rootPath);
}
return cache.getCurrentData(path).getData();
Copy link
Contributor

Choose a reason for hiding this comment

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

Handle null case in getCachedData method.

The getCachedData method should handle the case where cache.getCurrentData(path) returns null to avoid potential NullPointerException.

- return cache.getCurrentData(path).getData();
+ return cache.getCurrentData(path) != null ? cache.getCurrentData(path).getData() : null;
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private byte[] getCachedData(String path) throws Exception {
PathChildrenCache cache = cacheMap.get(rootPath);
if (cache == null || cache.getCurrentData() == null) {
setupPathChildrenCache();
cache = cacheMap.get(rootPath);
}
return cache.getCurrentData(path).getData();
private byte[] getCachedData(String path) throws Exception {
PathChildrenCache cache = cacheMap.get(rootPath);
if (cache == null || cache.getCurrentData() == null) {
setupPathChildrenCache();
cache = cacheMap.get(rootPath);
}
return cache.getCurrentData(path) != null ? cache.getCurrentData(path).getData() : null;

Comment on lines 144 to 159
// config center
public static ConfigKey<String> ZK_ADDRESS = ConfigKey
.build(
"sofa.rpc.config.center.address",
"127.0.0.1:2181",
false,
"The address of Zookeeper configuration center.",
new String[] { "zookeeper_address" });

public static ConfigKey<String> NACOS_ADDRESS = ConfigKey
.build(
"sofa.rpc.config.center.address",
"127.0.0.1:8848",
false,
"The address of Nacos configuration center.",
new String[] { "nacos_address" });
Copy link
Contributor

Choose a reason for hiding this comment

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

Use different key names for NACOS_ADDRESS and ZK_ADDRESS.

The key names for NACOS_ADDRESS and ZK_ADDRESS are the same. Consider using different key names to avoid conflicts.

- "sofa.rpc.config.center.address",
+ "sofa.rpc.config.center.nacos.address",
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// config center
public static ConfigKey<String> ZK_ADDRESS = ConfigKey
.build(
"sofa.rpc.config.center.address",
"127.0.0.1:2181",
false,
"The address of Zookeeper configuration center.",
new String[] { "zookeeper_address" });
public static ConfigKey<String> NACOS_ADDRESS = ConfigKey
.build(
"sofa.rpc.config.center.address",
"127.0.0.1:8848",
false,
"The address of Nacos configuration center.",
new String[] { "nacos_address" });
// config center
public static ConfigKey<String> ZK_ADDRESS = ConfigKey
.build(
"sofa.rpc.config.center.address",
"127.0.0.1:2181",
false,
"The address of Zookeeper configuration center.",
new String[] { "zookeeper_address" });
public static ConfigKey<String> NACOS_ADDRESS = ConfigKey
.build(
"sofa.rpc.config.center.nacos.address",
"127.0.0.1:8848",
false,
"The address of Nacos configuration center.",
new String[] { "nacos_address" });

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 223547e and 528d0fb.

Files selected for processing (14)
  • all/pom.xml (2 hunks)
  • config/config-apollo/src/test/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManagerTest.java (3 hunks)
  • config/config-nacos/pom.xml (1 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • config/config-nacos/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager (1 hunks)
  • config/config-nacos/src/test/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManagerTest.java (1 hunks)
  • config/config-nacos/src/test/resources/log4j.xml (1 hunks)
  • config/config-zk/pom.xml (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager (1 hunks)
  • config/config-zk/src/test/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManagerTest.java (1 hunks)
  • config/config-zk/src/test/resources/log4j.xml (1 hunks)
  • config/pom.xml (1 hunks)
  • core/common/src/main/java/com/alipay/sofa/rpc/common/config/RpcConfigKeys.java (1 hunks)
Files skipped from review due to trivial changes (4)
  • config/config-nacos/pom.xml
  • config/config-nacos/src/test/resources/log4j.xml
  • config/config-zk/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager
  • config/config-zk/src/test/resources/log4j.xml
Files skipped from review as they are similar to previous changes (5)
  • all/pom.xml
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java
  • config/config-nacos/src/main/resources/META-INF/services/sofa-rpc/com.alipay.sofa.rpc.dynamic.DynamicConfigManager
  • config/config-zk/pom.xml
  • config/pom.xml
Additional comments not posted (10)
config/config-apollo/src/test/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManagerTest.java (1)

32-33: Use of Factory Method for Instantiation.

The use of DynamicConfigManagerFactory.getDynamicManager for instantiating apolloDynamicConfigManager promotes flexibility and decouples the test from specific implementations. This is a good practice for enhancing configurability.

config/config-nacos/src/test/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManagerTest.java (1)

32-33: Consistent Use of Factory Method for Instantiation.

The instantiation of nacosDynamicConfigManager using DynamicConfigManagerFactory.getDynamicManager is consistent with best practices, ensuring flexibility and decoupling from specific implementations.

config/config-zk/src/test/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManagerTest.java (1)

32-33: Adoption of Factory Pattern for Instantiation.

The use of DynamicConfigManagerFactory.getDynamicManager for creating zookeeperDynamicConfigManager instances aligns with the factory pattern, enhancing the test's flexibility and maintainability.

config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (6)

17-47: Imports and Class Definition Approved.

The imports and class definition are appropriate for integrating Zookeeper as a configuration center.


110-118: Method Approved: getProviderServiceProperty.

The method is implemented correctly and handles exceptions gracefully.


120-128: Method Approved: getConsumerServiceProperty.

The method is implemented correctly and handles exceptions gracefully.


130-138: Method Approved: getProviderMethodProperty.

The method is implemented correctly and handles exceptions gracefully.


140-148: Method Approved: getConsumerMethodProperty.

The method is implemented correctly and handles exceptions gracefully.


51-51: Make zkClient Final.

The zkClient field could be made final to ensure it is not reassigned after initialization.

- private final CuratorFramework zkClient;
+ private final CuratorFramework zkClient;

Likely invalid or redundant comment.

core/common/src/main/java/com/alipay/sofa/rpc/common/config/RpcConfigKeys.java (1)

144-159: New Configuration Keys Approved.

The new configuration keys ZK_ADDRESS and NACOS_ADDRESS are well-defined and enhance the framework's configuration flexibility.

Comment on lines 105 to 108
@Override
public void initServiceConfiguration(String service) {
//TODO not now
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Reminder: Implement Service Configuration Initialization.

The initServiceConfiguration method is currently a placeholder. Ensure to implement the service configuration initialization in the future.

Do you want me to generate the implementation code or open a GitHub issue to track this task?

Comment on lines 51 to 59
private final CuratorFramework zkClient;
private static final String ADDRESS = SofaConfigs
.getOrDefault(RpcConfigKeys.ZK_ADDRESS);
private static final String DEFAULT_NAMESPACE = "sofa-rpc";
private static final String CONFIG_NODE = "config";
private static final String DEFAULT_APP = "sofa-rpc";
private final String appName;
private final String rootPath;
private ConcurrentMap<String, String> configMap = new ConcurrentHashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider Using SofaConfigs#getOrDefault for Default Values.

The DEFAULT_NAMESPACE and DEFAULT_APP values are hardcoded. Consider using SofaConfigs#getOrDefault for these values to allow for configuration flexibility.

- private static final String DEFAULT_NAMESPACE = "sofa-rpc";
- private static final String DEFAULT_APP = "sofa-rpc";
+ private static final String DEFAULT_NAMESPACE = SofaConfigs.getOrDefault(RpcConfigKeys.ZK_NAMESPACE, "sofa-rpc");
+ private static final String DEFAULT_APP = SofaConfigs.getOrDefault(RpcConfigKeys.ZK_APP, "sofa-rpc");
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final CuratorFramework zkClient;
private static final String ADDRESS = SofaConfigs
.getOrDefault(RpcConfigKeys.ZK_ADDRESS);
private static final String DEFAULT_NAMESPACE = "sofa-rpc";
private static final String CONFIG_NODE = "config";
private static final String DEFAULT_APP = "sofa-rpc";
private final String appName;
private final String rootPath;
private ConcurrentMap<String, String> configMap = new ConcurrentHashMap<>();
private final CuratorFramework zkClient;
private static final String ADDRESS = SofaConfigs
.getOrDefault(RpcConfigKeys.ZK_ADDRESS);
private static final String DEFAULT_NAMESPACE = SofaConfigs.getOrDefault(RpcConfigKeys.ZK_NAMESPACE, "sofa-rpc");
private static final String DEFAULT_APP = SofaConfigs.getOrDefault(RpcConfigKeys.ZK_APP, "sofa-rpc");
private static final String CONFIG_NODE = "config";
private static final String DEFAULT_APP = "sofa-rpc";
private final String appName;
private final String rootPath;
private ConcurrentMap<String, String> configMap = new ConcurrentHashMap<>();

@sofastack-cla sofastack-cla bot added size/XXL and removed size/XL labels Sep 23, 2024
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Outside diff range and nitpick comments (3)
core/api/src/main/java/com/alipay/sofa/rpc/listener/ConfigListener.java (1)

30-37: Update the method documentation to English for consistency.

The method documentation is currently in Chinese, which is inconsistent with the rest of the codebase. Please update the documentation to English to maintain consistency and improve readability for non-Chinese speakers.

Apply this diff to update the method documentation:

-    /**
-     * 处理配置变更事件
-     *
-     * @param event 配置变更事件
-     */
+    /**
+     * Process the configuration changed event.
+     *
+     * @param event the configuration changed event
+     */
config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (1)

117-121: Implement Listener Removal Mechanism

The addListener method adds listeners for configuration changes but there is no corresponding method to remove them. Without a removal mechanism, this could lead to memory leaks in long-running applications if listeners are not managed properly.

Consider adding a removeListener method to allow for the proper cleanup of listeners when they are no longer needed.

config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1)

160-160: Enhance error logs with appName for better context

Including the application name in the error logs can provide more context during troubleshooting. This small addition can significantly aid in identifying issues in multi-application environments.

Apply this diff to improve the log messages:

- LOGGER.error("Failed to getConfig for key:{}, group:{}", key, group, e);
+ LOGGER.error("Failed to getConfig for appName:{}, key:{}, group:{}", getAppName(), key, group, e);

- LOGGER.error("Failed to add listener for key:{}, group:{}", key, group, e);
+ LOGGER.error("Failed to add listener for appName:{}, key:{}, group:{}", getAppName(), key, group, e);

Also applies to: 173-173

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 528d0fb and 853ed3f.

Files selected for processing (13)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (5 hunks)
  • config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • core-impl/client/src/main/java/com/alipay/sofa/rpc/client/AbstractCluster.java (2 hunks)
  • core-impl/client/src/main/java/com/alipay/sofa/rpc/client/lb/AutoLoadBalancer.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangeType.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (3 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManagerFactory.java (2 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/listener/ConfigListener.java (2 hunks)
  • core/common/src/main/java/com/alipay/sofa/rpc/common/utils/StringUtils.java (1 hunks)
Additional comments not posted (22)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangeType.java (1)

24-39: LGTM!

The ConfigChangeType enum is well-defined and follows best practices:

  • The constant names are clear and self-explanatory.
  • Each constant is accompanied by a concise and accurate description.
  • The enum provides a structured way to represent different types of configuration changes.

Great job!

core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (5)

27-35: LGTM!

The class declaration and properties are well-defined and capture the necessary information for a configuration change event. Extending EventObject is appropriate, and declaring the properties as final ensures immutability.


37-47: LGTM!

The constructors are well-designed, offering both convenience and flexibility. Initializing the super constructor with a composite string of key and group uniquely identifies the event source, which is a good practice.


49-63: LGTM!

The getter methods are straightforward and follow the naming convention, allowing access to all property values.


65-73: LGTM!

Overriding the toString method to provide a meaningful string representation of the event, including all property values, aids in debugging and logging.


75-93: LGTM!

Overriding the equals and hashCode methods is necessary for proper equality comparison and hash-based collections. The equals method correctly compares the events based on their property values, and the hashCode method generates a consistent hash code.

core-impl/client/src/main/java/com/alipay/sofa/rpc/client/lb/AutoLoadBalancer.java (1)

56-56: LGTM!

The comment update clarifies the context of the dynamic configuration priority, specifying that it is at the request level. This aligns with the existing code logic and helps in understanding the behavior.

core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (7)

19-20: LGTM!

The import statement for com.alipay.sofa.common.config.ConfigKey is necessary and looks good.


26-27: LGTM!

The constant DYNAMIC_ALIAS is declared correctly and follows the existing naming convention.


28-29: LGTM!

The constant DYNAMIC_URL is declared correctly and follows the existing naming convention.


30-35: LGTM!

The constants CONFIG_NODE, DEFAULT_NAMESPACE, and DEFAULT_GROUP are declared correctly and follow the existing naming conventions. The default values for namespace and group are appropriately set to "sofa-rpc".


36-43: LGTM!

The ConfigKey<String> instance ZK_ADDRESS is created correctly with appropriate parameters. The key name follows the naming convention, and the default value points to a local Zookeeper instance, which is suitable for development environments. The description and alias are provided for clarity and backward compatibility.


44-50: LGTM!

The ConfigKey<String> instance NACOS_ADDRESS is created correctly with appropriate parameters. The key name follows the naming convention, and the default value points to a local Nacos instance, which is suitable for development environments. The description and alias are provided for clarity and backward compatibility.


51-58: LGTM!

The ConfigKey<String> instance APOLLO_ADDRESS is created correctly with appropriate parameters. The key name follows the naming convention, and the default value points to a local Apollo instance, which is suitable for development environments. The description and alias are provided for clarity and backward compatibility.

core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (3)

21-21: LGTM!

The import statement for ConfigListener is necessary for the newly added addListener method.


37-40: LGTM!

The addition of the protected getAppName() method is a good practice, as it provides controlled access to the appName field for subclasses.


94-101: LGTM!

The addition of the abstract methods addListener and getConfig is a good design choice, as they provide a way to manage configuration listeners and retrieve configuration values. The methods are well-documented and have clear parameters and return types.

These methods will need to be implemented by concrete subclasses, such as ZookeeperDynamicConfigManager and NacosDynamicConfigManager, to provide the actual functionality for adding listeners and retrieving configurations.

Also applies to: 103-109

core/common/src/main/java/com/alipay/sofa/rpc/common/utils/StringUtils.java (1)

42-45: LGTM!

The new constant KEY_SEPARATOR is declared correctly and follows the naming convention. It provides a centralized way to reference the separator, making the code more maintainable.

core-impl/client/src/main/java/com/alipay/sofa/rpc/client/AbstractCluster.java (1)

Line range hint 689-707: LGTM!

The comment update clarifies the scope of the dynamic configuration and the addition of a blank line enhances readability. The changes do not affect the logic or behavior of the method.

core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManagerFactory.java (2)

101-105: Reevaluate the warning threshold for dynamic managers

The code logs a warning if ALL_DYNAMICS.size() > 3. This arbitrary threshold may not be suitable for all use cases. In environments where more than three dynamic managers are normal, this could lead to unnecessary warnings.

Consider verifying whether this threshold is appropriate for your application's deployment scenarios.


121-123: Avoid catching Throwable

Catching Throwable can inadvertently catch serious errors that shouldn't be handled at this level, such as OutOfMemoryError. It's better practice to catch Exception to handle recoverable exceptions.

Update the catch block to handle Exception instead:

-    } catch (Throwable e) {
+    } catch (Exception e) {

Likely invalid or redundant comment.

bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (1)

31-32: Necessary imports added for dynamic configuration

The imports for ConfigChangedEvent and ConfigChangeType are appropriately added to support handling dynamic configuration events.

Comment on lines 95 to 97
public static DynamicConfigManager getDynamicManagerWithUrl(String appName, String url) {

String[] urlValues = url.split("://", 2);
Copy link
Contributor

Choose a reason for hiding this comment

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

Handle potential ArrayIndexOutOfBoundsException when parsing URL

The method getDynamicManagerWithUrl splits the url using url.split("://", 2) but does not validate the result. If the url does not contain "://", accessing urlValues[1] will lead to an ArrayIndexOutOfBoundsException.

Apply this diff to add input validation:

 public static DynamicConfigManager getDynamicManagerWithUrl(String appName, String url) {

+    if (url == null || !url.contains("://")) {
+        throw new IllegalArgumentException("Invalid URL format. Expected format: alias://address");
+    }

     String[] urlValues = url.split("://", 2);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static DynamicConfigManager getDynamicManagerWithUrl(String appName, String url) {
String[] urlValues = url.split("://", 2);
public static DynamicConfigManager getDynamicManagerWithUrl(String appName, String url) {
if (url == null || !url.contains("://")) {
throw new IllegalArgumentException("Invalid URL format. Expected format: alias://address");
}
String[] urlValues = url.split("://", 2);

Comment on lines 95 to 127
public static DynamicConfigManager getDynamicManagerWithUrl(String appName, String url) {

String[] urlValues = url.split("://", 2);

classLock.lock();
try {
if (ALL_DYNAMICS.size() > 3) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Size of dynamic manager is greater than 3, Please check it!");
}
}
DynamicConfigManager registry = ALL_DYNAMICS.get(urlValues[0]);
if (registry == null) {
ExtensionClass<DynamicConfigManager> ext = ExtensionLoaderFactory.getExtensionLoader(
DynamicConfigManager.class)
.getExtensionClass(urlValues[0]);
if (ext == null) {
throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager",
urlValues[0]));
}
registry = ext.getExtInstance(new Class[] { String.class, String.class }, new Object[] { appName,
urlValues[1] });
ALL_DYNAMICS.put(urlValues[0], registry);
}
return registry;
} catch (Throwable e) {
throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager",
urlValues[0]),
e);
} finally {
classLock.unlock();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Optimize synchronization for better concurrency

Currently, the method uses a Lock to ensure thread safety when accessing ALL_DYNAMICS. This can lead to contention and reduced concurrency. Utilizing ConcurrentMap methods like computeIfAbsent can simplify the code and improve performance.

Consider refactoring the method as follows:

 public static DynamicConfigManager getDynamicManagerWithUrl(String appName, String url) {

+    if (url == null || !url.contains("://")) {
+        throw new IllegalArgumentException("Invalid URL format. Expected format: alias://address");
+    }

     String[] urlValues = url.split("://", 2);

-    classLock.lock();
-    try {
-        if (ALL_DYNAMICS.size() > 3) {
-            if (LOGGER.isWarnEnabled()) {
-                LOGGER.warn("Size of dynamic manager is greater than 3, Please check it!");
-            }
-        }
-        DynamicConfigManager registry = ALL_DYNAMICS.get(urlValues[0]);
-        if (registry == null) {
-            ExtensionClass<DynamicConfigManager> ext = ExtensionLoaderFactory.getExtensionLoader(
-                DynamicConfigManager.class)
-                .getExtensionClass(urlValues[0]);
-            if (ext == null) {
-                throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager",
-                    urlValues[0]));
-            }
-            registry = ext.getExtInstance(new Class[] { String.class, String.class }, new Object[] { appName,
-                    urlValues[1] });
-            ALL_DYNAMICS.put(urlValues[0], registry);
-        }
-        return registry;
-    } catch (Throwable e) {
-        throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager",
-            urlValues[0]),
-            e);
-    } finally {
-        classLock.unlock();
-    }

+    if (ALL_DYNAMICS.size() > 3 && LOGGER.isWarnEnabled()) {
+        LOGGER.warn("Size of dynamic manager is greater than 3, Please check it!");
+    }
+
+    try {
+        return ALL_DYNAMICS.computeIfAbsent(urlValues[0], key -> {
+            ExtensionClass<DynamicConfigManager> ext = ExtensionLoaderFactory.getExtensionLoader(
+                DynamicConfigManager.class).getExtensionClass(key);
+            if (ext == null) {
+                throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager", key));
+            }
+            return ext.getExtInstance(new Class[] { String.class, String.class }, new Object[] { appName, urlValues[1] });
+        });
+    } catch (Exception e) {
+        throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager",
+            urlValues[0]), e);
+    }
 }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static DynamicConfigManager getDynamicManagerWithUrl(String appName, String url) {
String[] urlValues = url.split("://", 2);
classLock.lock();
try {
if (ALL_DYNAMICS.size() > 3) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Size of dynamic manager is greater than 3, Please check it!");
}
}
DynamicConfigManager registry = ALL_DYNAMICS.get(urlValues[0]);
if (registry == null) {
ExtensionClass<DynamicConfigManager> ext = ExtensionLoaderFactory.getExtensionLoader(
DynamicConfigManager.class)
.getExtensionClass(urlValues[0]);
if (ext == null) {
throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager",
urlValues[0]));
}
registry = ext.getExtInstance(new Class[] { String.class, String.class }, new Object[] { appName,
urlValues[1] });
ALL_DYNAMICS.put(urlValues[0], registry);
}
return registry;
} catch (Throwable e) {
throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager",
urlValues[0]),
e);
} finally {
classLock.unlock();
}
}
public static DynamicConfigManager getDynamicManagerWithUrl(String appName, String url) {
if (url == null || !url.contains("://")) {
throw new IllegalArgumentException("Invalid URL format. Expected format: alias://address");
}
String[] urlValues = url.split("://", 2);
if (ALL_DYNAMICS.size() > 3 && LOGGER.isWarnEnabled()) {
LOGGER.warn("Size of dynamic manager is greater than 3, Please check it!");
}
try {
return ALL_DYNAMICS.computeIfAbsent(urlValues[0], key -> {
ExtensionClass<DynamicConfigManager> ext = ExtensionLoaderFactory.getExtensionLoader(
DynamicConfigManager.class).getExtensionClass(key);
if (ext == null) {
throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager", key));
}
return ext.getExtInstance(new Class[] { String.class, String.class }, new Object[] { appName, urlValues[1] });
});
} catch (Exception e) {
throw new SofaRpcRuntimeException(LogCodes.getLog(LogCodes.ERROR_LOAD_EXT, "DynamicConfigManager",
urlValues[0]), e);
}
}

Comment on lines 67 to 71
protected ApolloDynamicConfigManager(String appName,String address) {
super(appName);
System.setProperty(APOLLO_APPID_KEY, appName);
System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + address);
config = ConfigService.getConfig(DynamicConfigKeys.DEFAULT_GROUP);
Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid Using System.setProperty in Constructors

Setting system properties within a constructor can have unintended side effects, as system properties are global to the JVM and affect all parts of the application. This may lead to unpredictable behavior, especially in multi-threaded environments or when multiple instances are created.

Consider refactoring the code to avoid setting system properties in the constructor. Instead, pass the necessary configuration directly to the components that require it.

Comment on lines 123 to 154
public class ApolloListener implements ConfigChangeListener {

private Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();

ApolloListener() {}

@Override
public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
if ("".equals(change.getNewValue())) {
LOGGER.info("an empty rule is received for key: " + key);
return;
}

ConfigChangedEvent event =
new ConfigChangedEvent(key, change.getNamespace(), change.getNewValue(), getChangeType(change));
listeners.forEach(listener -> listener.process(event));
}
}

private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
}
return ConfigChangeType.MODIFIED;
}

void addListener(ConfigListener configListener) {
this.listeners.add(configListener);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure Thread Safety When Managing Listeners

The listeners set in the ApolloListener class uses CopyOnWriteArraySet for thread safety. While this is suitable for scenarios with infrequent mutations and frequent iterations, it may not be the most efficient choice if listeners are added or removed frequently.

Evaluate whether ConcurrentHashMap or another thread-safe collection would be more appropriate, especially if listener additions and removals occur often.

Comment on lines 133 to 135
if ("".equals(change.getNewValue())) {
LOGGER.info("an empty rule is received for key: " + key);
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Incorrect Use of return Inside Loop

Using return inside the loop in the onChange method will exit the entire method when an empty new value is encountered. This prevents processing of subsequent keys, which may lead to missed configuration changes.

Apply this diff to fix the issue:

- if ("".equals(change.getNewValue())) {
    LOGGER.info("an empty rule is received for key: " + key);
-   return;
+   continue;
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ("".equals(change.getNewValue())) {
LOGGER.info("an empty rule is received for key: " + key);
return;
if ("".equals(change.getNewValue())) {
LOGGER.info("an empty rule is received for key: " + key);
continue;
}

Comment on lines 166 to 177
//启用接口级别动态配置
final String dynamicUrl = consumerConfig.getParameter(DynamicConfigKeys.DYNAMIC_URL);
if (StringUtils.isNotBlank(dynamicUrl)) {
final DynamicConfigManager dynamicManager = DynamicConfigManagerFactory.getDynamicManagerWithUrl(
consumerConfig.getAppName(), dynamicUrl);

//build listeners for dynamic config
consumerConfig.setConfigListener(buildConfigListener(this,dynamicManager));
}



Copy link
Contributor

Choose a reason for hiding this comment

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

Overwriting existing ConfigListener may lead to unintended behavior

In the interface-level dynamic configuration, you set a new ConfigListener on the consumerConfig using:

consumerConfig.setConfigListener(buildConfigListener(this, dynamicManager));

Since consumerConfig may already have a ConfigListener set earlier in the code, this operation could overwrite the existing listener, potentially disrupting previously established configurations or listeners.

Consider modifying the implementation to support multiple ConfigListeners or ensure that you're not unintentionally overwriting an existing listener. You might implement a mechanism to aggregate listeners or check if a listener is already set before assigning a new one.

Comment on lines 217 to 227
/**
* Build ConfigListener for consumer bootstrap with dynamic config.
*
* @param bootstrap ConsumerBootstrap
* @param dynamicManager DynamicConfigManager
* @return ConfigListener
*/
protected ConfigListener buildConfigListener(ConsumerBootstrap bootstrap, DynamicConfigManager dynamicManager) {
return new ConsumerAttributeListener(dynamicManager);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Clarify the usage of overloaded buildConfigListener methods

The introduction of an overloaded buildConfigListener method adds flexibility, but it may cause confusion due to the similar method names and parameters:

  • protected ConfigListener buildConfigListener(ConsumerBootstrap bootstrap)
  • protected ConfigListener buildConfigListener(ConsumerBootstrap bootstrap, DynamicConfigManager dynamicManager)

To improve code readability and maintainability, consider renaming one of the methods to more clearly reflect its purpose, such as buildDynamicConfigListener. This differentiation helps future developers understand the distinction without examining method signatures.

Comment on lines 474 to 481
public void initWith(String key, DynamicConfigManager dynamicManager) {
dynamicManager.addListener(key, this);
// 初始化配置值
String rawConfig = dynamicManager.getConfig(key);
if (!StringUtils.isEmpty(rawConfig)) {
process(new ConfigChangedEvent(key, "sofa-rpc",rawConfig));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure listeners are unregistered to prevent memory leaks

In the initWith method, the ConsumerAttributeListener registers itself with the DynamicConfigManager:

dynamicManager.addListener(key, this);

However, there is no corresponding logic to remove this listener when it is no longer needed. This might lead to memory leaks if the listener remains registered after the consumer is unreferenced.

Consider implementing a mechanism to unregister the listener when the consumer is unreferenced or destroyed. This can be done in the unRefer method by calling a hypothetical removeListener method:

dynamicManager.removeListener(key, this);

Comment on lines 483 to 501
@Override
public void process(ConfigChangedEvent event){
if (event.getChangeType().equals(ConfigChangeType.DELETED)) {
newValueMap = null;
} else {
// ADDED or MODIFIED
String[] lines = event.getContent().split("\n");
for (String line : lines) {
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
newValueMap.put(key, value);
}
}
}
attrUpdated(newValueMap);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Add error handling for configuration parsing in process method

In the process method, the configuration content is split and parsed without validating the input:

String[] lines = event.getContent().split("\n");
for (String line : lines) {
    String[] keyValue = line.split("=", 2);
    if (keyValue.length == 2) {
        String key = keyValue[0].trim();
        String value = keyValue[1].trim();
        newValueMap.put(key, value);
    }
}

If the configuration content is malformed, this could lead to incorrect behavior or missed configurations.

Consider adding error handling to manage malformed lines gracefully. For example, log a warning when a line doesn't conform to the expected key=value format:

for (String line : lines) {
    String[] keyValue = line.split("=", 2);
    if (keyValue.length == 2) {
        String key = keyValue[0].trim();
        String value = keyValue[1].trim();
        newValueMap.put(key, value);
    } else {
        LOGGER.warn("Malformed configuration line: {}", line);
    }
}

@Override
public void configChanged(Map newValue) {
public void configChanged(Map newValueMap) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused configChanged method with empty implementation

The method public void configChanged(Map newValueMap) is declared but has an empty body:

public void configChanged(Map newValueMap) {

}

Since it doesn't perform any actions and isn't called within this class, it may be unnecessary.

If this method is not required, consider removing it to clean up the code. If it's intended for future use or to fulfill an interface contract, consider adding a comment explaining its purpose.

@Narzisss Narzisss changed the title integrate Zookeeper and Nacos as configuration centers Optimize dynamic config: integrate Zookeeper & Nacos, support interface-level dynamic config Sep 23, 2024
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (2)
core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (2)

932-935: Use a more explicit indicator for resetting to default values

Using a single space " " as an indicator to reset to the default value may not be intuitive and can lead to confusion. Consider defining a constant or using a more descriptive value to improve code readability and maintainability.

Define a constant for the reset indicator:

private static final String RESET_TO_DEFAULT = " ";

And use it in the condition:

if (RESET_TO_DEFAULT.equals(newValueStr)) {
    // Reset to default value
}

Alternatively, use a clearer value like "DEFAULT":

private static final String RESET_TO_DEFAULT = "DEFAULT";

if (RESET_TO_DEFAULT.equals(newValueStr)) {
    // Reset to default value
}

1035-1041: Unnecessary null check for dynamicConfigValueCache

Since dynamicConfigValueCache is initialized during declaration and is not set to null elsewhere, the null check if (dynamicConfigValueCache != null) is unnecessary and can be removed to simplify the code.

Apply this diff to remove the unnecessary null check:

- if (dynamicConfigValueCache != null){
    return dynamicConfigValueCache.get(key);
- }
+ return dynamicConfigValueCache.get(key);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 853ed3f and b4ab414.

📒 Files selected for processing (8)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (8 hunks)
  • config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (5 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java
🔇 Additional comments (8)
config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (6)

19-37: LGTM: New imports are appropriate for the changes.

The added imports are relevant to the new functionality implemented in the class, including configuration management, logging, and concurrent data structures.


49-49: Specify Access Modifier and Make LOGGER Static Final

The LOGGER variable is declared without an access modifier and is not static or final. It's a best practice to declare loggers as private static final to ensure they are class-level constants and not modifiable.

Apply this diff to correct it:

- Logger LOGGER = LoggerFactory.getLogger(ApolloDynamicConfigManager.class);
+ private static final Logger LOGGER = LoggerFactory.getLogger(ApolloDynamicConfigManager.class);

51-55: LGTM: Well-defined constants for Apollo configuration.

The new constants are appropriately named and provide clear references for Apollo-specific configuration keys and protocol prefix.


59-59: LGTM: Thread-safe map for storing listeners.

The watchListenerMap is correctly declared as a ConcurrentMap, which is suitable for managing listeners in a thread-safe manner.


76-80: LGTM: Well-implemented service configuration initialization.

The initServiceConfiguration method correctly retrieves the service configuration and triggers the listener if a non-empty configuration is found.


115-120: LGTM: Efficient and thread-safe listener management.

The addListener method effectively manages listeners using the watchListenerMap. It ensures thread-safety by using computeIfAbsent and adds the listener to Apollo's configuration change mechanism.

core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (2)

218-222: Appropriate addition of dynamic configuration cache

The new dynamicConfigValueCache field is correctly declared and initialized using ConcurrentHashMap, ensuring thread-safe access to dynamic configuration values.


973-977: Correctly updating dynamic configuration cache

The code appropriately updates dynamicConfigValueCache by adding or removing the property based on the new value, ensuring that dynamic configurations are accurately reflected.

Comment on lines 61 to 85
protected ApolloDynamicConfigManager(String appName) {
super(appName);
super(appName, SofaConfigs.getOrDefault(DynamicConfigKeys.APOLLO_ADDRESS));
System.setProperty(APOLLO_APPID_KEY, appName);
System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + getAddress());
config = ConfigService.getAppConfig();
}

protected ApolloDynamicConfigManager(String appName, String address) {
super(appName, address);
System.setProperty(APOLLO_APPID_KEY, appName);
System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + address);
config = ConfigService.getAppConfig();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Reconsider setting system properties in constructors

Setting system properties within constructors can lead to unexpected behavior, especially in multi-threaded environments or when multiple instances are created. Consider refactoring to avoid this practice.

Suggestion:

  1. Create a separate method for initializing Apollo configuration.
  2. Call this method from the constructors or externally after object creation.

Example refactoring:

private void initApolloConfig(String appName, String address) {
    System.setProperty(APOLLO_APPID_KEY, appName);
    System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + address);
    config = ConfigService.getAppConfig();
}

Update constructors to use this method:

protected ApolloDynamicConfigManager(String appName) {
    super(appName, SofaConfigs.getOrDefault(DynamicConfigKeys.APOLLO_ADDRESS));
    initApolloConfig(appName, getAddress());
}

protected ApolloDynamicConfigManager(String appName, String address) {
    super(appName, address);
    initApolloConfig(appName, address);
}

This approach provides more flexibility and reduces potential side effects.

Comment on lines 122 to 167
public class ApolloListener implements ConfigChangeListener {

private Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();

ApolloListener() {
}

@Override
public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
if ("".equals(change.getNewValue())) {
LOGGER.info("an empty rule is received for key: " + key);
return;
}

ConfigChangedEvent event =
new ConfigChangedEvent(key, change.getNewValue(), getChangeType(change));
listeners.forEach(listener -> listener.process(event));
}
}

private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
}
return ConfigChangeType.MODIFIED;
}

void addListener(ConfigListener configListener) {
this.listeners.add(configListener);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix premature return in onChange method

The onChange method in the ApolloListener class has an issue that can lead to missed configuration changes. When an empty rule is encountered, the method returns prematurely, preventing the processing of subsequent keys.

Apply this diff to fix the issue:

@Override
public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
    for (String key : changeEvent.changedKeys()) {
        ConfigChange change = changeEvent.getChange(key);
        if ("".equals(change.getNewValue())) {
            LOGGER.info("an empty rule is received for key: " + key);
-           return;
+           continue;
        }

        ConfigChangedEvent event =
                new ConfigChangedEvent(key, change.getNewValue(), getChangeType(change));
        listeners.forEach(listener -> listener.process(event));
    }
}

This change ensures that all changed keys are processed, even if some have empty values.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class ApolloListener implements ConfigChangeListener {
private Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();
ApolloListener() {
}
@Override
public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
if ("".equals(change.getNewValue())) {
LOGGER.info("an empty rule is received for key: " + key);
return;
}
ConfigChangedEvent event =
new ConfigChangedEvent(key, change.getNewValue(), getChangeType(change));
listeners.forEach(listener -> listener.process(event));
}
}
private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
}
return ConfigChangeType.MODIFIED;
}
void addListener(ConfigListener configListener) {
this.listeners.add(configListener);
}
}
public class ApolloListener implements ConfigChangeListener {
private Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();
ApolloListener() {
}
@Override
public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
if ("".equals(change.getNewValue())) {
LOGGER.info("an empty rule is received for key: " + key);
continue;
}
ConfigChangedEvent event =
new ConfigChangedEvent(key, change.getNewValue(), getChangeType(change));
listeners.forEach(listener -> listener.process(event));
}
}
private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
}
return ConfigChangeType.MODIFIED;
}
void addListener(ConfigListener configListener) {
this.listeners.add(configListener);
}
}

Comment on lines 932 to 935
if(newValueStr.equals(" ")){
// 改为默认值
newValueStr = configValueCache.get(property) == null ? null : configValueCache.get(property).toString();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential NullPointerException when comparing newValueStr

In the condition if(newValueStr.equals(" ")), if newValueStr is null, this will result in a NullPointerException. Consider reversing the comparison or adding a null check to prevent this exception.

Apply this diff to fix the potential NullPointerException:

- if(newValueStr.equals(" ")){
+ if(" ".equals(newValueStr)){

Alternatively, add a null check:

+ if(newValueStr != null && newValueStr.equals(" ")){
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(newValueStr.equals(" ")){
// 改为默认值
newValueStr = configValueCache.get(property) == null ? null : configValueCache.get(property).toString();
}
if(" ".equals(newValueStr)){
// 改为默认值
newValueStr = configValueCache.get(property) == null ? null : configValueCache.get(property).toString();
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 29

🧹 Outside diff range and nitpick comments (16)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (1)

34-43: Constructors are well-implemented, but consider revising the event source.

The constructors are well-designed:

  • They provide flexibility in creating the event object.
  • The use of this() to call the more specific constructor is a good practice.
  • All fields are properly initialized.

However, there's a potential improvement:

Consider revising the super(key) call in the constructor. Using the key as the source of the event might not be the most appropriate choice. The source typically represents the object that generated the event, which in this case might be a configuration manager or similar object. Consider adding a source parameter to the constructor and using it in the super() call.

Example:

public ConfigChangedEvent(Object source, String key, String content, ConfigChangeType changeType) {
    super(source);
    this.key = key;
    this.content = content;
    this.changeType = changeType;
}
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/base/BaseZkTest.java (1)

29-34: Enhance the class Javadoc comment.

While the Javadoc comment is present, it could be more informative. Consider adding details about the purpose of this base class and how it should be used in integration tests.

Here's a suggested improvement:

/**
 * Base class for ZooKeeper integration tests.
 * This class provides setup and teardown methods for a TestingServer instance,
 * which simulates a ZooKeeper server for testing purposes.
 *
 * @author Narziss
 * @version BaseZkTest.java, v 0.1 2024年10月08日 10:20 Narziss
 */
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (3)

19-31: Consider adding blank lines between import groups for better readability.

While the imports are correct and logically grouped, adding blank lines between different package imports can improve readability. Consider reorganizing the imports as follows:

import com.alipay.sofa.rpc.config.ApplicationConfig;
import com.alipay.sofa.rpc.config.ConsumerConfig;
import com.alipay.sofa.rpc.dynamic.DynamicConfigKeys;
import com.alipay.sofa.rpc.dynamic.DynamicConfigManager;
import com.alipay.sofa.rpc.dynamic.DynamicConfigManagerFactory;
import com.alipay.sofa.rpc.dynamic.nacos.NacosDynamicConfigManager;
import com.alipay.sofa.rpc.test.HelloService;

import org.junit.Assert;
import org.junit.Test;

import java.lang.reflect.Field;
import java.util.Map;

33-37: Enhance the class Javadoc for better documentation.

The current Javadoc is minimal. Consider expanding it to provide more context about the purpose of this test class and what aspects of Nacos dynamic configuration it's testing. For example:

/**
 * Integration test class for Nacos dynamic configuration in SOFA RPC.
 * This class tests the functionality of dynamic configuration updates
 * using Nacos as the configuration center.
 *
 * @author Narziss
 * @version NacosDynamicConfigTest.java, v 0.1 2024年09月28日 12:11 Narziss
 */

1-72: Overall assessment: Functional but needs refinement.

This new test file for Nacos dynamic configuration provides valuable coverage for the feature. However, several areas need attention to improve its robustness and maintainability:

  1. Enhance documentation with more detailed class and method Javadocs.
  2. Improve test setup and teardown to ensure isolation and proper resource management.
  3. Reduce coupling with implementation details by avoiding reflection and considering the use of mocking frameworks.
  4. Break down the large test method into smaller, more focused tests.
  5. Add more comprehensive assertions and error handling.

Addressing these points will significantly improve the quality and reliability of this test class.

core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (2)

28-29: LGTM: CONFIG_NODE constant added correctly.

The CONFIG_NODE constant is appropriately named and valued. However, consider adding a brief comment to explain its purpose for better code readability.

Consider adding a comment like this:

/**
 * Constant representing the configuration node identifier.
 */
public static final String CONFIG_NODE = "config";

30-31: LGTM: DEFAULT_NAMESPACE constant added correctly.

The DEFAULT_NAMESPACE constant is appropriately named and valued. However, consider adding a brief comment to explain its purpose and usage for better code readability.

Consider adding a comment like this:

/**
 * Constant representing the default namespace for SOFA RPC configurations.
 */
public static final String DEFAULT_NAMESPACE = "sofa-rpc";
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java (1)

57-59: Consider alternatives to reflection for accessing private fields.

While reflection is sometimes necessary in tests, it can make the test brittle if the internal structure of ZookeeperDynamicConfigManager changes. Consider if there's a way to expose the zkClient for testing purposes, perhaps through a protected method in the base test class.

test/test-integration/pom.xml (3)

282-287: LGTM. Consider documenting multiple configuration options.

The addition of ZooKeeper as another configuration management option is good for comprehensive testing. However, with multiple options (Nacos, Apollo, ZooKeeper), it would be helpful to document the purpose and usage of each in the project's README or a separate testing guide.

Could you add a comment in the POM file or update the project documentation to explain the usage of these multiple configuration options in the test scope?


288-293: LGTM. Consider using property for version management.

The addition of curator-test aligns well with the ZooKeeper configuration support. However, to maintain consistency and ease future updates, consider defining the version in a property. This approach is already used for other dependencies in this POM file.

Suggest adding a property for the Curator version:

<properties>
    <!-- other properties -->
    <curator.version>4.3.0</curator.version>
</properties>

<!-- In the dependency declaration -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-test</artifactId>
    <version>${curator.version}</version>
    <scope>test</scope>
</dependency>

Line range hint 209-293: Overall LGTM with suggestions for improvement.

The changes to the POM file indicate a shift towards supporting multiple configuration management options (Nacos, Apollo, ZooKeeper) for comprehensive testing. This is a good approach for ensuring compatibility and flexibility. However, there are a few areas that could be improved:

  1. Clarify the purpose of having both Nacos and Apollo dependencies, as this might lead to confusion.
  2. Consider adding documentation (either in the POM file or project README) explaining the usage and purpose of each configuration management option in the test scope.
  3. Implement consistent version management by using properties for all dependency versions, including the newly added curator-test.

These improvements will enhance the maintainability and clarity of the project's dependency management.

config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3)

90-91: Use English for code comments and handle unimplemented methods appropriately

The TODO comment // TODO 暂不支持 is in Chinese. For consistency and readability among all contributors, please use English for code comments.

Additionally, since the method initServiceConfiguration(String service) is not yet supported, consider throwing an UnsupportedOperationException to explicitly indicate that this method is unimplemented.

Apply this diff to update the comment and handle the unimplemented method:

@Override
public void initServiceConfiguration(String service) {
-    // TODO 暂不支持
+    // TODO Not supported yet
+    throw new UnsupportedOperationException("initServiceConfiguration(String service) is not supported yet.");
}

130-132: Use English for code comments and handle unimplemented methods appropriately

The TODO comment //TODO 暂不支持 is in Chinese. Please use English for code comments to ensure clarity for all contributors.

Since the method getServiceAuthRule(String service) is not implemented, consider throwing an UnsupportedOperationException to indicate that this functionality is not currently supported.

Apply this diff to update the comment and handle the unimplemented method:

@Override
public AuthRuleGroup getServiceAuthRule(String service) {
-    //TODO 暂不支持
+    // TODO Not supported yet
+    throw new UnsupportedOperationException("getServiceAuthRule(String service) is not supported yet.");
}

142-142: Specify access modifier and declare the field as final

The listeners field does not have an explicit access modifier and is not declared as final. For better encapsulation and thread safety, consider declaring it as private final.

Apply this diff to update the field declaration:

- Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();
+ private final Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();
config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (2)

201-205: Handle Exception Consistently When Starting NodeCache

In the constructor of ZookeeperConfigListener, when starting the nodeCache, exceptions are caught and logged. Ensure that the exception message provides clear context by including additional information, such as the path or reason for failure.

Consider enhancing the log message:

} catch (Exception e) {
-     LOGGER.error("Failed to add listener for path:{}", pathKey, e);
+     LOGGER.error("Failed to start NodeCache for path [{}]: {}", pathKey, e.getMessage(), e);
}

55-55: Consider Declaring configMap as final

The configMap is a ConcurrentMap that is initialized once and its reference does not change. Declaring it as final can make the code clearer by indicating that the reference will not be reassigned.

Apply the following change:

- private ConcurrentMap<String, String> configMap = new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, String> configMap = new ConcurrentHashMap<>();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between b4ab414 and 92351f2.

📒 Files selected for processing (13)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (7 hunks)
  • config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (5 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (4 hunks)
  • test/test-integration/pom.xml (2 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/base/BaseZkTest.java (1 hunks)
🧰 Additional context used
🔇 Additional comments (30)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (3)

26-32: LGTM: Well-structured event class with immutable fields.

The ConfigChangedEvent class is well-designed:

  • It extends EventObject, which is appropriate for an event class.
  • The use of private final fields (key, content, and changeType) ensures immutability, which is a good practice for event objects.

This structure will provide a robust and thread-safe representation of configuration change events.


45-55: LGTM: Getter methods are correctly implemented.

The getter methods getKey(), getContent(), and getChangeType() are well-implemented:

  • They provide access to the private fields, which is necessary for the event to be useful.
  • The methods are simple and straightforward, returning the corresponding field values.

This implementation maintains encapsulation while allowing necessary access to the event data.


1-56: 🛠️ Refactor suggestion

Consider adding toString() method and serialVersionUID for improved robustness.

The overall implementation of ConfigChangedEvent is solid. However, consider the following improvements:

  1. Implement a toString() method for easier debugging and logging.
  2. Add a serialVersionUID as the class is indirectly Serializable through EventObject.

Here's an example implementation:

private static final long serialVersionUID = 1L;

@Override
public String toString() {
    return "ConfigChangedEvent{" +
           "key='" + key + '\'' +
           ", content='" + content + '\'' +
           ", changeType=" + changeType +
           '}';
}

Also, please ensure that the ConfigChangeType enum is properly defined in the project. Run the following command to verify its existence:

✅ Verification successful

Recommendations for Enhancements in ConfigChangedEvent Class

The ConfigChangedEvent class is well-implemented. To further enhance its robustness and debuggability, consider the following improvements:

  1. Implement toString() Method

    • Facilitates easier debugging and logging by providing a string representation of the object.
  2. Add serialVersionUID

    • Since the class is indirectly Serializable through EventObject, defining a serialVersionUID ensures version compatibility during serialization.
private static final long serialVersionUID = 1L;

@Override
public String toString() {
    return "ConfigChangedEvent{" +
           "key='" + key + '\'' +
           ", content='" + content + '\'' +
           ", changeType=" + changeType +
           '}';
}
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify the existence of ConfigChangeType enum
rg --type java "enum\s+ConfigChangeType" -g '!ConfigChangedEvent.java'

Length of output: 176

test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/base/BaseZkTest.java (2)

17-27: LGTM: Package declaration and imports are appropriate.

The package name follows Java conventions, and the imports are relevant to the class's functionality. Good job on keeping the imports clean and focused.


1-62: Overall assessment: Good foundation with room for improvement.

The BaseZkTest class provides a solid foundation for ZooKeeper-based integration tests. It correctly sets up and tears down the test environment, which is crucial for reliable testing. However, there are opportunities to enhance its robustness and maintainability:

  1. Improve Javadoc comments for better documentation.
  2. Enhance error handling and logging in both setup and teardown methods.
  3. Consider using a random available port for the TestingServer to avoid potential conflicts.
  4. Ensure proper resource cleanup using try-with-resources or similar patterns.

Implementing these suggestions will make the class more resilient and easier to maintain in the long run.

test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (1)

1-17: LGTM: Proper license header and package declaration.

The file includes the correct Apache 2.0 license header and the package is properly declared.

core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (5)

19-20: LGTM: Import statement added correctly.

The import for com.alipay.sofa.common.config.ConfigKey is necessary for the newly added ConfigKey variables and is correctly placed at the beginning of the file.


32-38: LGTM: DYNAMIC_URL ConfigKey added correctly, but clarify default value.

The DYNAMIC_URL ConfigKey is well-defined with a clear description. However, the default value is set to a single space character (" "). Is this intentional, or should it be an empty string or null?

Could you please clarify the reasoning behind using a space as the default value for DYNAMIC_URL? If it's not intentional, consider changing it to an empty string ("") or null, whichever is more appropriate for your use case.


40-46: LGTM: ZK_ADDRESS ConfigKey added correctly.

The ZK_ADDRESS ConfigKey is well-defined with a clear description and a sensible default value for a local Zookeeper instance. The key follows a consistent naming convention with the other configuration centers.


48-54: LGTM: NACOS_ADDRESS ConfigKey added correctly.

The NACOS_ADDRESS ConfigKey is well-defined with a clear description and a sensible default value for a local Nacos instance. The key follows a consistent naming convention with the other configuration centers.


55-61: LGTM: APOLLO_ADDRESS ConfigKey added correctly, but clarify scope.

The APOLLO_ADDRESS ConfigKey is well-defined with a clear description and a sensible default value for a local Apollo instance. The key follows a consistent naming convention with the other configuration centers.

However, the PR objectives mentioned only Zookeeper and Nacos integration. Could you please clarify if Apollo integration is also part of this PR's scope? If so, consider updating the PR description to include this information.

Please confirm if Apollo integration is intended to be part of this PR. If it is, update the PR description to reflect this addition. If not, consider moving this ConfigKey to a separate PR focused on Apollo integration.

test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java (2)

1-37: LGTM: File structure and imports are well-organized.

The file has an appropriate license header, relevant imports, and proper class documentation. The structure follows good practices for test files.


40-79: LGTM: Overall test structure is well-designed.

The test method effectively sets up the environment, performs the necessary operations, and verifies the expected outcome. It covers the main functionality of Zookeeper dynamic configuration integration.

test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (3)

1-17: LGTM: File structure and package declaration are correct.

The file includes the appropriate license header and follows standard Java conventions for package declaration.


36-43: LGTM: Class and test method declarations are correct.

The ApolloDynamicConfigTest class and testApolloDynamicConfig method are properly declared following JUnit conventions.


19-34: Consider removing unused imports.

Some of the imported classes might not be used in the current implementation. It's a good practice to remove unused imports to keep the code clean and improve readability.

To verify unused imports, run the following command:

test/test-integration/pom.xml (1)

276-281: Clarify the need for both Nacos and Apollo dependencies.

Earlier, the Apollo dependency was replaced with Nacos. However, Apollo is now added back as a test dependency. This might lead to confusion. Could you please clarify the reason for including both Nacos and Apollo? If both are needed for different test scenarios, consider adding a comment explaining this.

#!/bin/bash
# Check for usage of both Nacos and Apollo in test files
echo "Files using Nacos:"
rg --type java "import.*com\.alibaba\.nacos" test/
echo "Files using Apollo:"
rg --type java "import.*com\.ctrip\.framework\.apollo" test/
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (2)

67-75: Overloaded 'initServiceConfiguration' method enhances flexibility

Adding the new method with ConfigListener parameter allows for more flexible service configuration initialization, enabling listeners to react to configuration changes.


121-129: Addition of 'addListener' method improves configuration management

The new addListener method allows registration of configuration listeners based on a specified key, enhancing the dynamic configuration capabilities of the system.

config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3)

62-66: Reconsider setting system properties in constructors

Setting system properties within constructors can lead to unexpected behavior, especially in multi-threaded environments or when multiple instances are created. This practice can cause side effects that are hard to track and debug.

Consider refactoring the code to avoid setting system properties in the constructor. Instead, initialize the necessary configurations within the components that require them or use a dedicated initialization method.


72-81: Reconsider setting system properties in constructors

Similar to the above, setting system properties in the constructor can introduce global side effects. It's advisable to refactor this code to prevent potential issues arising from the modification of system properties during object creation.


157-162: Handle all PropertyChangeType cases explicitly

In the getChangeType method, only DELETED and MODIFIED change types are handled, with all other types defaulting to MODIFIED. If the PropertyChangeType enum includes other values like ADDED, consider handling them explicitly to avoid unexpected behavior.

Run the following script to check if there are other PropertyChangeType values that need to be handled:

config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1)

51-51: Remove Redundant final Keyword

The field zkClient is already declared as final. This might be a typographical error in the code provided.

bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (4)

150-151: Overwriting existing ConfigListener may lead to unintended behavior


161-167: Missing ConfigListener for request-level dynamic configuration


464-488: Good addition of error handling for malformed configuration lines

The added logic in the process method to handle malformed configuration lines enhances the robustness of the system by logging warnings when a line doesn't conform to the expected key=value format.


491-491: Unused configChanged method with empty implementation

core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (3)

218-222: Addition of dynamicConfigValueCache for dynamic configuration

The new field dynamicConfigValueCache is appropriately added and initialized with a ConcurrentHashMap<>. This ensures thread-safe operations when handling dynamic configurations.


Line range hint 952-977: Proper management of method configurations in dynamic cache

The code correctly updates dynamicConfigValueCache when modifying method configurations. This ensures that dynamic changes are accurately reflected and retrieved.


1035-1044: Efficient retrieval of method configuration values

The method getMethodConfigValue now prioritizes dynamicConfigValueCache over configValueCache, which enhances the dynamic configuration retrieval process. The implementation is sound.

Comment on lines +36 to +46
@BeforeClass
public static void adBeforeClass() {
RpcRunningState.setUnitTestMode(true);

try {
server = new TestingServer(2181, true);
server.start();
} catch (Exception e) {
e.printStackTrace();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve robustness and error handling in adBeforeClass method.

While the method sets up the testing environment correctly, there are a few areas for improvement:

  1. The hardcoded port 2181 might cause issues if it's already in use. Consider using a random available port or making it configurable.
  2. The exception handling could be more informative. Instead of just printing the stack trace, consider logging a meaningful error message.

Here's a suggested improvement:

@BeforeClass
public static void adBeforeClass() throws Exception {
    RpcRunningState.setUnitTestMode(true);

    try {
        server = new TestingServer(0, true); // Use a random available port
        server.start();
        System.out.println("ZooKeeper TestingServer started on port: " + server.getPort());
    } catch (Exception e) {
        System.err.println("Failed to start ZooKeeper TestingServer: " + e.getMessage());
        throw e; // Rethrow to fail the test setup
    }
}

This version uses a random available port, provides more informative logging, and rethrows the exception to ensure test setup failure is properly handled.

Comment on lines +48 to +61
@AfterClass
public static void adAfterClass() {
RpcRuntimeContext.destroy();
RpcInternalContext.removeContext();
RpcInvokeContext.removeContext();

if (server != null) {
try {
server.stop();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance cleanup and error handling in adAfterClass method.

The method does a good job of cleaning up resources, but there are opportunities for improvement:

  1. Consider using try-with-resources for closing the TestingServer.
  2. Improve error handling and logging for better diagnostics.

Here's a suggested improvement:

@AfterClass
public static void adAfterClass() {
    try {
        RpcRuntimeContext.destroy();
        RpcInternalContext.removeContext();
        RpcInvokeContext.removeContext();

        if (server != null) {
            try {
                server.close();
                System.out.println("ZooKeeper TestingServer stopped successfully");
            } catch (IOException e) {
                System.err.println("Error stopping ZooKeeper TestingServer: " + e.getMessage());
            }
        }
    } finally {
        server = null; // Ensure the server reference is cleared even if an exception occurs
    }
}

This version provides more informative logging, uses close() instead of stop() (as close() is the preferred method in newer versions of TestingServer), and ensures the server reference is always cleared.

Comment on lines 39 to 52
@Test
public void testNacosDynamicConfig() throws Exception {
System.setProperty(DynamicConfigKeys.DYNAMIC_URL.getKey(), "nacos://127.0.0.1:8848");
ApplicationConfig clientApplication = new ApplicationConfig();
clientApplication.setAppName("demo");

ConsumerConfig<HelloService> consumerConfig = new ConsumerConfig<HelloService>()
.setInterfaceId(HelloService.class.getName())
.setProtocol("bolt")
.setDirectUrl("bolt://127.0.0.1:12200")
.setConnectTimeout(10 * 1000)
.setApplication(clientApplication);

consumerConfig.refer();
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider improving test setup and configuration.

While the basic setup is correct, there are a few areas for improvement:

  1. Setting system properties in a test method can affect other tests. Consider using @Before and @After methods to set up and clean up the system property.

  2. The hard-coded Nacos server address (127.0.0.1:8848) might not work in all environments. Consider making this configurable or using a mock server for testing.

  3. Ensure proper cleanup after the test. Add a method to reset the system property:

@After
public void cleanup() {
    System.clearProperty(DynamicConfigKeys.DYNAMIC_URL.getKey());
}
  1. Consider using @BeforeClass to set up shared resources if they're used across multiple test methods in this class.

Comment on lines 54 to 71
// 获取接口对应的动态配置监听器
DynamicConfigManager dynamicConfigManager = DynamicConfigManagerFactory.getDynamicManagerWithUrl
(clientApplication.getAppName(), System.getProperty(DynamicConfigKeys.DYNAMIC_URL.getKey()));
Field field = NacosDynamicConfigManager.class.getDeclaredField("watchListenerMap");
field.setAccessible(true);
Map<String, NacosDynamicConfigManager.NacosConfigListener> watchListenerMap = (Map<String, NacosDynamicConfigManager.NacosConfigListener>) field
.get(dynamicConfigManager);
NacosDynamicConfigManager.NacosConfigListener nacosConfigListener = watchListenerMap.get(consumerConfig
.getInterfaceId());

// 测试配置更新
String configValue = "timeout=5000";
nacosConfigListener.innerReceive(consumerConfig.getInterfaceId(), consumerConfig.getAppName(), configValue);
Assert.assertEquals(5000, consumerConfig.getMethodTimeout("sayHello"));
configValue = "timeout=5000\n.sayHello.timeout=6000";
nacosConfigListener.innerReceive(consumerConfig.getInterfaceId(), consumerConfig.getAppName(), configValue);
Assert.assertEquals(6000, consumerConfig.getMethodTimeout("sayHello"));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Refactor test to reduce coupling and improve robustness.

While the test logic is functional, there are several areas for improvement:

  1. Avoid using reflection to access private fields. This creates tight coupling with the implementation and can break if the internal structure changes. Consider exposing necessary methods for testing or use dependency injection to provide a testable configuration.

  2. Add error handling for reflection operations. If you must use reflection, ensure you handle potential exceptions:

try {
    Field field = NacosDynamicConfigManager.class.getDeclaredField("watchListenerMap");
    field.setAccessible(true);
    // ... rest of the reflection code
} catch (NoSuchFieldException | IllegalAccessException e) {
    Assert.fail("Failed to access watchListenerMap: " + e.getMessage());
}
  1. Consider using a mocking framework like Mockito to simulate the behavior of NacosDynamicConfigManager instead of relying on its internal implementation.

  2. Add more assertions to verify the state before and after each configuration change.

  3. Consider breaking this large test method into smaller, more focused test methods for each aspect of the functionality you're testing.

Comment on lines 71 to 75
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace hard-coded sleep with a more robust waiting mechanism.

Using a fixed sleep time can lead to flaky tests if the configuration update takes longer than expected. Consider using a polling mechanism with a timeout, or if available, a callback or event system to detect when the configuration has been updated.

Here's a potential improvement:

public void waitForConfigUpdate(ConsumerConfig<?> config, String methodName, int expectedTimeout, long maxWaitTime) {
    long startTime = System.currentTimeMillis();
    while (System.currentTimeMillis() - startTime < maxWaitTime) {
        if (config.getMethodTimeout(methodName) == expectedTimeout) {
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted while waiting for config update", e);
        }
    }
    throw new AssertionError("Config was not updated within the expected time");
}

// Usage
waitForConfigUpdate(consumerConfig, "sayHello", 5000, 10000);

Comment on lines +213 to +227
public void nodeChanged() throws Exception {
ChildData childData = nodeCache.getCurrentData();
String content = null;
ConfigChangeType changeType;
if (childData == null) {
changeType = ConfigChangeType.DELETED;

} else if (childData.getStat().getVersion() == 0) {
content = new String(childData.getData(), RpcConstants.DEFAULT_CHARSET);
changeType = ConfigChangeType.ADDED;
} else {
content = new String(childData.getData(), RpcConstants.DEFAULT_CHARSET);
changeType = ConfigChangeType.MODIFIED;
}
ConfigChangedEvent configChangeEvent = new ConfigChangedEvent(pathKey, (String) content, changeType);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential NullPointerException in nodeChanged Method

In the nodeChanged() method, childData.getData() may return null, which would cause a NullPointerException when passed to new String(...). Additionally, the condition childData.getStat().getVersion() == 0 may not reliably indicate that a node was added. Consider adjusting the logic to handle null values safely and to accurately determine the change type.

Apply the following change:

@Override
public void nodeChanged() throws Exception {
    ChildData childData = nodeCache.getCurrentData();
    String content = null;
    ConfigChangeType changeType;
    if (childData == null) {
        changeType = ConfigChangeType.DELETED;
    } else {
+       byte[] data = childData.getData();
+       content = data != null ? new String(data, RpcConstants.DEFAULT_CHARSET) : null;
+       int version = childData.getStat().getVersion();
+       if (version == 0) {
            changeType = ConfigChangeType.ADDED;
        } else {
            changeType = ConfigChangeType.MODIFIED;
        }
    }
    ConfigChangedEvent configChangeEvent = new ConfigChangedEvent(pathKey, content, changeType);
    listeners.forEach(listener -> listener.process(configChangeEvent));
}

This change ensures that childData.getData() is checked for null and handles it appropriately.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void nodeChanged() throws Exception {
ChildData childData = nodeCache.getCurrentData();
String content = null;
ConfigChangeType changeType;
if (childData == null) {
changeType = ConfigChangeType.DELETED;
} else if (childData.getStat().getVersion() == 0) {
content = new String(childData.getData(), RpcConstants.DEFAULT_CHARSET);
changeType = ConfigChangeType.ADDED;
} else {
content = new String(childData.getData(), RpcConstants.DEFAULT_CHARSET);
changeType = ConfigChangeType.MODIFIED;
}
ConfigChangedEvent configChangeEvent = new ConfigChangedEvent(pathKey, (String) content, changeType);
public void nodeChanged() throws Exception {
ChildData childData = nodeCache.getCurrentData();
String content = null;
ConfigChangeType changeType;
if (childData == null) {
changeType = ConfigChangeType.DELETED;
} else {
byte[] data = childData.getData();
content = data != null ? new String(data, RpcConstants.DEFAULT_CHARSET) : null;
int version = childData.getStat().getVersion();
if (version == 0) {
changeType = ConfigChangeType.ADDED;
} else {
changeType = ConfigChangeType.MODIFIED;
}
}
ConfigChangedEvent configChangeEvent = new ConfigChangedEvent(pathKey, content, changeType);
listeners.forEach(listener -> listener.process(configChangeEvent));
}

Comment on lines +129 to +166
@Override
public String getProviderServiceProperty(String service, String key) {
try {
String configValue = configMap.get(DynamicConfigKeyHelper.buildProviderServiceProKey(service, key));
return configValue != null ? configValue : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}

@Override
public String getConsumerServiceProperty(String service, String key) {
try {
String configValue = configMap.get(DynamicConfigKeyHelper.buildConsumerServiceProKey(service, key));
return configValue != null ? configValue : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}

@Override
public String getProviderMethodProperty(String service, String method, String key) {
try {
String configValue = configMap.get(DynamicConfigKeyHelper.buildProviderMethodProKey(service, method, key));
return configValue != null ? configValue : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}

@Override
public String getConsumerMethodProperty(String service, String method, String key) {
try {
String configValue = configMap.get(DynamicConfigKeyHelper.buildConsumerMethodProKey(service, method, key));
return configValue != null ? configValue : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Log Exceptions in Configuration Retrieval Methods

In methods like getProviderServiceProperty, exceptions are caught, and a default value is returned without logging. Not logging exceptions can make troubleshooting difficult. Consider logging the exceptions to aid in diagnosing problems.

Apply the following change to each method:

} catch (Exception e) {
+    LOGGER.error("Error retrieving provider service property for service: " + service + ", key: " + key, e);
     return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}

Apply similar changes to:

  • getConsumerServiceProperty(String service, String key)
  • getProviderMethodProperty(String service, String method, String key)
  • getConsumerMethodProperty(String service, String method, String key)

Committable suggestion was skipped due to low confidence.

Comment on lines 465 to 467
for (String key : newValueMap.keySet()) {
newValueMap.put(key, "");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider using clear() to reset newValueMap

In the process method, you're resetting all values in newValueMap to empty strings:

for (String key : newValueMap.keySet()) {
    newValueMap.put(key, "");
}

If the intention is to clear the map, using newValueMap.clear(); is more efficient and expresses the intent more clearly. If you need to retain the keys with empty values, please add a comment to explain this behavior.

Apply this diff to clear the map:

-for (String key : newValueMap.keySet()) {
-    newValueMap.put(key, "");
-}
+newValueMap.clear();

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid using wildcard imports; import specific classes instead

Using wildcard imports like import java.util.*; can lead to namespace pollution and may introduce ambiguity between class names. It's best practice to import only the specific classes you need.

Apply this diff to import specific classes:

-import java.util.*;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;

Committable suggestion was skipped due to low confidence.

Comment on lines 932 to 935
if(StringUtils.isBlank(newValueStr)){
// 改为默认值
newValueStr = configValueCache.get(property) == null ? null : configValueCache.get(property).toString();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential NullPointerException when accessing configValueCache

In line 934, configValueCache.get(property) is called without checking if configValueCache is initialized. If configValueCache is null, this will result in a NullPointerException.

Consider adding a null check before accessing configValueCache:

+ if (configValueCache != null && configValueCache.get(property) != null) {
    newValueStr = configValueCache.get(property).toString();
+ } else {
    newValueStr = null;
+ }

Alternatively, ensure that configValueCache is initialized before it's accessed in this method.

Committable suggestion was skipped due to low confidence.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Outside diff range and nitpick comments (9)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (2)

40-58: LGTM with a minor suggestion: Add null check for remainUrl.

The updated constructor and new parseRemainUrl method effectively handle the URL parsing, addressing previous concerns about URL decoding and improving code organization. However, consider adding an explicit null check for remainUrl in the constructor to prevent potential NullPointerExceptions.

Consider adding this null check at the beginning of the constructor:

if (remainUrl == null) {
    throw new IllegalArgumentException("remainUrl must not be null");
}

60-69: LGTM with a suggestion: Return an unmodifiable view of params.

The new getter methods appropriately provide access to the private fields. However, to prevent potential external modification of the internal state, consider returning an unmodifiable view of the params Map.

Modify the getParams method as follows:

protected Map<String, String> getParams() {
    return Collections.unmodifiableMap(params);
}

Don't forget to add the necessary import:

import java.util.Collections;
test/test-integration/pom.xml (1)

270-281: LGTM. Consider using a property for Curator version.

The addition of ZooKeeper and Curator dependencies aligns with the PR objectives. However, to maintain consistency and ease future updates, consider defining the Curator version in a property.

Apply this diff to use a property for the Curator version:

+    <properties>
+        <curator.version>4.3.0</curator.version>
+        <!-- ... other properties ... -->
+    </properties>

     <!-- ... other dependencies ... -->
     <dependency>
         <groupId>org.apache.curator</groupId>
         <artifactId>curator-test</artifactId>
-        <version>4.3.0</version>
+        <version>${curator.version}</version>
         <scope>test</scope>
     </dependency>
bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (3)

492-492: Remove unused configChanged method

The configChanged method is empty and doesn't seem to be used. Consider removing it to improve code cleanliness.

Apply this diff to remove the unused method:

-    @Override
-    public void configChanged(Map newValueMap) {
-
-    }

Line range hint 496-571: Enhance error handling and logging in attrUpdated method

The attrUpdated method handles configuration changes well, but could benefit from improved error handling and logging.

Consider the following improvements:

  1. Use try-with-resources for managing resources that need to be closed.
  2. Add more detailed logging, especially for the re-referencing process.
  3. Consider using a custom exception for configuration change errors to make error handling more specific.

Here's a partial example of how you might implement these suggestions:

@Override
public synchronized void attrUpdated(Map newValueMap) {
    String appName = consumerConfig.getAppName();
    Map<String, String> newValues = (Map<String, String>) newValueMap;
    Map<String, String> oldValues = new HashMap<>();
    boolean rerefer = false;

    try {
        rerefer = compareAndUpdateValues(newValues, oldValues);
    } catch (Exception e) {
        LOGGER.errorWithApp(appName, "Error comparing consumer attributes: {}", e.getMessage(), e);
        return;
    }

    if (rerefer) {
        try {
            rereferConsumer(newValues, oldValues);
        } catch (ConfigurationChangeException e) {
            LOGGER.errorWithApp(appName, "Error during consumer re-referencing: {}", e.getMessage(), e);
            rollbackChanges(oldValues);
        }
    }
}

private void rereferConsumer(Map<String, String> newValues, Map<String, String> oldValues) throws ConfigurationChangeException {
    String appName = consumerConfig.getAppName();
    try {
        LOGGER.infoWithApp(appName, "Starting consumer re-referencing process");
        unSubscribe();
        updateAttributes(newValues);
        switchCluster();
        LOGGER.infoWithApp(appName, "Consumer re-referencing completed successfully");
    } catch (Exception e) {
        throw new ConfigurationChangeException("Failed to re-refer consumer", e);
    }
}

private void rollbackChanges(Map<String, String> oldValues) {
    for (Map.Entry<String, String> entry : oldValues.entrySet()) {
        consumerConfig.updateAttribute(entry.getKey(), entry.getValue(), true);
    }
    subscribe();
}

This refactoring improves error handling, adds more detailed logging, and makes the code more modular and easier to maintain.


Line range hint 1-624: Improve code organization and documentation

While the changes implement dynamic configuration management effectively, there are opportunities to improve code organization and documentation:

  1. Consider grouping related methods together (e.g., all methods related to dynamic configuration).
  2. Add JavaDoc comments to new methods and fields, explaining their purpose and usage.
  3. Update the class-level JavaDoc to mention the new dynamic configuration capabilities.

Here's an example of how you might improve the documentation:

/**
 * Default consumer bootstrap.
 * This class now supports dynamic configuration management, including interface-level
 * and request-level dynamic configurations.
 *
 * @author <a href="mailto:[email protected]">GengZhang</a>
 */
@Extension("sofa")
public class DefaultConsumerBootstrap<T> extends ConsumerBootstrap<T> {
    // ... existing code ...

    /**
     * Builds a ConfigListener for handling dynamic configuration changes.
     *
     * @param bootstrap ConsumerBootstrap
     * @return ConfigListener
     */
    protected ConfigListener buildConfigListener(ConsumerBootstrap bootstrap) {
        return new ConsumerAttributeListener();
    }

    // ... other methods ...

    /**
     * Inner class for handling consumer attribute changes in dynamic configuration.
     */
    private class ConsumerAttributeListener implements ConfigListener {
        /**
         * Stores new configuration values during processing.
         */
        private Map<String, String> newValueMap = new HashMap<>();

        /**
         * List of configuration keys that can be dynamically updated.
         */
        private static final List<String> DYNAMIC_CONFIG_KEYS = Arrays.asList(
            RpcConstants.CONFIG_KEY_TIMEOUT,
            RpcConstants.CONFIG_KEY_RETRIES,
            RpcConstants.CONFIG_KEY_LOADBALANCER
        );

        // ... existing methods ...

        /**
         * Processes configuration change events.
         *
         * @param event The configuration change event
         */
        @Override
        public void process(ConfigChangedEvent event) {
            // ... implementation ...
        }

        // ... other methods ...
    }
}

These improvements in documentation will make the code more maintainable and easier for other developers to understand and work with.

test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java (1)

42-42: Declare logger as private static final

To adhere to standard Java conventions and ensure thread safety, declare the logger as private static final.

Apply this diff:

-    Logger logger = LoggerFactory.getLogger(ZookeeperDynamicConfigTest.class);
+    private static final Logger logger = LoggerFactory.getLogger(ZookeeperDynamicConfigTest.class);
config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (2)

135-135: Adjust Access Modifier for Inner Class ApolloListener

The inner class ApolloListener is declared as public. Since it's intended for internal use within ApolloDynamicConfigManager, consider making it private or package-private to encapsulate it and prevent unintended access from external classes.

Apply this diff to adjust the access modifier:

-public class ApolloListener implements ConfigChangeListener {
+class ApolloListener implements ConfigChangeListener {

73-74: Check for Null or Blank appName Before Setting System Property

In the constructor, appName is used without checking if it's null or blank before setting the system property. This could lead to setting an invalid system property value.

Consider adding a null or blank check:

System.setProperty(APOLLO_APPID_KEY, appName);
+if (StringUtils.isNotBlank(appName)) {
+    System.setProperty(APOLLO_APPID_KEY, appName);
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between d841de9 and eaba723.

📒 Files selected for processing (8)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (7 hunks)
  • config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (4 hunks)
  • test/test-integration/pom.xml (2 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java
🧰 Additional context used
🔇 Additional comments (11)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (5)

21-24: LGTM: Import statements are appropriate.

The added import statements for ConfigListener and Java collections are necessary for the new functionality introduced in the class.


36-38: LGTM: New fields added and initialized properly.

The new private fields address and params are appropriately added to store parsed information from the remainUrl. The initialization of params as an empty HashMap addresses the previous concern about potential NullPointerExceptions.


80-87: LGTM: New initServiceConfiguration method adds flexibility.

The new overloaded initServiceConfiguration method with a ConfigListener parameter provides additional flexibility for service configuration initialization. The method is well-documented with appropriate JavaDoc.


135-141: LGTM: New addListener method enhances configurability.

The new abstract addListener method is a valuable addition, allowing for the registration of configuration listeners. The method is well-documented with appropriate JavaDoc.


Line range hint 1-142: Overall LGTM: Significant improvements to DynamicConfigManager.

The changes to the DynamicConfigManager class significantly enhance its functionality and flexibility. The addition of URL parsing, new fields, getter methods, and listener support improves the class's capability to manage dynamic configurations. The modifications address previous concerns and generally follow good coding practices.

Minor suggestions for improvement have been made in the individual review comments, including:

  1. Adding a null check for remainUrl in the constructor.
  2. Returning an unmodifiable view of the params Map in the getParams method.

These changes, once implemented, will further improve the robustness and safety of the class.

bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (3)

32-33: LGTM: New imports for dynamic configuration management

The addition of ConfigChangedEvent and ConfigChangeType imports is consistent with the implementation of dynamic configuration features in this class.


150-151: LGTM: Implementation of ConfigListener

The addition of a ConfigListener built using buildConfigListener method and setting it to consumerConfig is a good practice for handling configuration changes.


169-177: LGTM: Implementation of interface-level dynamic configuration

The addition of interface-level dynamic configuration is a good enhancement. It checks for a dynamic URL, initializes a DynamicConfigManager, and sets up a listener for the interface.

To ensure that the dynamic configuration is properly set up, let's verify the usage of DynamicConfigKeys.DYNAMIC_URL:

✅ Verification successful

Dynamic Configuration Setup Verified Successfully

The usage of DynamicConfigKeys.DYNAMIC_URL in the DefaultConsumerBootstrap.java and the accompanying test cases confirms that the dynamic configuration is properly set up.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify the usage of DynamicConfigKeys.DYNAMIC_URL
rg --type java "DynamicConfigKeys.DYNAMIC_URL"

Length of output: 1914

config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3)

63-67: Avoid Using System.setProperty in Constructors

Setting system properties within constructors can have unintended side effects since system properties are global and affect the entire JVM. This may lead to unpredictable behavior, especially in multi-threaded environments or when multiple instances are created.


160-161: Ensure Thread Safety When Managing Listeners

The listeners set uses CopyOnWriteArraySet, which is suitable for scenarios with infrequent mutations and frequent iterations. If listeners are added or removed frequently, consider using a different thread-safe collection like ConcurrentHashMap to improve performance.


137-137: ⚠️ Potential issue

Consider Using Set<ConfigListener> Instead of Raw Type

The listeners set is declared without specifying the generic type, which can lead to type safety issues.

Apply this diff to specify the generic type:

-private Set listeners = new CopyOnWriteArraySet<>();
+private Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();

Likely invalid or redundant comment.

Comment on lines +264 to +269
<dependency>
<groupId>com.alipay.sofa</groupId>
<artifactId>sofa-rpc-config-apollo</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Clarify the need for Apollo dependency.

The addition of the sofa-rpc-config-apollo dependency seems to contradict the PR objective of replacing Apollo with Nacos. If this is no longer needed, consider removing it to complete the transition to Nacos.

If this dependency is still required for certain tests, please add a comment explaining its necessity. Otherwise, consider removing it with the following diff:

-        <dependency>
-            <groupId>com.alipay.sofa</groupId>
-            <artifactId>sofa-rpc-config-apollo</artifactId>
-            <version>${project.parent.version}</version>
-            <scope>test</scope>
-        </dependency>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<dependency>
<groupId>com.alipay.sofa</groupId>
<artifactId>sofa-rpc-config-apollo</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>

Comment on lines +204 to 207
<artifactId>sofa-rpc-config-nacos</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Apollo dependencies still present in the codebase.

The grep results indicate that sofa-rpc-config-apollo dependencies remain in the following files:

  • all/pom.xml
  • config/config-apollo/pom.xml
  • test/test-integration/pom.xml

Please ensure that all Apollo dependencies are fully removed to align with the PR objectives.

🔗 Analysis chain

LGTM. Verify complete removal of Apollo dependencies.

The replacement of Apollo with Nacos for configuration management is correctly implemented here. This change aligns with the PR objectives.

To ensure a complete transition, let's verify that all Apollo dependencies have been removed:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for any remaining Apollo dependencies
grep -R "sofa-rpc-config-apollo" .

Length of output: 391

Comment on lines 464 to 495
public void process(ConfigChangedEvent event) {
// 清除上次的赋值,并保留赋值过的key
for (String key : newValueMap.keySet()) {
newValueMap.put(key, "");
}
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
// ADDED or MODIFIED
String[] lines = event.getContent().split("\n");
for (String line : lines) {
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
for (String dynamicConfigKey : dynamicConfigKeys) {
if (key.equals(dynamicConfigKey) || key.endsWith("." + dynamicConfigKey)) {
newValueMap.put(key, value);
break;
}
}
} else {
LOGGER.warn("Malformed configuration line: {}", line);
}
}
}
attrUpdated(newValueMap);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor the process method for better readability

The process method is quite long and could benefit from some refactoring to improve readability and maintainability.

Consider extracting the logic for parsing configuration lines into a separate method. Here's a suggested refactoring:

private void parseConfigurationLines(String content) {
    String[] lines = content.split("\n");
    for (String line : lines) {
        String[] keyValue = line.split("=", 2);
        if (keyValue.length == 2) {
            String key = keyValue[0].trim();
            String value = keyValue[1].trim();
            for (String dynamicConfigKey : DYNAMIC_CONFIG_KEYS) {
                if (key.equals(dynamicConfigKey) || key.endsWith("." + dynamicConfigKey)) {
                    newValueMap.put(key, value);
                    break;
                }
            }
        } else {
            LOGGER.warn("Malformed configuration line: {}", line);
        }
    }
}

@Override
public void process(ConfigChangedEvent event) {
    newValueMap.clear();
    if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
        parseConfigurationLines(event.getContent());
    }
    attrUpdated(newValueMap);
}

This refactoring improves readability and separates concerns, making the code easier to maintain and test.

Comment on lines 453 to 457
private Map<String, String> newValueMap = new HashMap<>();

// 动态配置项
private List<String> dynamicConfigKeys = Arrays.asList(RpcConstants.CONFIG_KEY_TIMEOUT,
RpcConstants.CONFIG_KEY_RETRIES, RpcConstants.CONFIG_KEY_LOADBALANCER);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider using a static final list for dynamicConfigKeys

The dynamicConfigKeys list is initialized with constant values. Consider making it a static final list to improve performance and reduce memory usage.

Apply this diff to make dynamicConfigKeys a static final list:

-    private List<String> dynamicConfigKeys = Arrays.asList(RpcConstants.CONFIG_KEY_TIMEOUT,
-            RpcConstants.CONFIG_KEY_RETRIES, RpcConstants.CONFIG_KEY_LOADBALANCER);
+    private static final List<String> DYNAMIC_CONFIG_KEYS = Arrays.asList(RpcConstants.CONFIG_KEY_TIMEOUT,
+            RpcConstants.CONFIG_KEY_RETRIES, RpcConstants.CONFIG_KEY_LOADBALANCER);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Map<String, String> newValueMap = new HashMap<>();
// 动态配置项
private List<String> dynamicConfigKeys = Arrays.asList(RpcConstants.CONFIG_KEY_TIMEOUT,
RpcConstants.CONFIG_KEY_RETRIES, RpcConstants.CONFIG_KEY_LOADBALANCER);
private Map<String, String> newValueMap = new HashMap<>();
// 动态配置项
private static final List<String> DYNAMIC_CONFIG_KEYS = Arrays.asList(RpcConstants.CONFIG_KEY_TIMEOUT,
RpcConstants.CONFIG_KEY_RETRIES, RpcConstants.CONFIG_KEY_LOADBALANCER);

Comment on lines +61 to +63
Field field = ZookeeperDynamicConfigManager.class.getDeclaredField("zkClient");
field.setAccessible(true);
CuratorFramework zkClient = (CuratorFramework) field.get(dynamicConfigManager);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid using reflection to access private fields

Accessing the private field zkClient via reflection breaks encapsulation and may lead to brittle tests if the internal implementation changes.

Consider one of the following approaches:

  • Modify ZookeeperDynamicConfigManager to provide a public accessor method for zkClient.
  • Use existing public APIs to achieve the desired functionality without direct access to internal fields.
  • Mock the necessary components if you're aiming to simulate specific behaviors in your test.

This will enhance maintainability and reduce dependency on internal implementation details.

Comment on lines 66 to 74
if (zkClient.checkExists().forPath("/config/demo/com.alipay.sofa.rpc.test.HelloService") == null) {
zkClient.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.forPath("/config/demo/com.alipay.sofa.rpc.test.HelloService", "timeout=5000".getBytes());
} else {
zkClient.setData().forPath("/sofa-rpc/config/demo/com.alipay.sofa.rpc.test.HelloService",
"timeout=5000".getBytes());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix inconsistent Zookeeper node paths

There is an inconsistency in the Zookeeper node paths between the creation and update operations:

  • Creation path (lines 66-70):

    "/config/demo/com.alipay.sofa.rpc.test.HelloService"
  • Update path (lines 72-73):

    "/sofa-rpc/config/demo/com.alipay.sofa.rpc.test.HelloService"

This discrepancy may cause the update operation to affect a different node than intended, leading to unexpected behavior during the test.

Please ensure that the paths are consistent across creation, update, and deletion operations.

Apply this diff to correct the update path:

             zkClient.setData().forPath(
-                "/sofa-rpc/config/demo/com.alipay.sofa.rpc.test.HelloService",
+                "/config/demo/com.alipay.sofa.rpc.test.HelloService",
                 "timeout=5000".getBytes());

Comment on lines +85 to +86
// TODO 暂不支持
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Implement initServiceConfiguration(String service) Method

The method initServiceConfiguration(String service) contains a TODO comment indicating it's currently not supported. Leaving it unimplemented might lead to AbstractMethodError if called. Consider implementing the method or throwing an UnsupportedOperationException to handle this scenario gracefully.

Would you like assistance in implementing this method or should we update it to throw an exception? I can help draft the implementation or open a GitHub issue to track this task.

Example of throwing an exception:

@Override
public void initServiceConfiguration(String service) {
-    // TODO 暂不支持
+    throw new UnsupportedOperationException("initServiceConfiguration is not supported yet.");
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// TODO 暂不支持
}
@Override
public void initServiceConfiguration(String service) {
throw new UnsupportedOperationException("initServiceConfiguration is not supported yet.");
}

Comment on lines 152 to 160
private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
}
return ConfigChangeType.MODIFIED;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle All Possible Property Change Types

The getChangeType method currently handles only DELETED and defaults all other change types to MODIFIED. However, there may be other change types like ADDED. Explicitly handling all possible PropertyChangeType values ensures correct behavior and easier maintenance.

Update the method to handle additional change types:

private ConfigChangeType getChangeType(ConfigChange change) {
    if (change.getChangeType() == PropertyChangeType.DELETED) {
        return ConfigChangeType.DELETED;
+   } else if (change.getChangeType() == PropertyChangeType.ADDED) {
+       return ConfigChangeType.ADDED;
    }
    return ConfigChangeType.MODIFIED;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
}
return ConfigChangeType.MODIFIED;
}
private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
} else if (change.getChangeType() == PropertyChangeType.ADDED) {
return ConfigChangeType.ADDED;
}
return ConfigChangeType.MODIFIED;
}

Comment on lines 76 to 78
if (params != null && params.containsKey("cluster")) {
String clusterValue = (String)params.get("cluster");
System.setProperty(APOLLO_CLUSTER_KEY, clusterValue);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Specify Generic Types for Map Usage

The params variable is declared without generic types, which can lead to unchecked type warnings and potential ClassCastExceptions. It's good practice to specify the generic types for collections to ensure type safety.

Apply this diff to specify the generic types:

- Map params = getParams();
+ Map<String, Object> params = getParams();

Committable suggestion was skipped due to low confidence.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (6)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (5)

36-38: LGTM: New fields added for storing parsed URL information.

The addition of address and params fields enhances the class's ability to manage configuration details. Good practice initializing params to prevent NullPointerExceptions.

Consider adding a comment explaining the purpose of these fields for better code documentation.


40-43: LGTM: Constructor updated to handle remainUrl parameter.

The addition of the remainUrl parameter and the call to parseRemainUrl method improves the initialization process of the DynamicConfigManager.

Consider adding parameter validation for remainUrl to ensure it's not null or empty before parsing.


45-58: Improve robustness and security in parseRemainUrl method.

The method effectively parses the remainUrl to populate address and params fields. However, consider the following improvements:

  1. Add null check for remainUrl at the beginning of the method.
  2. Use java.net.URL for more robust URL parsing.
  3. Implement URL decoding for parameter keys and values to handle encoded characters correctly.
  4. Consider using a more robust query string parser to handle edge cases (e.g., parameters without values).

Example improvement:

protected void parseRemainUrl(String remainUrl) {
    if (remainUrl == null || remainUrl.isEmpty()) {
        throw new IllegalArgumentException("remainUrl must not be null or empty");
    }
    try {
        URL url = new URL(remainUrl);
        this.address = url.getProtocol() + "://" + url.getAuthority() + url.getPath();
        String query = url.getQuery();
        if (query != null && !query.isEmpty()) {
            for (String param : query.split("&")) {
                String[] pair = param.split("=");
                String key = URLDecoder.decode(pair[0], "UTF-8");
                String value = pair.length > 1 ? URLDecoder.decode(pair[1], "UTF-8") : "";
                this.params.put(key, value);
            }
        }
    } catch (MalformedURLException | UnsupportedEncodingException e) {
        throw new IllegalArgumentException("Invalid URL: " + remainUrl, e);
    }
}

This implementation is more robust and secure, handling various edge cases and properly decoding URL-encoded parameters.


60-69: LGTM: Getter methods added for encapsulation.

The addition of getter methods for appName, address, and params provides proper encapsulation. However, consider returning an unmodifiable view of the params Map to prevent external modification:

protected Map<String, String> getParams() {
    return Collections.unmodifiableMap(params);
}

This ensures that the internal state of the params Map cannot be modified by external code, maintaining the integrity of the parsed parameters.


135-141: LGTM: New addListener method for dynamic listener registration.

The addition of the abstract addListener method is a good design choice, allowing for dynamic registration of configuration listeners. This enhances the flexibility of the configuration management system.

Consider adding a corresponding removeListener method to allow for listener deregistration, which would provide a more complete API for listener management.

config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (1)

137-137: Declare listeners as final

The listeners set in the ApolloListener class is not declared as final. Since this set is initialized once and should not be reassigned, marking it as final enhances code clarity and thread safety.

Apply this diff to declare the field as final:

-    private Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();
+    private final Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between eaba723 and 6eaf0b0.

📒 Files selected for processing (8)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (7 hunks)
  • config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (4 hunks)
  • test/test-integration/pom.xml (2 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java
  • test/test-integration/pom.xml
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java
🧰 Additional context used
🔇 Additional comments (5)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (2)

21-24: LGTM: New imports added for enhanced functionality.

The addition of ConfigListener and java.util.Map imports aligns with the new features implemented in this class.


80-87: LGTM: New initServiceConfiguration method with listener support.

The addition of this overloaded abstract method enhances the flexibility of service configuration initialization by allowing the registration of a ConfigListener. This is a good design choice that maintains consistency with the abstract nature of the class while providing extended functionality.

config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3)

63-67: Also applies to: 73-79


75-75: Specify Generic Types for Map Usage

The params variable is declared without generic types, which can lead to unchecked type warnings and potential ClassCastExceptions. It's good practice to specify generic types for collections to ensure type safety.


126-129: ⚠️ Potential issue

Consider Throwing UnsupportedOperationException

The method getServiceAuthRule(String service) returns null with a TODO comment indicating it's not yet supported. Returning null might lead to NullPointerExceptions if the calling code does not handle it properly. Consider throwing an UnsupportedOperationException to make the unsupported status explicit.

Apply this diff to update the method:

@Override
public AuthRuleGroup getServiceAuthRule(String service) {
    // TODO 暂不支持
-   return null;
+   throw new UnsupportedOperationException("getServiceAuthRule is not supported yet.");
}

Likely invalid or redundant comment.

Comment on lines +152 to +160
private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
}
if (change.getChangeType() == PropertyChangeType.ADDED) {
return ConfigChangeType.ADDED;
}
return ConfigChangeType.MODIFIED;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle All Possible Change Types Explicitly

In the getChangeType method, all unhandled PropertyChangeTypes default to MODIFIED. If new change types are introduced in the future, this could lead to unexpected behavior.

Consider modifying the method to explicitly handle all possible change types or throw an exception for unknown types:

private ConfigChangeType getChangeType(ConfigChange change) {
    if (change.getChangeType() == PropertyChangeType.DELETED) {
        return ConfigChangeType.DELETED;
    }
    if (change.getChangeType() == PropertyChangeType.ADDED) {
        return ConfigChangeType.ADDED;
    }
+   if (change.getChangeType() == PropertyChangeType.MODIFIED) {
+       return ConfigChangeType.MODIFIED;
+   }
+   throw new IllegalArgumentException("Unknown change type: " + change.getChangeType());
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
}
if (change.getChangeType() == PropertyChangeType.ADDED) {
return ConfigChangeType.ADDED;
}
return ConfigChangeType.MODIFIED;
}
private ConfigChangeType getChangeType(ConfigChange change) {
if (change.getChangeType() == PropertyChangeType.DELETED) {
return ConfigChangeType.DELETED;
}
if (change.getChangeType() == PropertyChangeType.ADDED) {
return ConfigChangeType.ADDED;
}
if (change.getChangeType() == PropertyChangeType.MODIFIED) {
return ConfigChangeType.MODIFIED;
}
throw new IllegalArgumentException("Unknown change type: " + change.getChangeType());
}

Comment on lines +143 to +150
public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
ConfigChangedEvent event =
new ConfigChangedEvent(key, change.getNewValue(), getChangeType(change));
listeners.forEach(listener -> listener.process(event));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent Concurrent Modification Exceptions

Modifying the listeners set while iterating over it in the onChange method could lead to ConcurrentModificationException. Although CopyOnWriteArraySet is thread-safe for concurrent reads and writes, it's safer to iterate over a snapshot of the set.

Consider iterating over a snapshot of the listeners:

public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
    for (String key : changeEvent.changedKeys()) {
        ConfigChange change = changeEvent.getChange(key);
        ConfigChangedEvent event =
                new ConfigChangedEvent(key, change.getNewValue(), getChangeType(change));
-       listeners.forEach(listener -> listener.process(event));
+       new ArrayList<>(listeners).forEach(listener -> listener.process(event));
    }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
ConfigChangedEvent event =
new ConfigChangedEvent(key, change.getNewValue(), getChangeType(change));
listeners.forEach(listener -> listener.process(event));
}
}
public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
ConfigChangedEvent event =
new ConfigChangedEvent(key, change.getNewValue(), getChangeType(change));
new ArrayList<>(listeners).forEach(listener -> listener.process(event));
}
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Outside diff range and nitpick comments (7)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (1)

28-30: Consider adding Javadoc for the new constants.

While the constant names are self-explanatory, adding Javadoc comments would help developers understand:

  • The purpose and usage of these configuration nodes
  • Where these values are expected to be used in the configuration hierarchy
  • Any constraints or format requirements
+    /**
+     * The configuration node path used in the configuration center.
+     */
     public static final String      CONFIG_NODE       = "config";

+    /**
+     * The default namespace for SOFA RPC configurations in the configuration center.
+     */
     public static final String      DEFAULT_NAMESPACE = "sofa-rpc";
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (1)

33-37: Enhance test documentation and categorization.

Consider adding:

  1. A detailed class-level JavaDoc explaining the test scenarios and prerequisites.
  2. Test category annotations (e.g., @Category(IntegrationTest.class)) to properly categorize this as an integration test.
 /**
+ * Integration test for Nacos dynamic configuration functionality.
+ * Prerequisites:
+ * - Nacos server running on localhost:8848
+ * - Nacos server configured with default credentials (nacos/nacos)
+ *
  * @author Narziss
  * @version NacosDynamicConfigTest.java, v 0.1 2024年09月28日 12:11 Narziss
  */
+@Category(IntegrationTest.class)
 public class NacosDynamicConfigTest {
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (2)

80-88: Enhance method documentation for configuration listeners.

The documentation for these methods should include:

  • Lifecycle of the listeners
  • Threading model for listener callbacks
  • Error handling expectations
  • Example usage

Add the following to the JavaDoc:

/**
 * Init service's governance related configuration with a configuration change listener.
 * 
 * @param service  target service
 * @param listener config listener that will be notified when configuration changes.
 *                The listener should handle its own error cases and must be thread-safe.
 *                Callbacks will be made on the configuration center's event dispatch thread.
 * @throws IllegalArgumentException if service or listener is null
 */
public abstract void initServiceConfiguration(String service, ConfigListener listener);

/**
 * Add listener for configuration changes.
 *
 * @param key      config key to monitor for changes
 * @param listener config listener that will be notified when the specified key's value changes.
 *                The listener should handle its own error cases and must be thread-safe.
 *                Callbacks will be made on the configuration center's event dispatch thread.
 * @throws IllegalArgumentException if key or listener is null
 */
public abstract void addListener(String key, ConfigListener listener);

Also applies to: 135-141


Line range hint 21-142: Consider thread safety implications of the dynamic configuration design.

The current implementation handles configuration updates through listeners, but there are some architectural considerations:

  1. The params map should be thread-safe if it can be modified after initialization
  2. Consider using immutable configurations or copy-on-write patterns for thread safety
  3. Document the thread safety guarantees for implementing classes

Consider these options:

  1. Make params unmodifiable after construction:
protected Map<String, String> getParams() {
    return Collections.unmodifiableMap(params);
}
  1. Use ConcurrentHashMap if runtime modifications are needed:
private final Map<String, String> params = new ConcurrentHashMap<>();
  1. Add thread safety documentation to the class:
/**
 * Base class for dynamic configuration management.
 * Thread Safety: Implementations must ensure thread-safe access to configuration data
 * as it may be accessed concurrently by multiple threads.
 */
@Extensible(singleton = true)
public abstract class DynamicConfigManager {
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (1)

36-39: Enhance class documentation.

Consider adding more detailed documentation about:

  • The purpose of these tests
  • Prerequisites for running the tests
  • Expected behavior and test scenarios

Example improvement:

/**
 * Integration tests for Apollo Dynamic Configuration functionality.
 * Tests the dynamic configuration updates through Apollo configuration center,
 * including adding, modifying, and deleting configurations.
 * 
 * Prerequisites:
 * - Apollo configuration center running at 127.0.0.1:8080
 * - Default cluster configuration
 *
 * @author Narziss
 * @version ApolloDynamicConfigTest.java, v 0.1 2024年09月28日 10:46 Narziss
 */
test/test-integration/pom.xml (1)

276-281: Consider using property for Curator test version.

While the dependency is correctly added, consider managing the version through a property to align with the project's version management practices. This makes it easier to update versions across the project.

Apply this diff to use a property:

+    <properties>
+        <!-- existing properties -->
+        <curator.test.version>4.3.0</curator.test.version>
+    </properties>

     <dependencies>
         <!-- other dependencies -->
         <dependency>
             <groupId>org.apache.curator</groupId>
             <artifactId>curator-test</artifactId>
-            <version>4.3.0</version>
+            <version>${curator.test.version}</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1)

76-86: Use generic types for the params Map to ensure type safety

The params Map is used without type parameters, which may lead to unchecked type casting and compiler warnings. Parameterizing the Map enhances type safety and code clarity.

Apply this diff to parameterize the Map:

-        Map params = getParams();
+        Map<String, Object> params = getParams();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 6eaf0b0 and 7a354b3.

📒 Files selected for processing (9)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (7 hunks)
  • config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (4 hunks)
  • test/test-integration/pom.xml (2 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java (1 hunks)
🔇 Additional comments (15)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (3)

19-20: LGTM!

The import is correctly added for the ConfigKey usage.


32-38: ⚠️ Potential issue

Review the default value for CENTER_ADDRESS.

The default value is set to a single space " ", which seems unusual. Consider:

  1. Using an empty string "" if no default is intended
  2. Using a meaningful default value if one is expected
  3. Adding a comment explaining why a space is used (if intentional)

32-61: Consider adding address format validation.

The configuration keys accept any string value without validation. Consider:

  1. Adding validation logic in the configuration manager implementations
  2. Documenting the expected address format for each configuration center
  3. Adding format examples in the configuration key descriptions

For example:

  • Zookeeper: host1:port1,host2:port2
  • Nacos: host:port
  • Apollo: http(s)://host:port
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (1)

65-77: ⚠️ Potential issue

Add validation for configuration changes in Nacos.

The test only verifies the local state changes but doesn't validate if the configurations were actually persisted in Nacos.

Add verification of the actual Nacos state:

         // 测试配置新增
         String configValue = "timeout=5000";
         nacosConfigListener.innerReceive(consumerConfig.getInterfaceId(), consumerConfig.getAppName(), configValue);
+        // Verify configuration in Nacos
+        String actualConfig = configService.getConfig(consumerConfig.getInterfaceId(), consumerConfig.getAppName(), 3000);
+        Assert.assertEquals(configValue, actualConfig);
         Assert.assertEquals(5000, consumerConfig.getMethodTimeout("sayHello"));
test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java (2)

1-43: LGTM! Class structure and imports are well-organized.

The class follows proper structure with license header, necessary imports, and appropriate base class extension.


59-64: LGTM with existing concerns.

Note: Please address the existing review comment about avoiding reflection to access private fields.

test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (1)

42-56: Previous review comments are still applicable.

The test setup could be improved as per the previous review suggestions:

  • Add @After for cleanup
  • Add error handling
  • Verify initial state
test/test-integration/pom.xml (3)

204-207: LGTM!

The addition of the Nacos configuration dependency aligns with the PR objectives and follows the project's dependency management practices.


264-269: Skip comment about Apollo dependency.

This issue has already been addressed in a previous review comment.


270-275: LGTM!

The addition of the Zookeeper configuration dependency aligns with the PR objectives and follows the project's dependency management practices.

config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (2)

90-93: Service configuration initialization is correctly implemented

The method initServiceConfiguration(String service, ConfigListener listener) properly retrieves the raw configuration and notifies the listener when the configuration is available, ensuring that dynamic configurations are correctly initialized for the service.


152-160: ⚠️ Potential issue

Handle potential unknown PropertyChangeType values explicitly

The getChangeType method handles DELETED and ADDED types and defaults to MODIFIED for all other cases. If new PropertyChangeType values are introduced in the future, they will be incorrectly classified as MODIFIED.

Consider updating the method to explicitly handle all possible PropertyChangeType values or throw an exception for unknown types to prevent unexpected behavior.

Apply this diff to enhance robustness:

private ConfigChangeType getChangeType(ConfigChange change) {
    if (change.getChangeType() == PropertyChangeType.DELETED) {
        return ConfigChangeType.DELETED;
    }
    if (change.getChangeType() == PropertyChangeType.ADDED) {
        return ConfigChangeType.ADDED;
    }
+   if (change.getChangeType() == PropertyChangeType.MODIFIED) {
+       return ConfigChangeType.MODIFIED;
+   }
+   throw new IllegalArgumentException("Unknown change type: " + change.getChangeType());
}

Likely invalid or redundant comment.

bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (3)

166-170: [Duplicate Comment]: Missing ConfigListener for request-level dynamic configuration

The previous issue regarding the missing ConfigListener for request-level dynamic configuration remains applicable. Without setting a ConfigListener, the consumer may not respond to configuration changes at the request level.


Line range hint 182-219: [Duplicate Comment]: Ensure listeners are unregistered to prevent memory leaks

The issue previously raised about unregistering listeners from the DynamicConfigManager in the unRefer method is still valid. Consider implementing a mechanism to remove the listener when the consumer is unreferenced to prevent potential memory leaks.


460-467: [Duplicate Comment]: Consider making dynamicConfigKeys a static final field

The prior suggestion to make dynamicConfigKeys a static final field remains relevant. This would improve performance and reduce memory usage.

Comment on lines +46 to +53
ConsumerConfig<HelloService> consumerConfig = new ConsumerConfig<HelloService>()
.setInterfaceId(HelloService.class.getName())
.setProtocol("bolt")
.setDirectUrl("bolt://127.0.0.1:12200")
.setConnectTimeout(10 * 1000)
.setApplication(clientApplication);

consumerConfig.refer();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add timeout validation for consumer initialization.

The consumer configuration and reference operation could potentially hang if the Nacos server is not available.

Add timeout handling:

         ConsumerConfig<HelloService> consumerConfig = new ConsumerConfig<HelloService>()
             .setInterfaceId(HelloService.class.getName())
             .setProtocol("bolt")
             .setDirectUrl("bolt://127.0.0.1:12200")
             .setConnectTimeout(10 * 1000)
             .setApplication(clientApplication);
 
+        long startTime = System.currentTimeMillis();
         consumerConfig.refer();
+        Assert.assertTrue("Consumer initialization took too long",
+            System.currentTimeMillis() - startTime < 30000);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ConsumerConfig<HelloService> consumerConfig = new ConsumerConfig<HelloService>()
.setInterfaceId(HelloService.class.getName())
.setProtocol("bolt")
.setDirectUrl("bolt://127.0.0.1:12200")
.setConnectTimeout(10 * 1000)
.setApplication(clientApplication);
consumerConfig.refer();
ConsumerConfig<HelloService> consumerConfig = new ConsumerConfig<HelloService>()
.setInterfaceId(HelloService.class.getName())
.setProtocol("bolt")
.setDirectUrl("bolt://127.0.0.1:12200")
.setConnectTimeout(10 * 1000)
.setApplication(clientApplication);
long startTime = System.currentTimeMillis();
consumerConfig.refer();
Assert.assertTrue("Consumer initialization took too long",
System.currentTimeMillis() - startTime < 30000);

Comment on lines 78 to 79
System.clearProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure proper cleanup of system properties.

The cleanup should restore the original property value if it existed.

-        System.clearProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey());
+        } finally {
+            if (originalValue != null) {
+                System.setProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey(), originalValue);
+            } else {
+                System.clearProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey());
+            }
+        }

Committable suggestion was skipped due to low confidence.

Comment on lines 40 to 43
public void testNacosDynamicConfig() throws Exception {
System.setProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey(),
"nacos://127.0.0.1:8848?username=nacos&password=nacos");
ApplicationConfig clientApplication = new ApplicationConfig();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for system property configuration.

The system property setup should be wrapped in a try-finally block to ensure cleanup even if the test fails.

     @Test
     public void testNacosDynamicConfig() throws Exception {
-        System.setProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey(),
-            "nacos://127.0.0.1:8848?username=nacos&password=nacos");
+        String originalValue = System.getProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey());
+        try {
+            System.setProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey(),
+                "nacos://127.0.0.1:8848?username=nacos&password=nacos");

Committable suggestion was skipped due to low confidence.

Comment on lines 44 to 58
@Test
public void testZookeeperDynamicConfig() throws Exception {
System.setProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey(), "zookeeper://127.0.0.1:2181");
ApplicationConfig clientApplication = new ApplicationConfig();
clientApplication.setAppName("demo");

ConsumerConfig<HelloService> consumerConfig = new ConsumerConfig<HelloService>()
.setInterfaceId(HelloService.class.getName())
.setProtocol("bolt")
.setDirectUrl("bolt://127.0.0.1:12200")
.setConnectTimeout(10 * 1000)
.setApplication(clientApplication);

consumerConfig.refer();

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add proper test cleanup using @after or try-with-resources.

The test sets system properties and creates resources but doesn't ensure cleanup in case of test failure.

Add the following cleanup method:

@After
public void cleanup() {
    System.clearProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey());
    if (consumerConfig != null) {
        consumerConfig.unRefer();
    }
}

Comment on lines +65 to +74
// 新增或修改配置节点
if (zkClient.checkExists().forPath("/config/demo/com.alipay.sofa.rpc.test.HelloService") == null) {
zkClient.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.forPath("/config/demo/com.alipay.sofa.rpc.test.HelloService", "timeout=5000".getBytes());
} else {
zkClient.setData().forPath("/config/demo/com.alipay.sofa.rpc.test.HelloService",
"timeout=5000".getBytes());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve code maintainability and error handling.

Consider the following improvements:

  1. Extract the Zookeeper path to a constant to avoid duplication and potential inconsistencies
  2. Use try-with-resources for better error handling

Here's the suggested implementation:

private static final String CONFIG_PATH = "/config/demo/com.alipay.sofa.rpc.test.HelloService";

// Usage
if (zkClient.checkExists().forPath(CONFIG_PATH) == null) {
    try (var transaction = zkClient.inTransaction()) {
        transaction.create()
            .creatingParentsIfNeeded()
            .withMode(CreateMode.PERSISTENT)
            .forPath(CONFIG_PATH, "timeout=5000".getBytes())
            .commit();
    }
} else {
    try (var transaction = zkClient.inTransaction()) {
        transaction.setData()
            .forPath(CONFIG_PATH, "timeout=5000".getBytes())
            .commit();
    }
}

Comment on lines 67 to 94
// 测试配置新增
String configValue = "timeout=5000\n";
ConfigChange configChange = new ConfigChange("application", consumerConfig.getInterfaceId(), null, configValue, PropertyChangeType.ADDED);
Map<String, ConfigChange> changes= new HashMap<>();
changes.put(configChange.getPropertyName(), configChange);
ConfigChangeEvent event = new ConfigChangeEvent("application",changes);
apolloConfigListener.onChange(event);
Assert.assertEquals(5000, consumerConfig.getMethodTimeout("sayHello"));

// 测试配置修改
String oldValue = configValue;
configValue = "timeout=5000\n.sayHello.timeout=6000";
configChange = new ConfigChange("application", consumerConfig.getInterfaceId(), oldValue, configValue, PropertyChangeType.MODIFIED);
changes= new HashMap<>();
changes.put(configChange.getPropertyName(), configChange);
event = new ConfigChangeEvent("application",changes);
apolloConfigListener.onChange(event);
Assert.assertEquals(6000, consumerConfig.getMethodTimeout("sayHello"));

// 测试配置删除
configChange = new ConfigChange("application", consumerConfig.getInterfaceId(), configValue, null, PropertyChangeType.DELETED);
changes= new HashMap<>();
changes.put(configChange.getPropertyName(), configChange);
event = new ConfigChangeEvent("application",changes);
apolloConfigListener.onChange(event);
Assert.assertEquals(-1, consumerConfig.getMethodTimeout("sayHello"));

System.clearProperty(DynamicConfigKeys.CENTER_ADDRESS.getKey());
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance test scenarios and assertions.

The test scenarios could be more robust:

  1. Extract configuration values as constants
  2. Add assertions for configuration change events
  3. Consider potential race conditions
  4. Add negative test cases

Example improvement:

+ private static final String INITIAL_TIMEOUT = "timeout=5000\n";
+ private static final String MODIFIED_TIMEOUT = "timeout=5000\n.sayHello.timeout=6000";
+ private static final int EXPECTED_INITIAL_TIMEOUT = 5000;
+ private static final int EXPECTED_METHOD_TIMEOUT = 6000;
+ private static final int EXPECTED_DEFAULT_TIMEOUT = -1;

  // Test configuration addition
- String configValue = "timeout=5000\n";
+ String configValue = INITIAL_TIMEOUT;
  ConfigChange configChange = new ConfigChange("application", consumerConfig.getInterfaceId(), 
      null, configValue, PropertyChangeType.ADDED);
  // ... existing code ...
+ // Verify the change was processed
+ Thread.sleep(100); // Allow time for event processing
  Assert.assertEquals(5000, consumerConfig.getMethodTimeout("sayHello"));
+ Assert.assertTrue("Configuration should be marked as changed", 
+     consumerConfig.isDynamicConfigurationUpdated());

  // Add negative test
+ try {
+     configChange = new ConfigChange("application", consumerConfig.getInterfaceId(),
+         null, "invalid=config", PropertyChangeType.ADDED);
+     apolloConfigListener.onChange(event);
+     Assert.fail("Should throw exception for invalid configuration");
+ } catch (IllegalArgumentException expected) {
+     // Expected exception
+ }

Committable suggestion was skipped due to low confidence.

protected ApolloDynamicConfigManager(String appName, String remainUrl) {
super(appName, remainUrl);
System.setProperty(APOLLO_APPID_KEY, appName);
System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + getAddress());
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add a check for empty address before setting apollo.meta

In the constructor, getAddress() may return an empty string. Appending it to APOLLO_PROTOCOL_PREFIX results in apollo.meta being set to "http://", which is likely invalid and could lead to misconfigurations.

Consider adding a check to ensure getAddress() is not blank before setting the system property, similar to the check in the other constructor.

Apply this diff to add the check:

protected ApolloDynamicConfigManager(String appName, String remainUrl) {
    super(appName, remainUrl);
    System.setProperty(APOLLO_APPID_KEY, appName);
+   if (StringUtils.isNotBlank(getAddress())) {
        System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + getAddress());
+   }
    Map params = getParams();
    if (params != null && params.containsKey("cluster")) {
        String clusterValue = (String) params.get("cluster");
        System.setProperty(APOLLO_CLUSTER_KEY, clusterValue);
    }
    config = ConfigService.getAppConfig();
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + getAddress());
protected ApolloDynamicConfigManager(String appName, String remainUrl) {
super(appName, remainUrl);
System.setProperty(APOLLO_APPID_KEY, appName);
if (StringUtils.isNotBlank(getAddress())) {
System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + getAddress());
}
Map params = getParams();
if (params != null && params.containsKey("cluster")) {
String clusterValue = (String) params.get("cluster");
System.setProperty(APOLLO_CLUSTER_KEY, clusterValue);
}
config = ConfigService.getAppConfig();
}

Comment on lines 115 to 121
String configValue = configService.getConfig(
DynamicConfigKeyHelper.buildProviderServiceProKey(service, key),
group, DEFAULT_TIMEOUT);
return configValue != null ? configValue : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (NacosException e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Log exceptions in catch blocks for better debugging

In the methods getProviderServiceProperty, getConsumerServiceProperty, getProviderMethodProperty, and getConsumerMethodProperty, exceptions are caught but not logged. Logging exceptions can aid in debugging and provide visibility into issues when they occur.

Apply this diff to log the exceptions:

@@ -119,6 +119,7 @@ public String getProviderServiceProperty(String service, String key) {
         } catch (NacosException e) {
+            LOGGER.error("Failed to get provider service property for service: {}, key: {}.", service, key, e);
             return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
         }
     }
@@ -133,6 +134,7 @@ public String getConsumerServiceProperty(String service, String key) {
         } catch (NacosException e) {
+            LOGGER.error("Failed to get consumer service property for service: {}, key: {}.", service, key, e);
             return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
         }
     }
@@ -145,6 +147,7 @@ public String getProviderMethodProperty(String service, String method, String ke
         } catch (NacosException e) {
+            LOGGER.error("Failed to get provider method property for service: {}, method: {}, key: {}.", service, method, key, e);
             return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
         }
     }
@@ -157,6 +160,7 @@ public String getConsumerMethodProperty(String service, String method, String ke
         } catch (NacosException e) {
+            LOGGER.error("Failed to get consumer method property for service: {}, method: {}, key: {}.", service, method, key, e);
             return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
         }
     }

Also applies to: 127-133, 139-145, 151-157

Comment on lines +178 to +182
private NacosConfigListener createTargetListener(String key) {
NacosConfigListener configListener = new NacosConfigListener();
configListener.fillContext(key, group);
return configListener;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure NacosConfigListener has the fillContext method

In the createTargetListener method, the fillContext method is called on NacosConfigListener. Ensure that NacosConfigListener implements or inherits the fillContext method to prevent potential compilation errors.

If AbstractSharedListener doesn't provide this method, consider adding it to NacosConfigListener:

public class NacosConfigListener extends AbstractSharedListener {

    private String dataId;
    private String group;

    public void fillContext(String dataId, String group) {
        this.dataId = dataId;
        this.group = group;
    }

    // Existing code...
}

Comment on lines 458 to 495
private Map<String, String> newValueMap = new HashMap<>();

// 动态配置项
private final Set<String> dynamicConfigKeys = new HashSet<>();

ConsumerAttributeListener() {
dynamicConfigKeys.add(RpcConstants.CONFIG_KEY_TIMEOUT);
dynamicConfigKeys.add(RpcConstants.CONFIG_KEY_RETRIES);
dynamicConfigKeys.add(RpcConstants.CONFIG_KEY_LOADBALANCER);
}

@Override
public void process(ConfigChangedEvent event) {
// 清除上次的赋值,并保留赋值过的key
newValueMap.replaceAll((k, v) -> "");
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
// ADDED or MODIFIED
parseConfigurationLines(event.getContent());
}
attrUpdated(newValueMap);
}

private void parseConfigurationLines(String content) {
String[] lines = content.split("\n");
for (String line : lines) {
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
String tempKey = key.lastIndexOf(".") == -1 ? key : key.substring(key.lastIndexOf(".") + 1);
if (dynamicConfigKeys.contains(tempKey)) {
newValueMap.put(key, value);
}
} else {
LOGGER.warn("Malformed configuration line: {}", line);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure thread safety of newValueMap in ConsumerAttributeListener

The newValueMap is a HashMap that is accessed and modified in the process and parseConfigurationLines methods. If process can be called concurrently by multiple threads, this could lead to race conditions and inconsistent state. Consider synchronizing the process method or using a thread-safe map implementation, such as ConcurrentHashMap, for newValueMap.

Apply one of the following changes to fix the issue:

Option 1: Use ConcurrentHashMap for newValueMap:

- private Map<String, String> newValueMap = new HashMap<>();
+ private Map<String, String> newValueMap = new ConcurrentHashMap<>();

Option 2: Synchronize the process method:

- public void process(ConfigChangedEvent event) {
+ public synchronized void process(ConfigChangedEvent event) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Map<String, String> newValueMap = new HashMap<>();
// 动态配置项
private final Set<String> dynamicConfigKeys = new HashSet<>();
ConsumerAttributeListener() {
dynamicConfigKeys.add(RpcConstants.CONFIG_KEY_TIMEOUT);
dynamicConfigKeys.add(RpcConstants.CONFIG_KEY_RETRIES);
dynamicConfigKeys.add(RpcConstants.CONFIG_KEY_LOADBALANCER);
}
@Override
public void process(ConfigChangedEvent event) {
// 清除上次的赋值,并保留赋值过的key
newValueMap.replaceAll((k, v) -> "");
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
// ADDED or MODIFIED
parseConfigurationLines(event.getContent());
}
attrUpdated(newValueMap);
}
private void parseConfigurationLines(String content) {
String[] lines = content.split("\n");
for (String line : lines) {
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
String tempKey = key.lastIndexOf(".") == -1 ? key : key.substring(key.lastIndexOf(".") + 1);
if (dynamicConfigKeys.contains(tempKey)) {
newValueMap.put(key, value);
}
} else {
LOGGER.warn("Malformed configuration line: {}", line);
}
}
}
private Map<String, String> newValueMap = new ConcurrentHashMap<>();
// 动态配置项
private final Set<String> dynamicConfigKeys = new HashSet<>();
ConsumerAttributeListener() {
dynamicConfigKeys.add(RpcConstants.CONFIG_KEY_TIMEOUT);
dynamicConfigKeys.add(RpcConstants.CONFIG_KEY_RETRIES);
dynamicConfigKeys.add(RpcConstants.CONFIG_KEY_LOADBALANCER);
}
@Override
public void process(ConfigChangedEvent event) {
// 清除上次的赋值,并保留赋值过的key
newValueMap.replaceAll((k, v) -> "");
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
// ADDED or MODIFIED
parseConfigurationLines(event.getContent());
}
attrUpdated(newValueMap);
}
private void parseConfigurationLines(String content) {
String[] lines = content.split("\n");
for (String line : lines) {
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
String tempKey = key.lastIndexOf(".") == -1 ? key : key.substring(key.lastIndexOf(".") + 1);
if (dynamicConfigKeys.contains(tempKey)) {
newValueMap.put(key, value);
}
} else {
LOGGER.warn("Malformed configuration line: {}", line);
}
}
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Outside diff range and nitpick comments (7)
core/api/src/test/java/com/alipay/sofa/rpc/dynamic/DynamicUrlTest.java (2)

22-25: Enhance class-level documentation.

The class documentation should include:

  • Purpose of these tests
  • Description of DynamicUrl's role in the dynamic configuration system
  • Test coverage overview
 /**
+ * Test cases for DynamicUrl class which handles parsing and validation of configuration center addresses.
+ * These tests verify the URL parsing functionality for various formats including:
+ * - URLs with path and parameters
+ * - URLs with trailing slash and parameters
+ * - URLs with only parameters
+ * - URLs with only host and port
  * @author Narziss
  * @version DynamicUrl.java, v 0.1 2024年10月28日 21:40 Narziss
  */

26-79: Consider adding integration tests.

While these unit tests cover the URL parsing well, consider adding integration tests that verify:

  1. Integration with actual Zookeeper/Nacos/Apollo config centers
  2. Dynamic configuration updates through these URLs
  3. Error handling when config centers are unavailable

This would help ensure the complete dynamic configuration flow works as expected.

core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (3)

39-44: Add validation for appName parameter.

While the configCenterAddress validation is good, consider adding similar validation for appName to maintain consistency.

 protected DynamicConfigManager(String appName, String configCenterAddress) {
+    if (StringUtils.isBlank(appName)) {
+        throw new IllegalArgumentException("appName must not be blank");
+    }
     this.appName = appName;
     if (StringUtils.isNotBlank(configCenterAddress)) {
         this.dynamicUrl = new DynamicUrl(configCenterAddress);
     }
 }

118-125: Enhance addListener documentation.

Consider adding more detailed documentation for the addListener method:

  • Explain when this method should be used
  • Describe the expected behavior when a listener is added
  • Specify if multiple listeners per key are supported
     /**
      * Add config listener.
      *
+     * Registers a listener for configuration changes identified by the given key.
+     * The listener will be notified when the configuration value for the key changes.
+     * Multiple listeners may be registered for the same key.
      *
      * @param key      config key
      * @param listener config listener
+     * @throws IllegalArgumentException if key or listener is null
      */
     public void addListener(String key, ConfigListener listener) {
     }

Line range hint 31-126: Consider implementing a builder pattern for complex configuration scenarios.

The current design is good for basic configuration management, but as the configuration options grow (with Zookeeper, Nacos, etc.), consider implementing a builder pattern to handle the increasing complexity of configuration initialization. This would make it easier to:

  • Add new configuration sources
  • Handle different combinations of configuration options
  • Maintain backward compatibility
  • Provide a more fluent API for configuration setup
bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (1)

503-503: Remove empty configChanged method.

The configChanged method is empty and appears to be unused. If it's not required by the interface contract, consider removing it.

config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1)

61-61: Declare configMap as final

The configMap is only initialized once and not reassigned. Declaring it as final can improve clarity and thread safety.

Apply this diff:

- private ConcurrentMap<String, String> configMap = new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, String> configMap = new ConcurrentHashMap<>();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 7a354b3 and b8c60ae.

📒 Files selected for processing (12)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (7 hunks)
  • config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (6 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManagerFactory.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicUrl.java (1 hunks)
  • core/api/src/test/java/com/alipay/sofa/rpc/dynamic/DynamicUrlTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManagerFactory.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ZookeeperDynamicConfigTest.java
🔇 Additional comments (7)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicUrl.java (1)

37-37: Verify support for all configuration center types

The comment shows an example for Apollo format (apollo://), but the PR objectives mention Zookeeper and Nacos integration.

✅ Verification successful

Let me check the test file to understand the supported URL formats.


Let me check the actual DynamicUrl implementation to see if it's protocol-agnostic.


Let me check the Nacos implementation to understand how different protocols are handled.


Based on the evidence gathered, I can now provide the final response:

The URL parsing implementation correctly supports all configuration center types

The DynamicUrl class is protocol-agnostic and properly handles URLs for all configuration center types:

  • The implementation uses a generic regex pattern that works with any protocol (apollo://, nacos://, zookeeper://)
  • The codebase includes implementations for all three configuration centers:
    • NacosDynamicConfigManager
    • ApolloDynamicConfigManager
    • ZookeeperDynamicConfigManager
  • Integration tests exist for all three protocols in test-integration

The example comment showing apollo:// format is just for illustration and doesn't limit the functionality.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for configuration center URL patterns in the codebase
echo "Searching for configuration center URL patterns..."
rg -l "zookeeper://" "nacos://" "apollo://"

# Search for test cases covering different URL formats
echo "Searching for related test cases..."
rg -l "DynamicUrlTest" --type java

Length of output: 436


Script:

#!/bin/bash
# Check the test cases in DynamicUrlTest.java
cat core/api/src/test/java/com/alipay/sofa/rpc/dynamic/DynamicUrlTest.java

# Search for URL parsing implementation
ast-grep --pattern 'class DynamicUrl {
  $$$
  parse($$$) {
    $$$
  }
  $$$
}'

Length of output: 3825


Script:

#!/bin/bash
# Check the DynamicUrl implementation
cat core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicUrl.java

# Search for any protocol-specific handling in the codebase
rg -l "DynamicConfigFactory|ConfigFactory" --type java

Length of output: 3221


Script:

#!/bin/bash
# Check Nacos implementation
cat config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java

# Search for other config implementations
fd -e java -x grep -l "DynamicConfigManager" {}

Length of output: 10108

core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java (3)

20-22: LGTM! Import statements are appropriate.

The added imports for StringUtils and ConfigListener support the new functionality.


46-52: LGTM! Getter methods are well-implemented.

The getter methods follow Java conventions and provide appropriate access to private fields.


31-33: Verify DynamicUrl class existence and implementation.

The field additions look good, but let's ensure the DynamicUrl class is properly implemented.

✅ Verification successful

DynamicUrl class is properly implemented and well-tested

The DynamicUrl class exists and is well-implemented with:

  • Core URL parsing functionality with protocol, host, port, path and parameters
  • Comprehensive test coverage in DynamicUrlTest.java
  • Active usage in multiple config implementations (Apollo, Zookeeper, Nacos)
  • Clean integration with DynamicConfigManager
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for DynamicUrl class definition
ast-grep --pattern 'class DynamicUrl {
  $$$
}'

# Search for DynamicUrl usage patterns
rg "DynamicUrl" -A 5

Length of output: 14083

config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (1)

128-132: ⚠️ Potential issue

Prevent multiple registrations of the same listener

In the addListener method, calling config.addChangeListener(apolloListener, Collections.singleton(key)); each time may result in the same ApolloListener being registered multiple times for the same key. This can lead to duplicate notifications and performance overhead.

Consider adding a registration check to ensure the listener is added only once:

public void addListener(String key, ConfigListener listener) {
    ApolloListener apolloListener = watchListenerMap.computeIfAbsent(key, k -> new ApolloListener());
    apolloListener.addListener(listener);
+   if (!apolloListener.isRegistered()) {
        config.addChangeListener(apolloListener, Collections.singleton(key));
+       apolloListener.setRegistered(true);
+   }
}

You'll need to modify the ApolloListener class:

public class ApolloListener implements ConfigChangeListener {
    private Set<ConfigListener> listeners = new CopyOnWriteArraySet<>();
+   private volatile boolean registered = false;

    void addListener(ConfigListener configListener) {
        this.listeners.add(configListener);
    }

+   boolean isRegistered() {
+       return registered;
+   }

+   void setRegistered(boolean registered) {
+       this.registered = registered;
+   }

    // existing methods...
}

Likely invalid or redundant comment.

config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (2)

73-73: ⚠️ Potential issue

Verify the Logic of Dynamic Refresh Condition

The condition if (!getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE)) might cause the dynamic refresh features to initialize only when DYNAMIC_REFRESH_ENABLE is set to false. Typically, we expect dynamic refresh features to be initialized when DYNAMIC_REFRESH_ENABLE is true.

Please confirm if this logic is intentional. If not, consider updating the condition:

- if (!getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE)) {
+ if (getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE)) {

210-224: ⚠️ Potential issue

Handle Potential Null Values in nodeChanged() Method

In the nodeChanged() method, there is a possibility of a NullPointerException if childData or childData.getData() is null. Ensure that you check for null values before accessing them.

Apply this diff to handle potential null values safely:

@Override
public void nodeChanged() throws Exception {
    ChildData childData = nodeCache.getCurrentData();
    String content = null;
    ConfigChangeType changeType;
    if (childData == null) {
        changeType = ConfigChangeType.DELETED;
    } else {
+       if (childData.getData() != null) {
            content = new String(childData.getData(), RpcConstants.DEFAULT_CHARSET);
+       }
        int version = childData.getStat().getVersion();
        if (version == 0) {
            changeType = ConfigChangeType.ADDED;
        } else {
            changeType = ConfigChangeType.MODIFIED;
        }
    }
    ConfigChangedEvent configChangeEvent = new ConfigChangedEvent(pathKey, content, changeType);
    listeners.forEach(listener -> listener.process(configChangeEvent));
}

Likely invalid or redundant comment.

Comment on lines +91 to +93
public String getParam(String key) {
return params.get(key);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding getParam with default value

The current getParam implementation returns null for missing keys. Consider adding an overloaded method that accepts a default value.

 public String getParam(String key) {
     return params.get(key);
 }
+
+ public String getParam(String key, String defaultValue) {
+     return params.getOrDefault(key, defaultValue);
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String getParam(String key) {
return params.get(key);
}
public String getParam(String key) {
return params.get(key);
}
public String getParam(String key, String defaultValue) {
return params.getOrDefault(key, defaultValue);
}

private final String host;
private final int port;
private final String path;
private final Map<String, String> params = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider making params Map immutable

The params Map is exposed via getParams() which could allow external modifications. Consider using Collections.unmodifiableMap() in the getter to prevent modifications to the internal state.

- private final Map<String, String> params = new HashMap<>();
+ private final Map<String, String> params = new HashMap<>();
+ 
+ public Map<String, String> getParams() {
+     return Collections.unmodifiableMap(params);
+ }

Committable suggestion was skipped due to low confidence.

Comment on lines +57 to +65
private void parseQueryParams(String query) {
String[] paramPairs = query.split("&");
for (String paramPair : paramPairs) {
String[] keyValue = paramPair.split("=", 2);
if (keyValue.length == 2) {
params.put(keyValue[0], keyValue[1]);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enhance query parameter parsing robustness

The current implementation doesn't handle:

  • URL encoded characters (e.g., %20 for spaces)
  • Empty parameter values
  • Duplicate parameter names
  • Special characters in parameter names/values

Consider using URLDecoder and adding validation:

 private void parseQueryParams(String query) {
+    if (query == null || query.isEmpty()) {
+        return;
+    }
     String[] paramPairs = query.split("&");
     for (String paramPair : paramPairs) {
         String[] keyValue = paramPair.split("=", 2);
-        if (keyValue.length == 2) {
-            params.put(keyValue[0], keyValue[1]);
+        if (keyValue.length > 0) {
+            String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
+            String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8) : "";
+            if (!key.isEmpty()) {
+                params.put(key, value);
+            }
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void parseQueryParams(String query) {
String[] paramPairs = query.split("&");
for (String paramPair : paramPairs) {
String[] keyValue = paramPair.split("=", 2);
if (keyValue.length == 2) {
params.put(keyValue[0], keyValue[1]);
}
}
}
private void parseQueryParams(String query) {
if (query == null || query.isEmpty()) {
return;
}
String[] paramPairs = query.split("&");
for (String paramPair : paramPairs) {
String[] keyValue = paramPair.split("=", 2);
if (keyValue.length > 0) {
String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8) : "";
if (!key.isEmpty()) {
params.put(key, value);
}
}
}
}

Comment on lines 38 to 55
public DynamicUrl(String configCenterAddress) {
// 正则表达式解析协议、主机、端口、路径和参数,其中路径和参数是可选的
String regex = "^(\\w+)://([^:/]+):(\\d+)(/[^?]*)?(\\?.*)?$";
Matcher matcher = Pattern.compile(regex).matcher(configCenterAddress);
if (matcher.find()) {
this.protocol = matcher.group(1);
this.host = matcher.group(2);
this.port = Integer.parseInt(matcher.group(3));
// 判断路径是否为空或者为 "/"
this.path = (matcher.group(4) != null && !matcher.group(4).equals("/")) ? matcher.group(4) : "";
this.address = this.host + ":" + this.port + this.path;
if (matcher.group(5) != null) {
parseQueryParams(matcher.group(5).substring(1));
}
} else {
throw new IllegalArgumentException("Invalid URL format");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Several improvements needed for URL parsing

  1. The regex pattern should be pre-compiled as a static final field for better performance
  2. Missing null check for configCenterAddress
  3. Consider handling URL encoding/decoding for special characters

Apply these improvements:

+ private static final Pattern URL_PATTERN = Pattern.compile("^(\\w+)://([^:/]+):(\\d+)(/[^?]*)?(\\?.*)?$");

public DynamicUrl(String configCenterAddress) {
+     if (configCenterAddress == null) {
+         throw new IllegalArgumentException("configCenterAddress cannot be null");
+     }
-     String regex = "^(\\w+)://([^:/]+):(\\d+)(/[^?]*)?(\\?.*)?$";
-     Matcher matcher = Pattern.compile(regex).matcher(configCenterAddress);
+     Matcher matcher = URL_PATTERN.matcher(configCenterAddress);

Committable suggestion was skipped due to low confidence.

Comment on lines +28 to +52
@Test
public void testWithPathAndParams() {
DynamicUrl dynamicUrl = new DynamicUrl("apollo://127.0.0.1:8080/config?appId=xxx&cluster=yyy");
Assert.assertEquals("apollo", dynamicUrl.getProtocol());
Assert.assertEquals("127.0.0.1", dynamicUrl.getHost());
Assert.assertEquals(8080, dynamicUrl.getPort());
Assert.assertEquals("/config", dynamicUrl.getPath());
Assert.assertEquals("127.0.0.1:8080/config", dynamicUrl.getAddress());
Assert.assertNotNull(dynamicUrl.getParams());
Assert.assertEquals("xxx", dynamicUrl.getParams().get("appId"));
Assert.assertEquals("yyy", dynamicUrl.getParams().get("cluster"));
}

@Test
public void testWithSlashAndParams() {
DynamicUrl dynamicUrl = new DynamicUrl("apollo://127.0.0.1:8080/?appId=xxx&cluster=yyy");
Assert.assertEquals("apollo", dynamicUrl.getProtocol());
Assert.assertEquals("127.0.0.1", dynamicUrl.getHost());
Assert.assertEquals(8080, dynamicUrl.getPort());
Assert.assertEquals("", dynamicUrl.getPath());// 如果路径为空,返回空字符串
Assert.assertEquals("127.0.0.1:8080", dynamicUrl.getAddress());
Assert.assertNotNull(dynamicUrl.getParams());
Assert.assertEquals("xxx", dynamicUrl.getParams().get("appId"));
Assert.assertEquals("yyy", dynamicUrl.getParams().get("cluster"));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Translate Chinese comment to English and add edge cases.

The test cases look good but have a few suggestions:

  1. Translate the Chinese comment to English for consistency
  2. Add edge cases for port validation
- Assert.assertEquals("", dynamicUrl.getPath());// 如果路径为空,返回空字符串
+ Assert.assertEquals("", dynamicUrl.getPath());// Returns empty string if path is empty

Consider adding these test cases:

@Test(expected = IllegalArgumentException.class)
public void testInvalidPort() {
    new DynamicUrl("apollo://127.0.0.1:65536");  // Port > 65535
}

@Test(expected = IllegalArgumentException.class)
public void testNegativePort() {
    new DynamicUrl("apollo://127.0.0.1:-1");
}

Comment on lines +104 to +107
@Override
public void initServiceConfiguration(String service) {
// TODO 暂不支持
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Implement the initServiceConfiguration(String service) Method

The initServiceConfiguration(String service) method is currently unimplemented and contains a TODO comment. Implementing this method is crucial for initializing service-specific configurations.

Would you like assistance in generating the implementation code or opening a GitHub issue to track this task?

Comment on lines +167 to +171
@Override
public AuthRuleGroup getServiceAuthRule(String service) {
//TODO 暂不支持
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Implement the getServiceAuthRule(String service) Method

The getServiceAuthRule(String service) method currently returns null and contains a TODO comment. To avoid potential NullPointerException and to fulfill its contract, please implement this method.

Would you like assistance in generating the implementation code or opening a GitHub issue to track this task?

Comment on lines +195 to +203
this.pathKey = pathKey;
this.nodeCache = new NodeCache(zkClient, pathKey);
nodeCache.getListenable().addListener(this);
try {
nodeCache.start();
} catch (Exception e) {
LOGGER.error("Failed to add listener for path:{}", pathKey, e);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure Resources are Properly Closed

The ZookeeperConfigListener class initializes nodeCache but does not provide a mechanism to close it. Not closing nodeCache can lead to resource leaks.

Consider adding a close() method to the ZookeeperConfigListener class to properly release resources:

public void close() {
    try {
        if (nodeCache != null) {
            nodeCache.close();
        }
    } catch (IOException e) {
        LOGGER.error("Failed to close nodeCache for path:{}", pathKey, e);
    }
}

Additionally, ensure that ZookeeperConfigListener instances are closed when no longer needed.

Comment on lines +230 to +231

}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Provide a Method to Close Zookeeper Resources

The class lacks a method to close the zkClient and any associated caches or listeners. This can lead to resource leaks and potential memory issues.

Consider adding a close() method to the ZookeeperDynamicConfigManager class:

public void close() {
    if (zkClient != null) {
        zkClient.close();
    }
    // Close any caches or listeners if applicable
}

Ensure that this method is called when the dynamic configuration manager is no longer needed.

Comment on lines +127 to +134
public String getProviderServiceProperty(String service, String key) {
try {
String configValue = configMap.get(DynamicConfigKeyHelper.buildProviderServiceProKey(service, key));
return configValue != null ? configValue : DynamicHelper.DEFAULT_DYNAMIC_VALUE;
} catch (Exception e) {
return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Log Exceptions in Configuration Retrieval Methods

In the methods for retrieving configuration properties, exceptions are caught but not logged. This can make debugging difficult.

Apply this diff to each catch block:

} catch (Exception e) {
+    LOGGER.error("Error retrieving configuration for key: " + key + ", service: " + service, e);
     return DynamicHelper.DEFAULT_DYNAMIC_VALUE;
}

This change applies to the following methods:

  • getProviderServiceProperty
  • getConsumerServiceProperty
  • getProviderMethodProperty
  • getConsumerMethodProperty

Also applies to: 137-144, 147-154, 157-164

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Outside diff range and nitpick comments (5)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (1)

41-42: Remove unnecessary semicolon.

There's an extra semicolon on line 42 that should be removed.

-    private Map<String, String> dynamicValueMap = new HashMap<>();
-    ;
+    private Map<String, String> dynamicValueMap = new HashMap<>();
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicUrl.java (1)

24-27: Enhance class documentation

The class documentation should include:

  • Purpose and responsibilities of the class
  • Format specification for the configCenterAddress
  • Examples of valid URLs
  • Description of thrown exceptions

Add comprehensive documentation:

 /**
+ * Parses and manages URLs for dynamic configuration centers.
+ * 
+ * <p>Supports URLs in the format:
+ * <pre>protocol://host:port/path?param1=value1&param2=value2</pre>
+ * where path and query parameters are optional.
+ *
+ * <p>Example valid URLs:
+ * <ul>
+ *   <li>zookeeper://localhost:2181/config?cluster=default</li>
+ *   <li>nacos://127.0.0.1:8848</li>
+ * </ul>
+ *
+ * @throws IllegalArgumentException if the URL format is invalid
  * @author Narziss
  * @version DynamicUrl.java, v 0.1 2024年10月28日 21:23 Narziss
  */
bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (1)

494-494: Remove empty configChanged method

The configChanged method is empty and appears to be unused. If it's not required by an interface contract, consider removing it.

core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (2)

218-220: Translate comments to English for consistency

Please consider translating the code comments to English to maintain consistency across the codebase.


957-959: Consider consistent updating of dynamicConfigValueCache

The current logic updates dynamicConfigValueCache only if it already contains the key. This might lead to missing updates for new properties. Consider removing the containsKey check to ensure all updates are applied.

Apply this diff to update the cache unconditionally:

- if (dynamicConfigValueCache.containsKey(property)) {
     dynamicConfigValueCache.put(property, newValue);
- }

Also applies to: 992-994

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between b8c60ae and 08a0a85.

📒 Files selected for processing (9)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (8 hunks)
  • config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (8 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicUrl.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigKeys.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java
🔇 Additional comments (4)
core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (2)

72-86: LGTM!

The getter implementations are clean and straightforward.


31-88: Verify integration with existing event handling system.

Let's verify how this new event class integrates with the existing event handling system and ensure consistent patterns are followed.

✅ Verification successful

Let me gather more specific information about the event handling implementation.


The ConfigChangedEvent implementation is well-integrated with the existing event system

The verification shows that:

  • The event class follows the established event handling patterns in the codebase
  • It's properly integrated into the dynamic configuration system with:
    • ConfigListener interface that defines the event processing contract
    • Multiple implementations (Zookeeper, Nacos, Apollo) that correctly use the event
    • Consistent parsing and handling of configuration changes across implementations
  • The event's structure (key, content, type) aligns with the requirements of all configuration manager implementations

The implementation is correct and maintains consistency with the existing event handling patterns in the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for similar event classes and their usage patterns
echo "Searching for other EventObject implementations..."
ast-grep --pattern 'class $_ extends EventObject {
  $$$
}'

echo -e "\nSearching for potential consumers of ConfigChangedEvent..."
rg -l "ConfigChangedEvent"

echo -e "\nSearching for existing event handling patterns..."
rg -l "EventObject|addListener|fireEvent"

Length of output: 4203


Script:

#!/bin/bash
# Check ConfigListener implementation and usage
echo "Checking ConfigListener implementation..."
cat core/api/src/main/java/com/alipay/sofa/rpc/listener/ConfigListener.java

echo -e "\nChecking DynamicConfigManager usage..."
cat core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicConfigManager.java

echo -e "\nChecking implementation in one of the config managers..."
cat config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java

Length of output: 15538

core/api/src/main/java/com/alipay/sofa/rpc/dynamic/DynamicUrl.java (1)

28-28: Verify integration with configuration center clients

Since this class will be used by both Zookeeper and Nacos implementations, we should verify that the URL format matches their client requirements.

✅ Verification successful

DynamicUrl implementation is compatible with both configuration centers

The verification shows that the DynamicUrl class correctly handles URL formats required by both Zookeeper and Nacos implementations:

  • Zookeeper uses simple host:port format (e.g., "zookeeper://127.0.0.1:2181")
  • Nacos supports host:port with optional authentication params (username/password)
  • Test cases verify all required URL formats including paths and parameters
  • Both configuration managers successfully use DynamicUrl to parse their respective connection strings
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for Zookeeper and Nacos client usage to verify URL format compatibility
echo "Searching for Zookeeper client usage..."
rg -l "org\.apache\.zookeeper\.ZooKeeper.*new" 

echo "Searching for Nacos client usage..."
rg -l "com\.alibaba\.nacos\.api\.NacosFactory"

Length of output: 267


Script:

#!/bin/bash
# Let's try a broader search for configuration center implementations and URL usage

# Search for Zookeeper related files
echo "=== Searching for Zookeeper related files ==="
fd -e java . | rg -l "zookeeper|ZooKeeper"

# Search for Nacos related files
echo -e "\n=== Searching for Nacos related files ==="
fd -e java . | rg -l "nacos|Nacos"

# Search for DynamicUrl usage
echo -e "\n=== Searching for DynamicUrl usage ==="
rg -l "DynamicUrl"

# Look at the actual DynamicUrl implementation
echo -e "\n=== DynamicUrl implementation ==="
find . -name "DynamicUrl.java" -exec cat {} \;

Length of output: 4357


Script:

#!/bin/bash
# Let's examine how the DynamicUrl is used in the configuration managers

echo "=== ZookeeperDynamicConfigManager Usage ==="
cat config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java

echo -e "\n=== NacosDynamicConfigManager Usage ==="
cat config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java

echo -e "\n=== DynamicUrl Test Cases ==="
cat core/api/src/test/java/com/alipay/sofa/rpc/dynamic/DynamicUrlTest.java

Length of output: 22490

bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (1)

167-173: ⚠️ Potential issue

Add error handling for request-level dynamic configuration initialization

The initialization of request-level dynamic configuration lacks proper error handling. If getDynamicManager returns null or initServiceConfiguration fails, it could lead to runtime errors.

Apply this diff to add error handling:

 if (StringUtils.isNotBlank(dynamicAlias)) {
+    try {
         final DynamicConfigManager dynamicManager = DynamicConfigManagerFactory.getDynamicManager(
                 consumerConfig.getAppName(), dynamicAlias);
+        if (dynamicManager == null) {
+            LOGGER.warn("Failed to get DynamicConfigManager for alias: {}", dynamicAlias);
+            return proxyIns;
+        }
         dynamicManager.initServiceConfiguration(consumerConfig.getInterfaceId());
+    } catch (Exception e) {
+        LOGGER.error("Failed to initialize request-level dynamic configuration", e);
+    }
 }

Likely invalid or redundant comment.

private final ConfigChangeType changeType;


private Map<String, String> dynamicValueMap = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Protect against external modifications of dynamicValueMap.

The dynamicValueMap is mutable and exposed through a getter, which could lead to unexpected modifications. Consider making it immutable.

-    private Map<String, String> dynamicValueMap = new HashMap<>();
+    private final Map<String, String> dynamicValueMap = new HashMap<>();

     public Map<String, String> getDynamicValueMap() {
-        return dynamicValueMap;
+        return Collections.unmodifiableMap(dynamicValueMap);
     }

Committable suggestion was skipped due to low confidence.

Comment on lines 44 to 56
public ConfigChangedEvent(String key, String content) {
this(key, content, ConfigChangeType.MODIFIED);
}

public ConfigChangedEvent(String key, String content, ConfigChangeType changeType) {
super(key);
this.key = key;
this.content = content;
this.changeType = changeType;
if (content != null) {
parseConfigurationLines(content);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add parameter validation and improve EventObject source.

The constructors should validate the key parameter and use a more meaningful source object for the EventObject.

     public ConfigChangedEvent(String key, String content, ConfigChangeType changeType) {
-        super(key);
+        super(new Object()); // or a more meaningful source object
+        if (key == null) {
+            throw new IllegalArgumentException("key cannot be null");
+        }
         this.key = key;
         this.content = content;
         this.changeType = changeType;
         if (content != null) {
             parseConfigurationLines(content);
         }
     }

Committable suggestion was skipped due to low confidence.

Comment on lines 58 to 70
private void parseConfigurationLines(String content) {
String[] lines = content.split("\n");
for (String line : lines) {
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String mapKey = keyValue[0].trim();
String mapValue = keyValue[1].trim();
dynamicValueMap.put(mapKey, mapValue);
} else {
LOGGER.warn("Malformed configuration line: {}", line);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enhance configuration parsing robustness.

The parsing logic needs improvements to handle edge cases and provide better error handling.

     private void parseConfigurationLines(String content) {
         String[] lines = content.split("\n");
         for (String line : lines) {
+            if (line == null || line.trim().isEmpty() || line.trim().startsWith("#")) {
+                continue;
+            }
+            line = line.trim();
             String[] keyValue = line.split("=", 2);
             if (keyValue.length == 2) {
                 String mapKey = keyValue[0].trim();
                 String mapValue = keyValue[1].trim();
+                if (mapKey.isEmpty()) {
+                    LOGGER.warn("Empty key in configuration line: {}", line);
+                    continue;
+                }
                 dynamicValueMap.put(mapKey, mapValue);
             } else {
                 LOGGER.warn("Malformed configuration line: {}", line);
             }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void parseConfigurationLines(String content) {
String[] lines = content.split("\n");
for (String line : lines) {
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String mapKey = keyValue[0].trim();
String mapValue = keyValue[1].trim();
dynamicValueMap.put(mapKey, mapValue);
} else {
LOGGER.warn("Malformed configuration line: {}", line);
}
}
}
private void parseConfigurationLines(String content) {
String[] lines = content.split("\n");
for (String line : lines) {
if (line == null || line.trim().isEmpty() || line.trim().startsWith("#")) {
continue;
}
line = line.trim();
String[] keyValue = line.split("=", 2);
if (keyValue.length == 2) {
String mapKey = keyValue[0].trim();
String mapValue = keyValue[1].trim();
if (mapKey.isEmpty()) {
LOGGER.warn("Empty key in configuration line: {}", line);
continue;
}
dynamicValueMap.put(mapKey, mapValue);
} else {
LOGGER.warn("Malformed configuration line: {}", line);
}
}
}

Comment on lines +71 to +101
public String getOriginalUrl() {
return originalUrl;
}

public String getProtocol() {
return protocol;
}

public String getAddress() {
return address;
}

public String getHost() {
return host;
}

public int getPort() {
return port;
}

public String getPath() {
return path;
}

public Map<String, String> getParams() {
return params;
}

public String getParam(String key) {
return params.get(key);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add utility methods for configuration center integration

Consider adding these utility methods to enhance integration with configuration centers:

+/**
+ * Builds a connection string suitable for the configuration center client
+ */
+public String getConnectionString() {
+    return String.format("%s:%d", host, port);
+}
+
+/**
+ * Creates a unique identifier for this configuration source
+ */
+public String getSourceId() {
+    return String.format("%s-%s-%d-%s", protocol, host, port, 
+        path.isEmpty() ? "default" : path.substring(1));
+}
+
+/**
+ * Checks if this URL represents a specific configuration center type
+ */
+public boolean isProtocol(String protocolName) {
+    return protocol.equalsIgnoreCase(protocolName);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String getOriginalUrl() {
return originalUrl;
}
public String getProtocol() {
return protocol;
}
public String getAddress() {
return address;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getPath() {
return path;
}
public Map<String, String> getParams() {
return params;
}
public String getParam(String key) {
return params.get(key);
}
public String getOriginalUrl() {
return originalUrl;
}
public String getProtocol() {
return protocol;
}
public String getAddress() {
return address;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getPath() {
return path;
}
public Map<String, String> getParams() {
return params;
}
public String getParam(String key) {
return params.get(key);
}
/**
* Builds a connection string suitable for the configuration center client
*/
public String getConnectionString() {
return String.format("%s:%d", host, port);
}
/**
* Creates a unique identifier for this configuration source
*/
public String getSourceId() {
return String.format("%s-%s-%d-%s", protocol, host, port,
path.isEmpty() ? "default" : path.substring(1));
}
/**
* Checks if this URL represents a specific configuration center type
*/
public boolean isProtocol(String protocolName) {
return protocol.equalsIgnoreCase(protocolName);
}

Comment on lines +41 to +59
public DynamicUrl(String configCenterAddress) {
this.originalUrl = configCenterAddress;
// 正则表达式解析协议、主机、端口、路径和参数,其中路径和参数是可选的
String regex = "^(\\w+)://([^:/]+):(\\d+)(/[^?]*)?(\\?.*)?$";
Matcher matcher = Pattern.compile(regex).matcher(configCenterAddress);
if (matcher.find()) {
this.protocol = matcher.group(1);
this.host = matcher.group(2);
this.port = Integer.parseInt(matcher.group(3));
// 判断路径是否为空或者为 "/"
this.path = (matcher.group(4) != null && !matcher.group(4).equals("/")) ? matcher.group(4) : "";
this.address = this.host + ":" + this.port + this.path;
if (matcher.group(5) != null) {
parseQueryParams(matcher.group(5).substring(1));
}
} else {
throw new IllegalArgumentException("Invalid URL format");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add input validation and translate comments

Several improvements needed:

  1. Add null/empty check for configCenterAddress
  2. Translate Chinese comments to English
  3. Consider adding protocol validation against allowed values (zookeeper, nacos)

Apply these improvements:

 public DynamicUrl(String configCenterAddress) {
+    if (configCenterAddress == null || configCenterAddress.trim().isEmpty()) {
+        throw new IllegalArgumentException("configCenterAddress cannot be null or empty");
+    }
     this.originalUrl = configCenterAddress;
-    // 正则表达式解析协议、主机、端口、路径和参数,其中路径和参数是可选的
+    // Parse protocol, host, port, path (optional), and parameters (optional)
     String regex = "^(\\w+)://([^:/]+):(\\d+)(/[^?]*)?(\\?.*)?$";
     Matcher matcher = Pattern.compile(regex).matcher(configCenterAddress);
     if (matcher.find()) {
         this.protocol = matcher.group(1);
+        if (!isValidProtocol(protocol)) {
+            throw new IllegalArgumentException("Unsupported protocol: " + protocol);
+        }
         this.host = matcher.group(2);
         this.port = Integer.parseInt(matcher.group(3));
-        // 判断路径是否为空或者为 "/"
+        // Check if path is empty or "/"
         this.path = (matcher.group(4) != null && !matcher.group(4).equals("/")) ? matcher.group(4) : "";
         this.address = this.host + ":" + this.port + this.path;
         if (matcher.group(5) != null) {
             parseQueryParams(matcher.group(5).substring(1));
         }
     } else {
-        throw new IllegalArgumentException("Invalid URL format");
+        throw new IllegalArgumentException("Invalid URL format. Expected: protocol://host:port[/path][?param=value]");
     }
 }

+private boolean isValidProtocol(String protocol) {
+    return "zookeeper".equals(protocol) || "nacos".equals(protocol);
+}

Committable suggestion was skipped due to low confidence.

Comment on lines 72 to +91
protected ApolloDynamicConfigManager(String appName) {
super(appName);
config = ConfigService.getAppConfig();
super(appName, SofaConfigs.getOrCustomDefault(DynamicConfigKeys.CONFIG_CENTER_ADDRESS, ""));
if (getDynamicUrl() != null) {
if (StringUtils.isNotBlank(getDynamicUrl().getParam(APOLLO_PARAM_APPID_KEY))) {
System.setProperty(APOLLO_APPID_KEY, getDynamicUrl().getParam(APOLLO_PARAM_APPID_KEY));
}
if (StringUtils.isNotBlank(getDynamicUrl().getAddress())) {
System.setProperty(APOLLO_ADDR_KEY, APOLLO_PROTOCOL_PREFIX + getDynamicUrl().getAddress());
}
if (StringUtils.isNotBlank(getDynamicUrl().getParam(APOLLO_PARAM_CLUSTER_KEY))) {
System.setProperty(APOLLO_CLUSTER_KEY, getDynamicUrl().getParam(APOLLO_PARAM_CLUSTER_KEY));
}
if (StringUtils.isNotBlank(getDynamicUrl().getParam(APOLLO_PARAM_NAMESPACE_KEY))) {
config = ConfigService.getConfig(getDynamicUrl().getParam(APOLLO_PARAM_NAMESPACE_KEY));
} else {
config = ConfigService.getAppConfig();
}
} else {
config = ConfigService.getAppConfig();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Simplify constructor logic and add null checks

The constructor contains complex initialization logic and potential null pointer risks. Consider:

  1. Extract the Apollo configuration initialization into a separate method
  2. Add null checks before accessing getDynamicUrl() parameters

Apply this diff to improve the code:

 protected ApolloDynamicConfigManager(String appName) {
     super(appName, SofaConfigs.getOrCustomDefault(DynamicConfigKeys.CONFIG_CENTER_ADDRESS, ""));
+    initApolloConfig();
+ }
+
+ private void initApolloConfig() {
     if (getDynamicUrl() != null) {
-        if (StringUtils.isNotBlank(getDynamicUrl().getParam(APOLLO_PARAM_APPID_KEY))) {
+        String appId = getDynamicUrl().getParam(APOLLO_PARAM_APPID_KEY);
+        if (StringUtils.isNotBlank(appId)) {
-            System.setProperty(APOLLO_APPID_KEY, getDynamicUrl().getParam(APOLLO_PARAM_APPID_KEY));
+            System.setProperty(APOLLO_APPID_KEY, appId);
         }
         // Similar changes for other parameters...
     }
     config = ConfigService.getAppConfig();
 }

Committable suggestion was skipped due to low confidence.

Comment on lines 461 to 464
// 可以动态配置的选项
private final Set<String> dynamicConfigKeys = new HashSet<>();
private Map<String, String> newValueMap = new HashMap<>();

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use ConcurrentHashMap for thread safety

The newValueMap is accessed from multiple methods that could be called concurrently. Using a regular HashMap is not thread-safe.

Apply this diff:

-private Map<String, String> newValueMap = new HashMap<>();
+private Map<String, String> newValueMap = new ConcurrentHashMap<>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 可以动态配置的选项
private final Set<String> dynamicConfigKeys = new HashSet<>();
private Map<String, String> newValueMap = new HashMap<>();
// 可以动态配置的选项
private final Set<String> dynamicConfigKeys = new HashSet<>();
private Map<String, String> newValueMap = new ConcurrentHashMap<>();

Comment on lines 471 to 491
@Override
public void process(ConfigChangedEvent event) {
// 清除上次的动态配置值缓存
consumerConfig.getDynamicConfigValueCache().clear();
// 获取对应配置项的默认值
for (String key : newValueMap.keySet()) {
newValueMap.put(key, String.valueOf(consumerConfig.getConfigValueCache().get(key)));
}
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
// ADDED or MODIFIED
Map<String, String> dynamicValueMap = event.getDynamicValueMap();
for (String key : dynamicValueMap.keySet()) {
String tempKey = key.lastIndexOf(".") == -1 ? key : key.substring(key.lastIndexOf(".") + 1);
if (dynamicConfigKeys.contains(tempKey)) {
consumerConfig.getDynamicConfigValueCache().put(key, dynamicValueMap.get(key));
newValueMap.put(key, dynamicValueMap.get(key));
}
}
}
attrUpdated(newValueMap);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize configuration parsing for better performance

The current implementation of the process method can be optimized by:

  1. Using StringBuilder for string manipulations
  2. Avoiding repeated substring operations
  3. Using more efficient key extraction

Apply this diff to optimize the method:

 @Override
 public void process(ConfigChangedEvent event) {
     consumerConfig.getDynamicConfigValueCache().clear();
-    for (String key : newValueMap.keySet()) {
-        newValueMap.put(key, String.valueOf(consumerConfig.getConfigValueCache().get(key)));
-    }
+    // Batch update default values
+    newValueMap.replaceAll((key, value) -> 
+        String.valueOf(consumerConfig.getConfigValueCache().get(key)));

     if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
         Map<String, String> dynamicValueMap = event.getDynamicValueMap();
         for (String key : dynamicValueMap.keySet()) {
-            String tempKey = key.lastIndexOf(".") == -1 ? key : key.substring(key.lastIndexOf(".") + 1);
+            int lastDotIndex = key.lastIndexOf('.');
+            String tempKey = lastDotIndex == -1 ? key : key.substring(lastDotIndex + 1);
             if (dynamicConfigKeys.contains(tempKey)) {
-                consumerConfig.getDynamicConfigValueCache().put(key, dynamicValueMap.get(key));
-                newValueMap.put(key, dynamicValueMap.get(key));
+                String value = dynamicValueMap.get(key);
+                consumerConfig.getDynamicConfigValueCache().put(key, value);
+                newValueMap.put(key, value);
             }
         }
     }
     attrUpdated(newValueMap);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public void process(ConfigChangedEvent event) {
// 清除上次的动态配置值缓存
consumerConfig.getDynamicConfigValueCache().clear();
// 获取对应配置项的默认值
for (String key : newValueMap.keySet()) {
newValueMap.put(key, String.valueOf(consumerConfig.getConfigValueCache().get(key)));
}
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
// ADDED or MODIFIED
Map<String, String> dynamicValueMap = event.getDynamicValueMap();
for (String key : dynamicValueMap.keySet()) {
String tempKey = key.lastIndexOf(".") == -1 ? key : key.substring(key.lastIndexOf(".") + 1);
if (dynamicConfigKeys.contains(tempKey)) {
consumerConfig.getDynamicConfigValueCache().put(key, dynamicValueMap.get(key));
newValueMap.put(key, dynamicValueMap.get(key));
}
}
}
attrUpdated(newValueMap);
}
@Override
public void process(ConfigChangedEvent event) {
// 清除上次的动态配置值缓存
consumerConfig.getDynamicConfigValueCache().clear();
// Batch update default values
newValueMap.replaceAll((key, value) ->
String.valueOf(consumerConfig.getConfigValueCache().get(key)));
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
// ADDED or MODIFIED
Map<String, String> dynamicValueMap = event.getDynamicValueMap();
for (String key : dynamicValueMap.keySet()) {
int lastDotIndex = key.lastIndexOf('.');
String tempKey = lastDotIndex == -1 ? key : key.substring(lastDotIndex + 1);
if (dynamicConfigKeys.contains(tempKey)) {
String value = dynamicValueMap.get(key);
consumerConfig.getDynamicConfigValueCache().put(key, value);
newValueMap.put(key, value);
}
}
}
attrUpdated(newValueMap);
}

@@ -939,7 +953,10 @@ public boolean updateAttribute(String property, String newValueStr, boolean over
Class propertyClazz = getMethod.getReturnType(); // 旧值的类型
// 拿到旧的值
Object oldValue = null;
Object newValue = CompatibleTypeUtils.convert(newValueStr, propertyClazz);
Object newValue = newValueStr == "null" ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use equals() method for String comparison

Comparing Strings using == checks for reference equality instead of content equality. To compare the content of newValueStr to "null", use the equals() method. Additionally, to avoid a potential NullPointerException, it's safer to call equals() on the constant.

Apply this diff to fix the comparison:

- Object newValue = newValueStr == "null" ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);
+ Object newValue = "null".equals(newValueStr) ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object newValue = newValueStr == "null" ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);
Object newValue = "null".equals(newValueStr) ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);

@@ -971,7 +988,10 @@
Class propertyClazz = getMethod.getReturnType(); // 旧值的类型
// 拿到旧的值
Object oldValue = BeanUtils.getProperty(this, property, propertyClazz);
Object newValue = CompatibleTypeUtils.convert(newValueStr, propertyClazz);
Object newValue = newValueStr == "null" ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use equals() method for String comparison

Comparing Strings using == checks for reference equality instead of content equality. To compare the content of newValueStr to "null", use the equals() method. Additionally, to avoid a potential NullPointerException, it's safer to call equals() on the constant.

Apply this diff to fix the comparison:

- Object newValue = newValueStr == "null" ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);
+ Object newValue = "null".equals(newValueStr) ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object newValue = newValueStr == "null" ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);
Object newValue = "null".equals(newValueStr) ? null : CompatibleTypeUtils.convert(newValueStr, propertyClazz);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Outside diff range and nitpick comments (7)
config/config-apollo/src/test/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManagerTest.java (1)

32-33: Consider making the test environment configurable.

While the factory pattern usage is good, hardcoding the "test" environment might limit test scenarios. Consider making it configurable through test properties or system properties.

-    private DynamicConfigManager apolloDynamicConfigManager = DynamicConfigManagerFactory.getDynamicManager("test",
-            "apollo");
+    private static final String TEST_ENV = System.getProperty("test.env", "test");
+    private DynamicConfigManager apolloDynamicConfigManager = DynamicConfigManagerFactory.getDynamicManager(TEST_ENV,
+            "apollo");
config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (1)

Line range hint 140-143: Implement getServiceAuthRule method

The method should follow the same pattern as other property getters, using Apollo's config to retrieve the auth rule.

Consider implementing the method like this:

@Override
public AuthRuleGroup getServiceAuthRule(String service) {
-    //TODO 暂不支持
-    return null;
+    String authRule = config.getProperty(DynamicConfigKeyHelper.buildServiceAuthKey(service),
+            DynamicHelper.DEFAULT_DYNAMIC_VALUE);
+    return StringUtils.isNotBlank(authRule) ? AuthRuleGroup.buildFromJson(authRule) : null;
}
bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (2)

23-23: Consider using specific imports instead of wildcards.

For better maintainability and to avoid potential naming conflicts, consider importing specific classes instead of using wildcards.

Apply this diff:

-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;

Also applies to: 32-37, 51-54, 63-63


501-501: Remove empty configChanged method.

The configChanged method is empty and appears to be unused. Consider removing it if it's not required by the interface contract.

config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (2)

54-54: Standardize Modifier Order in Variable Declarations

In Java, the conventional order of modifiers is private static final. The current declaration of LOGGER uses private final static. For consistency and readability, consider updating it:

-private final static Logger LOGGER = LoggerFactory.getLogger(ZookeeperDynamicConfigManager.class);
+private static final Logger LOGGER = LoggerFactory.getLogger(ZookeeperDynamicConfigManager.class);

183-186: Simplify the createTargetListener Method

The createTargetListener method can be simplified by returning the new instance directly, enhancing code clarity:

-private ZookeeperConfigListener createTargetListener(String pathKey) {
-    ZookeeperConfigListener configListener = new ZookeeperConfigListener(pathKey);
-    return configListener;
-}
+private ZookeeperConfigListener createTargetListener(String pathKey) {
+    return new ZookeeperConfigListener(pathKey);
+}
core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (1)

1041-1043: Remove unnecessary null check for 'dynamicConfigValueCache'

Since dynamicConfigValueCache is initialized during declaration and is not set to null elsewhere, the null check in getMethodConfigValue is unnecessary and can be removed to simplify the code.

Apply this diff to remove the null check:

-    if (dynamicConfigValueCache != null) {
         value = dynamicConfigValueCache.get(key);
-    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 08a0a85 and 0f613cf.

📒 Files selected for processing (9)
  • bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (8 hunks)
  • config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (3 hunks)
  • config/config-apollo/src/test/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManagerTest.java (3 hunks)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java (1 hunks)
  • config/config-zk/src/main/java/com/alipay/sofa/rpc/dynamic/zk/ZookeeperDynamicConfigManager.java (1 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (8 hunks)
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java (1 hunks)
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • config/config-nacos/src/main/java/com/alipay/sofa/rpc/dynamic/nacos/NacosDynamicConfigManager.java
  • core/api/src/main/java/com/alipay/sofa/rpc/dynamic/ConfigChangedEvent.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/ApolloDynamicConfigTest.java
  • test/test-integration/src/test/java/com/alipay/sofa/rpc/test/config/NacosDynamicConfigTest.java
🔇 Additional comments (5)
config/config-apollo/src/test/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManagerTest.java (2)

19-20: LGTM!

The new imports align with the architectural changes to support multiple configuration centers.


29-30: LGTM!

Logger initialization follows standard practices with proper use of static final modifier.

config/config-apollo/src/main/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManager.java (1)

97-99: ⚠️ Potential issue

Replace TODO comment with proper exception handling

Instead of leaving a TODO comment, throw an UnsupportedOperationException to clearly indicate that this operation is not supported.

Apply this diff:

@Override
public void initServiceConfiguration(String service) {
-    // TODO 暂不支持
+    throw new UnsupportedOperationException("Service configuration initialization without listener is not supported");
}

Likely invalid or redundant comment.

bootstrap/bootstrap-api/src/main/java/com/alipay/sofa/rpc/bootstrap/DefaultConsumerBootstrap.java (1)

167-173: ⚠️ Potential issue

Add error handling for request-level dynamic configuration.

The initialization lacks error handling which could lead to runtime errors if getDynamicManager returns null or if initialization fails.

Apply this diff:

 if (StringUtils.isNotBlank(dynamicAlias)) {
+    try {
         final DynamicConfigManager dynamicManager = DynamicConfigManagerFactory.getDynamicManager(
                 consumerConfig.getAppName(), dynamicAlias);
+        if (dynamicManager == null) {
+            LOGGER.warn("Failed to get DynamicConfigManager for alias: {}", dynamicAlias);
+            return proxyIns;
+        }
         dynamicManager.initServiceConfiguration(consumerConfig.getInterfaceId());
+    } catch (Exception e) {
+        LOGGER.error("Failed to initialize request-level dynamic configuration", e);
+    }
 }

Likely invalid or redundant comment.

core/api/src/main/java/com/alipay/sofa/rpc/config/AbstractInterfaceConfig.java (1)

1045-1048: Check 'configValueCache' for null to prevent potential NullPointerException

Unlike dynamicConfigValueCache, the configValueCache may be null, as it is initialized to null and only populated under certain conditions. Ensure that the null check for configValueCache remains to prevent potential NullPointerException.

Comment on lines +43 to +56
String result = apolloDynamicConfigManager.getConsumerServiceProperty("serviceName", "timeout");
Assert.assertEquals(DynamicHelper.DEFAULT_DYNAMIC_VALUE, result);
}

@Test
public void getProviderMethodProperty() {
String result = apolloDynamicConfigManager.getProviderMethodProperty("serviceName", "methodName", "timeout");
Assert.assertEquals(DynamicHelper.DEFAULT_DYNAMIC_VALUE, result);
}

@Test
public void getConsumerMethodProperty() {
}

@Test
public void getServiceAuthRule() {
String result = apolloDynamicConfigManager.getConsumerMethodProperty("serviceName", "methodName", "timeout");
Assert.assertEquals(DynamicHelper.DEFAULT_DYNAMIC_VALUE, result);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance test coverage with additional test scenarios.

The current tests only verify default values. Consider adding:

  1. Tests with actual configuration values
  2. Negative test cases (null/invalid service names, methods)
  3. Tests verifying dynamic config changes
  4. Tests for initialization failures

Example test to add:

@Test
public void testDynamicConfigUpdate() {
    // Setup initial value
    String serviceName = "testService";
    String property = "timeout";
    String initialValue = apolloDynamicConfigManager.getConsumerServiceProperty(serviceName, property);
    
    // Simulate config change
    // TODO: Add mechanism to trigger config change
    
    // Verify updated value
    String updatedValue = apolloDynamicConfigManager.getConsumerServiceProperty(serviceName, property);
    Assert.assertNotEquals("Config should be updated", initialValue, updatedValue);
}

@Test(expected = IllegalArgumentException.class)
public void testInvalidServiceName() {
    apolloDynamicConfigManager.getConsumerServiceProperty(null, "timeout");
}

Comment on lines +146 to +150
public void addListener(String key, ConfigListener listener) {
ApolloListener apolloListener = watchListenerMap.computeIfAbsent(key, k -> new ApolloListener());
apolloListener.addListener(listener);
config.addChangeListener(apolloListener, Collections.singleton(key));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance listener management to prevent memory leaks

The current implementation lacks listener cleanup capabilities and doesn't prevent duplicate registrations.

Consider these improvements:

@Override
public void addListener(String key, ConfigListener listener) {
    ApolloListener apolloListener = watchListenerMap.computeIfAbsent(key, k -> new ApolloListener());
+    if (apolloListener.hasListener(listener)) {
+        return;  // Prevent duplicate registration
+    }
    apolloListener.addListener(listener);
    config.addChangeListener(apolloListener, Collections.singleton(key));
}

+@Override
+public void removeListener(String key, ConfigListener listener) {
+    ApolloListener apolloListener = watchListenerMap.get(key);
+    if (apolloListener != null) {
+        apolloListener.removeListener(listener);
+        if (apolloListener.isEmpty()) {
+            config.removeChangeListener(apolloListener);
+            watchListenerMap.remove(key);
+        }
+    }
}

Also add these methods to the ApolloListener class:

boolean hasListener(ConfigListener listener) {
    return listeners.contains(listener);
}

boolean isEmpty() {
    return listeners.isEmpty();
}

void removeListener(ConfigListener listener) {
    listeners.remove(listener);
}

Comment on lines +461 to +464
// 可以动态配置的选项
private final Set<String> supportDynamicConfigKeys = new HashSet<>();
private final Map<String, String> newValueMap = new HashMap<>();

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use ConcurrentHashMap for thread safety.

The newValueMap is accessed from multiple methods that could be called concurrently. Using a regular HashMap is not thread-safe.

Apply this diff:

-private final Map<String, String> newValueMap = new HashMap<>();
+private final Map<String, String> newValueMap = new ConcurrentHashMap<>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 可以动态配置的选项
private final Set<String> supportDynamicConfigKeys = new HashSet<>();
private final Map<String, String> newValueMap = new HashMap<>();
// 可以动态配置的选项
private final Set<String> supportDynamicConfigKeys = new HashSet<>();
private final Map<String, String> newValueMap = new ConcurrentHashMap<>();

Comment on lines +471 to +498
@Override
public void process(ConfigChangedEvent event) {
// 清除上次的动态配置值缓存
consumerConfig.getDynamicConfigValueCache().clear();
// 获取对应配置项的默认值
for (String key : newValueMap.keySet()) {
if (consumerConfig.getConfigValueCache().get(key) != null) {
newValueMap.put(key, String.valueOf(consumerConfig.getConfigValueCache().get(key)));
} else {
newValueMap.put(key, null);
}
}
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
// ADDED or MODIFIED
Map<String, String> dynamicValueMap = event.getDynamicValueMap();
for (String key : dynamicValueMap.keySet()) {
String tempKey = key.lastIndexOf(".") == -1 ? key : key.substring(key.lastIndexOf(".") + 1);
if (supportDynamicConfigKeys.contains(tempKey)) {
String value = dynamicValueMap.get(key);
if (StringUtils.isNotBlank(value)) {
consumerConfig.getDynamicConfigValueCache().put(key, value);
newValueMap.put(key, value);
}
}
}
}
attrUpdated(newValueMap);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize configuration parsing for better performance.

The configuration parsing in the process method can be optimized by:

  1. Using StringBuilder for string manipulations
  2. Avoiding repeated substring operations
  3. Using more efficient key extraction

Apply this diff:

 @Override
 public void process(ConfigChangedEvent event) {
     consumerConfig.getDynamicConfigValueCache().clear();
-    for (String key : newValueMap.keySet()) {
-        if (consumerConfig.getConfigValueCache().get(key) != null) {
-            newValueMap.put(key, String.valueOf(consumerConfig.getConfigValueCache().get(key)));
-        } else {
-            newValueMap.put(key, null);
-        }
-    }
+    // Batch update default values
+    newValueMap.replaceAll((key, value) -> {
+        Object defaultValue = consumerConfig.getConfigValueCache().get(key);
+        return defaultValue != null ? String.valueOf(defaultValue) : null;
+    });

     if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
         Map<String, String> dynamicValueMap = event.getDynamicValueMap();
         for (String key : dynamicValueMap.keySet()) {
-            String tempKey = key.lastIndexOf(".") == -1 ? key : key.substring(key.lastIndexOf(".") + 1);
+            int lastDotIndex = key.lastIndexOf('.');
+            String tempKey = lastDotIndex == -1 ? key : key.substring(lastDotIndex + 1);
             if (supportDynamicConfigKeys.contains(tempKey)) {
                 String value = dynamicValueMap.get(key);
                 if (StringUtils.isNotBlank(value)) {
-                    consumerConfig.getDynamicConfigValueCache().put(key, value);
-                    newValueMap.put(key, value);
+                    // Batch update both caches
+                    consumerConfig.getDynamicConfigValueCache().put(key, value);
+                    newValueMap.put(key, value);
                 }
             }
         }
     }
     attrUpdated(newValueMap);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public void process(ConfigChangedEvent event) {
// 清除上次的动态配置值缓存
consumerConfig.getDynamicConfigValueCache().clear();
// 获取对应配置项的默认值
for (String key : newValueMap.keySet()) {
if (consumerConfig.getConfigValueCache().get(key) != null) {
newValueMap.put(key, String.valueOf(consumerConfig.getConfigValueCache().get(key)));
} else {
newValueMap.put(key, null);
}
}
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
// ADDED or MODIFIED
Map<String, String> dynamicValueMap = event.getDynamicValueMap();
for (String key : dynamicValueMap.keySet()) {
String tempKey = key.lastIndexOf(".") == -1 ? key : key.substring(key.lastIndexOf(".") + 1);
if (supportDynamicConfigKeys.contains(tempKey)) {
String value = dynamicValueMap.get(key);
if (StringUtils.isNotBlank(value)) {
consumerConfig.getDynamicConfigValueCache().put(key, value);
newValueMap.put(key, value);
}
}
}
}
attrUpdated(newValueMap);
}
@Override
public void process(ConfigChangedEvent event) {
consumerConfig.getDynamicConfigValueCache().clear();
// Batch update default values
newValueMap.replaceAll((key, value) -> {
Object defaultValue = consumerConfig.getConfigValueCache().get(key);
return defaultValue != null ? String.valueOf(defaultValue) : null;
});
if (!event.getChangeType().equals(ConfigChangeType.DELETED)) {
Map<String, String> dynamicValueMap = event.getDynamicValueMap();
for (String key : dynamicValueMap.keySet()) {
int lastDotIndex = key.lastIndexOf('.');
String tempKey = lastDotIndex == -1 ? key : key.substring(lastDotIndex + 1);
if (supportDynamicConfigKeys.contains(tempKey)) {
String value = dynamicValueMap.get(key);
if (StringUtils.isNotBlank(value)) {
// Batch update both caches
consumerConfig.getDynamicConfigValueCache().put(key, value);
newValueMap.put(key, value);
}
}
}
}
attrUpdated(newValueMap);
}

Comment on lines +175 to +185
//接口级别动态配置参数
Boolean dynamicConfigRefreshEnable = getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE);
String configCenterAddress = getOrDefault(DynamicConfigKeys.CONFIG_CENTER_ADDRESS);
if (dynamicConfigRefreshEnable && StringUtils.isNotBlank(configCenterAddress)) {
DynamicUrl dynamicUrl = new DynamicUrl(configCenterAddress);
//启用接口级别动态配置
final DynamicConfigManager dynamicManager = DynamicConfigManagerFactory.getDynamicManager(
consumerConfig.getAppName(), dynamicUrl.getProtocol());
dynamicManager.addListener(consumerConfig.getInterfaceId(), configListener);
dynamicManager.initServiceConfiguration(consumerConfig.getInterfaceId(), configListener);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add cleanup logic for interface-level dynamic configuration.

The interface-level configuration initialization lacks cleanup logic. If initialization fails after adding the listener, the listener remains registered, potentially causing memory leaks.

Apply this diff:

 if (dynamicConfigRefreshEnable && StringUtils.isNotBlank(configCenterAddress)) {
+    DynamicConfigManager dynamicManager = null;
+    ConfigListener registeredListener = null;
     try {
         DynamicUrl dynamicUrl = new DynamicUrl(configCenterAddress);
-        final DynamicConfigManager dynamicManager = DynamicConfigManagerFactory.getDynamicManager(
+        dynamicManager = DynamicConfigManagerFactory.getDynamicManager(
                 consumerConfig.getAppName(), dynamicUrl.getProtocol());
+        if (dynamicManager == null) {
+            LOGGER.warn("Failed to get DynamicConfigManager for protocol: {}", dynamicUrl.getProtocol());
+            return proxyIns;
+        }
         dynamicManager.addListener(consumerConfig.getInterfaceId(), configListener);
+        registeredListener = configListener;
         dynamicManager.initServiceConfiguration(consumerConfig.getInterfaceId(), configListener);
+    } catch (Exception e) {
+        LOGGER.error("Failed to initialize interface-level dynamic configuration", e);
+        if (dynamicManager != null && registeredListener != null) {
+            dynamicManager.removeListener(consumerConfig.getInterfaceId(), registeredListener);
+        }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//接口级别动态配置参数
Boolean dynamicConfigRefreshEnable = getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE);
String configCenterAddress = getOrDefault(DynamicConfigKeys.CONFIG_CENTER_ADDRESS);
if (dynamicConfigRefreshEnable && StringUtils.isNotBlank(configCenterAddress)) {
DynamicUrl dynamicUrl = new DynamicUrl(configCenterAddress);
//启用接口级别动态配置
final DynamicConfigManager dynamicManager = DynamicConfigManagerFactory.getDynamicManager(
consumerConfig.getAppName(), dynamicUrl.getProtocol());
dynamicManager.addListener(consumerConfig.getInterfaceId(), configListener);
dynamicManager.initServiceConfiguration(consumerConfig.getInterfaceId(), configListener);
}
//接口级别动态配置参数
Boolean dynamicConfigRefreshEnable = getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE);
String configCenterAddress = getOrDefault(DynamicConfigKeys.CONFIG_CENTER_ADDRESS);
if (dynamicConfigRefreshEnable && StringUtils.isNotBlank(configCenterAddress)) {
DynamicConfigManager dynamicManager = null;
ConfigListener registeredListener = null;
try {
DynamicUrl dynamicUrl = new DynamicUrl(configCenterAddress);
dynamicManager = DynamicConfigManagerFactory.getDynamicManager(
consumerConfig.getAppName(), dynamicUrl.getProtocol());
if (dynamicManager == null) {
LOGGER.warn("Failed to get DynamicConfigManager for protocol: {}", dynamicUrl.getProtocol());
return proxyIns;
}
dynamicManager.addListener(consumerConfig.getInterfaceId(), configListener);
registeredListener = configListener;
dynamicManager.initServiceConfiguration(consumerConfig.getInterfaceId(), configListener);
} catch (Exception e) {
LOGGER.error("Failed to initialize interface-level dynamic configuration", e);
if (dynamicManager != null && registeredListener != null) {
dynamicManager.removeListener(consumerConfig.getInterfaceId(), registeredListener);
}
}
}

.build();
zkClient.start();

if (!getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential Logical Error in Dynamic Refresh Condition

The condition in the if statement may be incorrect:

if (!getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE)) {

If DYNAMIC_REFRESH_ENABLE is true, it indicates that dynamic refresh is enabled. Therefore, the cache and listener should be set up when dynamic refresh is enabled. Consider removing the negation to correct the logic:

-if (!getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE)) {
+if (getOrDefault(DynamicConfigKeys.DYNAMIC_REFRESH_ENABLE)) {

Comment on lines +957 to +959
if (dynamicConfigValueCache.containsKey(property)) {
dynamicConfigValueCache.put(property, newValue);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Update 'dynamicConfigValueCache' without checking if the key exists

In the updateAttribute method, the code updates dynamicConfigValueCache only if it already contains the property key. This prevents new properties from being added to the dynamic configuration cache. To ensure that all dynamic configuration changes are captured, consider removing the containsKey check and always updating the cache.

Apply this diff to update the map without the containsKey check:

-    if (dynamicConfigValueCache.containsKey(property)) {
         dynamicConfigValueCache.put(property, newValue);
-    }

This change applies to both instances at lines 957-959 and 992-994.

Also applies to: 992-994

Comment on lines +711 to +719
/**
* Gets dynamic config value cache.
*
* @return the dynamic config value cache
*/
public Map<String, Object> getDynamicConfigValueCache() {
return dynamicConfigValueCache;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Return an unmodifiable view of 'dynamicConfigValueCache' to prevent external modification

The method getDynamicConfigValueCache() returns the mutable map dynamicConfigValueCache, exposing internal state. To maintain encapsulation and prevent unintended external modifications, consider returning an unmodifiable view of the map.

Apply this diff to return an unmodifiable map:

 public Map<String, Object> getDynamicConfigValueCache() {
-    return dynamicConfigValueCache;
+    return Collections.unmodifiableMap(dynamicConfigValueCache);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Gets dynamic config value cache.
*
* @return the dynamic config value cache
*/
public Map<String, Object> getDynamicConfigValueCache() {
return dynamicConfigValueCache;
}
/**
* Gets dynamic config value cache.
*
* @return the dynamic config value cache
*/
public Map<String, Object> getDynamicConfigValueCache() {
return Collections.unmodifiableMap(dynamicConfigValueCache);
}

Comment on lines +218 to +222
/**
* 动态配置的方法名称和方法参数配置的map
*/
protected transient volatile Map<String, Object> dynamicConfigValueCache = new ConcurrentHashMap<>();

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider making 'dynamicConfigValueCache' final to enhance thread safety

The field dynamicConfigValueCache is a ConcurrentHashMap and it's initialized during declaration. Making this field final ensures that it cannot be reassigned, which enhances thread safety and clarity.

Apply this diff to make the field final:

-protected transient volatile Map<String, Object> dynamicConfigValueCache = new ConcurrentHashMap<>();
+protected final transient Map<String, Object> dynamicConfigValueCache = new ConcurrentHashMap<>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 动态配置的方法名称和方法参数配置的map
*/
protected transient volatile Map<String, Object> dynamicConfigValueCache = new ConcurrentHashMap<>();
/**
* 动态配置的方法名称和方法参数配置的map
*/
protected final transient Map<String, Object> dynamicConfigValueCache = new ConcurrentHashMap<>();

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (1)
config/config-apollo/src/test/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManagerTest.java (1)

32-33: Consider parameterizing test environment values.

The hardcoded values "test" and "apollo" should be moved to constants or configuration to make the tests more maintainable and environment-independent.

-    private DynamicConfigManager apolloDynamicConfigManager = DynamicConfigManagerFactory.getDynamicManager("test",
-                                                                "apollo");
+    private static final String TEST_ENV = "test";
+    private static final String CONFIG_TYPE = "apollo";
+    private DynamicConfigManager apolloDynamicConfigManager = DynamicConfigManagerFactory.getDynamicManager(TEST_ENV,
+                                                                CONFIG_TYPE);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 0f613cf and 117b33c.

📒 Files selected for processing (1)
  • config/config-apollo/src/test/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManagerTest.java (3 hunks)
🔇 Additional comments (3)
config/config-apollo/src/test/java/com/alipay/sofa/rpc/dynamic/apollo/ApolloDynamicConfigManagerTest.java (3)

19-20: LGTM! Import changes align with the new implementation.


43-56: 🛠️ Refactor suggestion

Consider organizing test methods using nested test classes.

The test methods follow a pattern testing different property types. Consider using nested test classes to better organize and group related tests:

@Nested
class ConsumerConfigTests {
    @Test
    void shouldGetServiceProperty() {
        // existing test
    }
    
    @Test
    void shouldGetMethodProperty() {
        // existing test
    }
}

@Nested
class ProviderConfigTests {
    @Test
    void shouldGetServiceProperty() {
        // existing test
    }
    
    @Test
    void shouldGetMethodProperty() {
        // existing test
    }
}

The previous review comment about enhancing test coverage with additional scenarios is still valid and should be addressed.


29-33: Add proper test lifecycle management and Apollo-specific tests.

  1. Consider adding @Before and @After methods to manage test lifecycle:

    • Initialize Apollo client
    • Clean up test data
    • Reset configuration state
  2. Add Apollo-specific test scenarios:

    • Verify Apollo client initialization
    • Test Apollo configuration change notifications
    • Verify Apollo namespace handling

@EvenLjj EvenLjj merged commit 3e317ff into sofastack:master Oct 31, 2024
4 checks passed
@EvenLjj EvenLjj added this to the 5.14.0 milestone Dec 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla:yes CLA is ok First-time contributor First-time contributor size/XXL
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants