Skip to content

feat: support incremental configuration synchronization client #5373

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

Open
wants to merge 26 commits into
base: master
Choose a base branch
from

Conversation

jackie-coming
Copy link

@jackie-coming jackie-coming commented Apr 1, 2025

What's the purpose of this PR

之前的讨论在这个pr被关闭了, 我重新提交了pr,继续修复代码

Which issue(s) this PR fixes:

Fixes #

Brief changelog

XXXXX

Follow this checklist to help us incorporate your contribution quickly and easily:

  • Read the Contributing Guide before making this pull request.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Write necessary unit tests to verify the code.
  • Run mvn clean test to make sure this pull request doesn't break anything.
  • Update the CHANGES log.

Summary by CodeRabbit

  • New Features

    • Added support for incremental configuration synchronization, allowing clients to receive only changed configuration items instead of the full set, which can improve performance and reduce network usage.
    • Introduced a new configuration option to enable incremental sync (config-service.incremental.change.enabled), configurable via server settings.
  • Documentation

    • Updated deployment guides (English and Chinese) with details about the new incremental sync feature and configuration option.
    • Added release notes entry highlighting incremental configuration synchronization support.
  • Bug Fixes

    • None.
  • Tests

    • Added comprehensive tests to verify incremental sync logic, including scenarios for added, modified, and deleted configuration items, as well as public namespace overrides.
  • Chores

    • Updated default configuration SQL scripts to include the new incremental sync option.
    • Adjusted project versioning in build configuration.

@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Apr 1, 2025
Copy link
Contributor

coderabbitai bot commented Apr 1, 2025

Walkthrough

This change introduces support for incremental configuration synchronization in the Apollo configuration service. It adds a new feature toggle, config-service.incremental.change.enabled, configurable via database and documented in both English and Chinese deployment guides. The server-side logic is enhanced to detect when incremental sync is enabled and, if so, computes and returns only the configuration changes (additions, deletions, modifications) between the latest and client-side configurations. This is achieved through new interfaces, services, and caching mechanisms. Comprehensive tests are included to validate the incremental sync logic and its integration into the configuration controller.

Changes

File(s) Change Summary
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java Added method to check if incremental config sync is enabled.
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseRepository.java Added method to find release by key; renamed parameter for clarity.
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java Added method to find release by key.
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java Added conditional branch for incremental sync feature toggle (logic placeholder).
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java Injected new dependencies; updated logic to support incremental sync and response; updated constructor.
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java Added imports; formatting only.
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java Added method for batch retrieval of releases by keys.
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java Added cache for release keys; added batch retrieval method; updated initialization and metrics registration; changed access modifiers for some fields.
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/DefaultConfigService.java Added batch retrieval method for releases by keys.
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/IncrementalSyncService.java Introduced new interface for incremental sync logic.
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/DefaultIncrementalSyncService.java Added implementation for incremental sync: computes and caches config changes between releases.
apollo-configservice/src/main/resources/jpa/configdb.init.h2.sql
scripts/sql/profiles/h2-default/apolloconfigdb.sql
scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql
scripts/sql/profiles/mysql-default/apolloconfigdb.sql
scripts/sql/src/apolloconfigdb.sql
Added new configuration entry for incremental sync toggle.
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java Added tests for incremental sync logic and edge cases.
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCacheTest.java Added tests for batch retrieval of releases by keys, including caching scenarios.
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCacheAndCacheKeyIgnoreCaseTest.java Updated imports only.
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/DefaultConfigServiceTest.java Formatting only (added blank line).
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/DefaultIncrementalSyncServiceTest.java New test class for incremental sync logic, covering all change types.
changes/changes-2.4.0.md Added release note for incremental configuration sync feature.
docs/en/deployment/distributed-deployment-guide.md
docs/zh/deployment/distributed-deployment-guide.md
Documented new incremental sync configuration option and usage notes.
pom.xml Changed Apollo Java version property from 2.5.0-SNAPSHOT to 2.4.0-SNAPSHOT.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ConfigController
    participant ConfigService
    participant IncrementalSyncService

    Client->>ConfigController: queryConfig(appId, cluster, namespace, clientReleaseKey)
    ConfigController->>ConfigService: load latest merged config
    ConfigService-->>ConfigController: latestMergedReleaseKey, latestConfig
    ConfigController->>BizConfig: isConfigServiceIncrementalChangeEnabled()
    alt Incremental Sync Enabled
        ConfigController->>ConfigService: findReleasesByReleaseKeys(clientReleaseKeys)
        ConfigService-->>ConfigController: clientReleases
        ConfigController->>IncrementalSyncService: getConfigurationChanges(latestMergedReleaseKey, latestConfig, clientReleaseKey, clientConfig)
        IncrementalSyncService-->>ConfigController: configurationChanges
        ConfigController-->>Client: ApolloConfig (changes only, syncType=INCREMENTAL_SYNC)
    else Full Sync or Not Enabled
        ConfigController-->>Client: ApolloConfig (full config)
    end
Loading

Suggested labels

size:L, lgtm

Suggested reviewers

  • nobodyiam

Poem

In the warren of configs, a new path we pave,
Incremental updates, for the bytes we now save.
With caches and keys, and a toggle in place,
Configs hop swiftly, in a leaner embrace.
The docs are now richer, the tests cover all—
A rabbit’s delight in this incremental haul!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ 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 generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this 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: 1

🔭 Outside diff range comments (1)
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java (1)

1-643: 💡 Verification agent

🧩 Analysis chain

Address unchecked operation warning.

There's a GitHub Actions warning about unchecked operations in this file. While not directly related to your changes, it would be good to fix it.


🏁 Script executed:

#!/bin/bash
# Search for potential unchecked cast operations in the file
rg -n "operationContext\.get\(.*\)" apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java

Length of output: 224


Fix unchecked cast for branch release keys in ReleaseService.java
The GitHub Actions warning originates from the unchecked cast on line 349 in the getBranchReleaseKeys method. Please resolve this by adding a proper type check (e.g., using instanceof) or by leveraging a more specific Gson type reference to ensure that the cast to Collection<String> is safe.

  • Location: apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java (line 349)
  • Action: Instead of directly casting with (Collection<String>), verify that the object returned by operationContext.get(ReleaseOperationContext.BRANCH_RELEASE_KEYS) is an instance of Collection containing String items. Adjust the implementation accordingly to remove the unchecked cast warning.
🧰 Tools
🪛 GitHub Actions: build

[warning] 1-1: uses unchecked or unsafe operations. Recompile with -Xlint:unchecked for details.

🧹 Nitpick comments (11)
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java (1)

26-26: Interface enhancement for incremental synchronization support.

The ConfigService interface now extends IncrementalSyncConfigService in addition to ReleaseMessageListener, enhancing its capabilities to support incremental configuration updates. This is an essential architectural change that allows the configuration service to calculate and provide only the changes between different configuration versions rather than always sending complete configurations.

This extension aligns well with the configuration service's responsibility and promotes efficient data transfer by enabling differential updates. Make sure the implementations of this interface properly handle the increased memory requirements mentioned in the configuration comments.

docs/en/deployment/distributed-deployment-guide.md (1)

1644-1658: Well-documented configuration with minor confusion in the final note.

The documentation clearly explains the purpose, benefits, and considerations for enabling incremental configuration synchronization. It properly mentions version requirements for both server and client.

However, in the last paragraph, there appears to be a copy-paste error:

- > `config-service.cache.enabled` configuration adjustment requires a restart of the config service to take effect
+ > `config-service.incremental.change.enabled` configuration adjustment requires a restart of the config service to take effect

The note should reference the incremental change configuration parameter instead of the cache configuration parameter since that's what this section is about.

🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

1656-1656: Blank line inside blockquote
null

(MD028, no-blanks-blockquote)

docs/zh/deployment/distributed-deployment-guide.md (1)

1580-1591: Fix Markdown lint issue by removing extra blank line.

There's a blank line inside the blockquote which may trigger a lint error. Removing the blank line ensures proper formatting:

> ...
>
> ...
> ...
> ...
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

1590-1590: Blank line inside blockquote
null

(MD028, no-blanks-blockquote)

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCache.java (3)

57-57: Correct the constant name.

DEFAULT_EXPIRED_AFTER_ACCESS_IN_SENCONDS has a typographical error and should probably end with _SECONDS.

- private static final long DEFAULT_EXPIRED_AFTER_ACCESS_IN_SENCONDS = 10;
+ private static final long DEFAULT_EXPIRED_AFTER_ACCESS_IN_SECONDS = 10;

64-64: Restrict the visibility of the cache reference.

Consider making releasesCache private unless it must be accessed from externally. Public fields can introduce unintended external mutations.

- public LoadingCache<String, Optional<Release>> releasesCache;
+ private LoadingCache<String, Optional<Release>> releasesCache;

150-152: Add logging for the caught ExecutionException.

Swallowing the exception silently may hinder debugging. Logging the error or rethrowing a wrapped exception can help diagnose problems.

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java (2)

103-142: Consider using the enum for change types to avoid string literals.
The logic of detecting ADDED, DELETED, and MODIFIED configurations appears correct. However, you're currently using raw strings (e.g., "ADDED") instead of the ConfigurationChangeType enum. Relying on the enum would reduce the risk of typos and make the code more self-documenting.

- changes.add(new ConfigurationChange(newKey, latestReleaseConfigurations.get(newKey), "ADDED"));
+ changes.add(new ConfigurationChange(
+     newKey, 
+     latestReleaseConfigurations.get(newKey),
+     ConfigurationChangeType.ADDED.name()
+ ));

144-147: Avoid returning null in place of an unsupported or empty result.
findReleasesByReleaseKeys returns null, which may cause NullPointerExceptions for callers if they don't anticipate that scenario. Consider throwing an UnsupportedOperationException or returning an empty Map to be more explicit and safer.

-public Map<String, Release> findReleasesByReleaseKeys(Set<String> releaseKeys) {
-  return null;
+public Map<String, Release> findReleasesByReleaseKeys(Set<String> releaseKeys) {
+  return Collections.emptyMap();
}
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCacheTest.java (1)

203-223: Behavior when a release is not found.
Returning null for a missing release map is consistent with your implementation in AbstractConfigService. However, keep in mind that using an empty map might be clearer for callers since it avoids null checks.

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java (1)

163-194: Incremental change handling block.
Your incremental-change logic looks correct:

  • Parsing client release keys into a LinkedHashSet.
  • Looking up historical releases in order.
  • Computing differences with calcConfigurationChanges.
  • Setting the sync type to INCREMENTAL_SYNC.

As the controller method grows, consider extracting this block into a dedicated function in a service layer to enhance maintainability and keep the controller focused on request handling.

apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java (1)

538-572: Incremental sync test is implemented correctly.
Covers client- vs. server-side releases and asserts ConfigSyncType.INCREMENTAL_SYNC.

A complementary test for the case when isConfigServiceIncrementalChangeEnabled() is false would further enhance coverage.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7380bde and 56df3a8.

📒 Files selected for processing (20)
  • apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java (1 hunks)
  • apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseRepository.java (1 hunks)
  • apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java (1 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java (2 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java (6 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java (2 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java (1 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java (1 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCache.java (1 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/IncrementalSyncConfigService.java (1 hunks)
  • apollo-configservice/src/main/resources/jpa/configdb.init.h2.sql (1 hunks)
  • apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java (23 hunks)
  • apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCacheTest.java (1 hunks)
  • changes/changes-2.4.0.md (1 hunks)
  • docs/en/deployment/distributed-deployment-guide.md (1 hunks)
  • docs/zh/deployment/distributed-deployment-guide.md (1 hunks)
  • scripts/sql/profiles/h2-default/apolloconfigdb.sql (1 hunks)
  • scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql (1 hunks)
  • scripts/sql/profiles/mysql-default/apolloconfigdb.sql (1 hunks)
  • scripts/sql/src/apolloconfigdb.sql (1 hunks)
🧰 Additional context used
🧬 Code Definitions (2)
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java (1)
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCache.java (1)
  • ConfigServiceWithChangeCache (52-155)
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCacheTest.java (1)
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java (1)
  • RunWith (58-675)
🪛 GitHub Actions: build
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java

[warning] 1-1: uses unchecked or unsafe operations. Recompile with -Xlint:unchecked for details.

🪛 markdownlint-cli2 (0.17.2)
docs/en/deployment/distributed-deployment-guide.md

1656-1656: Blank line inside blockquote
null

(MD028, no-blanks-blockquote)

docs/zh/deployment/distributed-deployment-guide.md

1590-1590: Blank line inside blockquote
null

(MD028, no-blanks-blockquote)

🔇 Additional comments (65)
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseRepository.java (2)

40-40: Appropriate addition of repository method.

This method complements the existing findByReleaseKeyIn method by providing a way to fetch a single Release by its key, which will be useful for the incremental configuration synchronization feature.


48-48: Parameter name improvement.

The parameter name change from releaseKey to releaseKeys in this method signature improves clarity as it's dealing with a Set of keys.

changes/changes-2.4.0.md (1)

12-14: Documentation updated correctly.

The changelog entry appropriately documents the new incremental configuration synchronization client feature with a link to the related PR.

apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java (1)

243-245: Well-designed configuration flag.

The method follows the established pattern of other feature flag methods in the class and defaults to false, which is a safe choice for introducing new functionality. It will be used by the config service to determine whether to use incremental changes.

apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java (1)

122-124: Appropriate service method addition.

This method provides a clean interface to the new repository functionality, following the same pattern as other similar methods in the class.

scripts/sql/profiles/mysql-default/apolloconfigdb.sql (1)

505-506: Added new configuration toggle for incremental sync feature.

A new server configuration entry has been added to control the incremental configuration synchronization feature. The default value is set to false, which is appropriate for a feature that increases memory consumption.

The descriptive comment clearly explains the performance vs. memory trade-off, helping administrators make informed decisions when configuring the system. This follows the same pattern as the existing cache configuration.

scripts/sql/src/apolloconfigdb.sql (1)

493-494: Added configuration entry in source SQL template.

This adds the incremental configuration synchronization toggle in the source SQL template, consistent with the changes in the derived SQL files.

The configuration is properly described with a comment that explains the performance benefits and memory implications. Maintaining consistency between this source template and the generated SQL files is important for the build process.

scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql (1)

500-501: Added incremental sync configuration toggle for non-specified database profile.

The configuration entry for enabling incremental synchronization has been added consistently across all database profiles, including this one for non-specified databases.

The setting defaults to false, which is a safe default that allows explicit opt-in after considering the memory implications. The comment explains the purpose and trade-offs clearly.

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java (2)

33-33: Import for new feature.

The import statement for ConfigServiceWithChangeCache is needed to support the new incremental configuration synchronization feature.


73-76: Well-implemented conditional logic for incremental change support.

This code adds support for incremental configuration synchronization by checking if the feature is enabled before instantiating a ConfigServiceWithChangeCache. The implementation follows the existing pattern of conditional service instantiation and maintains backward compatibility.

The code properly reuses the existing constructor parameters, ensuring that all necessary dependencies are passed to the new implementation.

scripts/sql/profiles/h2-default/apolloconfigdb.sql (1)

486-487: Coherent configuration addition for incremental change feature.

The new configuration entry config-service.incremental.change.enabled properly follows the existing pattern with a descriptive comment about the feature's impact on performance and memory consumption. Setting the default to 'false' is a safe choice as it requires explicit opt-in.

apollo-configservice/src/main/resources/jpa/configdb.init.h2.sql (1)

22-23: Consistent H2 database configuration for incremental change feature.

The addition of the same configuration parameter to the H2 initialization script ensures consistency between different database environments. The default value is properly set to 'false', requiring explicit opt-in.

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/IncrementalSyncConfigService.java (2)

35-36: Method name clarity is commendable.

The method signature calcConfigurationChanges is descriptively named and the parameters convey intent clearly.


38-42: Adequate interface definition for fetching releases by keys.

The findReleasesByReleaseKeys method is straightforward to understand, promoting code clarity.

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java (1)

65-67: Consider the design implications of exposing these fields as protected.
Making releaseService and bizConfig protected allows subclasses to access them directly, which can be useful for extensions but can also reduce encapsulation and open up potential misuse if subclasses override or alter their behavior. Overall your change appears intentional to facilitate subclassing; just ensure that you trust subclasses to handle these references appropriately.

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java (1)

24-33: New imports look appropriate.
No immediate issues or concerns with these added imports; they align with your new method and data structures.

apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCacheTest.java (7)

93-111: Test case for added configurations.
This test successfully verifies an added configuration scenario. The assertions are clear and the logic is correct.


113-127: Test case for null latest configurations.
The validation is accurate, confirming that previously existing keys are considered deleted. No issues found.


129-143: Test case for null history configurations.
This mirrors the above scenario and shows the correct handling of a null history by treating all new keys as added. Looks good.


145-162: Test case for updating configurations.
Properly validates the transition from one value to another as a MODIFIED change. Implementation is sound.


164-179: Test case for deleted configurations.
Correctly checks that removal of an existing key is flagged as DELETED. No problems noted.


181-201: Caching test for release lookups.
This verifies that repeated lookups hit the cache rather than calling the service each time. Well-structured test that ensures caching logic correctness.


225-245: Test notification-based cache invalidation.
This test checks that a valid release can be retrieved after handling a release message without extra calls to findByReleaseKey. Implementation is correct.

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java (4)

19-52: New imports are appropriate.
The added imports (for BizConfig, ConfigurationChange, Sets, etc.) fit well with the incremental sync features and do not introduce unused references.


70-89: Constructor injection for BizConfig.
Injecting BizConfig here and storing it in a final member is consistent with other dependencies. Good use of constructor injection for clarity and testability.


147-157: 304 Not Modified early exit.
This logic checks if the client-side release key matches the latest merged release key and avoids sending a new payload if unchanged. This is a standard optimization to save bandwidth.


159-162: Creation of ApolloConfig with a merged release key.
Constructing the ApolloConfig using the newly combined release key is straightforward and clear.

apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java (38)

19-19: No issues with new import for BizConfig.
This import is needed for testing the new incremental sync feature.


29-30: New imports for ConfigurationChange and ConfigSyncType look correct.
These classes are appropriately used to capture config changes and the sync type.


34-34: New import for Sets is valid.
Used for handling sets of release keys in incremental sync tests.


37-37: New import for TypeToken is fine.
Allows parsing JSON configurations to maps for incremental sync checks.


44-44: Importing Type is correct.
Used in conjunction with TypeToken to parse configurations.


46-47: Imports of ArrayList and List are standard.
Used to store and verify configuration changes within tests.


66-67: Mocking BizConfig is necessary for toggling incremental sync in tests.
This aligns with the new feature under test.


84-85: Mocking additional Release objects is valid.
Allows simulating multiple release scenarios in test methods.


119-124: Stubbing namespace utilities is consistent with the test flow.
Ensures correct filtering and normalization of namespaces.


127-128: Stubbing message transformation is correct.
Verifies that notification messages are handled properly in tests.


137-139: Stubbing configService.loadConfig(...) with matching parameters is valid.
Reflects the intended test scenario for the default namespace.


147-149: Verifying configService.loadConfig(...) invocation is standard.
Ensures the service call is triggered exactly once.


155-156: Instance config audit verification is handled properly.
Checks that audits occur with the correct parameters.


166-168: Reused stub for loading config is consistent.
Confirms that private “file” namespace logic is uniformly tested.


170-172: Stubbing namespace filtering and normalization is valid.
Ensures the test can confirm correct transformations for file-based namespaces.


197-198: Stubbing configService.loadConfig(...) for private namespace is correct.
Validates the scenario for private configurations.


200-203: Namespace stubbing for private namespace is accurate.
Ensures filtering and normalization logic is tested thoroughly.


222-224: Returning null from configService.loadConfig(...) tests the not-found case.
Good coverage for missing release scenarios.


240-242: Repeated stub for no-change scenario remains valid.
Tests the flow when client release matches server release key.


246-250: Query call with all arguments is properly formed.
Ensures the test checks for 304 Not Modified logic in the controller.


265-266: Stubbing config loading for an app-owned namespace is appropriate.
Ensures coverage for application-specific namespaces.


298-300: Stub for public namespace without app override stands correct.
Helps validate fallback logic in the test.


303-305: Loading config from the public app ID is handled properly.
Verifies fallback release is fetched as intended.


313-314: Method call line breaks are unchanged logically.
No issues with passing existing parameters.


322-323: Instance config audit utility verification is correct.
Ensures correct usage for auditing public namespaces.


336-338: Stubbing null return simulates missing app override.
Tests fallback to the public release.


341-343: Public config load stub is valid.
Checks that the public release is found and returned.


345-348: Namespace utility stubs ensure correct translation from the file extension.
No issues spotted.


349-350: Stubbing appNamespaceService.findByAppIdAndNamespace(...) is consistent.
Expects a null return if no app-specific definition exists.


378-380: Valid stubbing for app-level release in combined override tests.
Helps confirm merges from multiple sources.


385-387: Public-level release stub is properly set up.
Facilitates testing of combined app & public overrides.


406-406: Instance config audit verification calls remain consistent.
Multiple audits are expected for app and public releases.

Also applies to: 411-411


461-463: Verifying configService.loadConfig(...) is never called is appropriate.
Checks the scenario for NO_APPID_PLACEHOLDER as expected.


482-483: Stub load config for public namespace with no actual app ID is correct.
Handles fallback to the public app in the test.


485-486: Namespace normalization stub is consistent.
Ensures placeholders are correctly mapped to real namespaces.


574-599: Handling missing old releases is well-tested.
The logic for incremental sync with a null or unknown release key is verified successfully.


601-671: Tests public namespace overrides within incremental sync thoroughly.
Effectively validates merging of configurations from app and public releases.


673-675: Using TypeToken<Map<String, String>> to parse configurations is correct.
Essential for calculating configuration changes.

Copy link
Member

@nobodyiam nobodyiam left a comment

Choose a reason for hiding this comment

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

I copied some comments from the previous PR #5288

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

♻️ Duplicate comments (1)
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java (1)

190-197: Business logic leakage into controller – consider moving to service layer

The controller assembles historical releases, merges configurations and
derives the diff. This mixes orchestration with business rules and
duplicates logic already living in IncrementalSyncService
(see previous review by nobodyiam).

Prefer delegating the entire “get incremental response” operation to
IncrementalSyncService, letting the controller focus on HTTP concerns.

🧹 Nitpick comments (10)
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/DefaultConfigService.java (1)

64-71: Comment is misleading compared to implementation.

The comment "since there is no cache, so do nothing" doesn't match the actual implementation which does retrieve data from the releaseService.

Consider updating the comment to accurately reflect what the method does:

-  // since there is no cache, so do nothing
+  // directly retrieve from releaseService without cache

Also, consider returning an empty map instead of null when no releases are found, to avoid potential NullPointerExceptions:

   if (releasesMap != null) {
     return ImmutableMap.copyOf(releasesMap.stream().collect(ImmutableMap.toImmutableMap(Release::getReleaseKey, release -> release)));
   }
-  return null;
+  return ImmutableMap.of();
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/IncrementalSyncService.java (1)

25-32: Add documentation for the interface and method.

This new interface is critical for the incremental sync feature, but lacks documentation explaining its purpose and how it should be used.

Consider adding more detailed JavaDoc to explain:

  1. The purpose of this interface
  2. What ConfigurationChange represents
  3. The expected behavior when computing differences
  4. How to interpret the returned list

For example:

/**
 * Service to calculate incremental configuration changes between server and client configurations.
 * This service helps reduce network traffic by sending only the differences rather than full configurations.
 */
public interface IncrementalSyncService {

  /**
   * Computes the configuration changes between the latest server configuration and client-side configuration.
   * 
   * @param latestMergedReleaseKey The release key of the latest merged configuration on server side
   * @param latestReleaseConfigurations The configuration map of the latest release on server side
   * @param clientSideReleaseKey The release key of the client's current configuration
   * @param clientSideConfigurations The configuration map of the client's current configuration
   * @return A list of configuration changes (additions, deletions, modifications) that the client
   *         needs to apply to synchronize with the server
   */
  List<ConfigurationChange> getConfigurationChanges(...);
}
docs/en/deployment/distributed-deployment-guide.md (1)

1666-1680: Documentation has minor inconsistencies.

The documentation for the new incremental synchronization configuration is generally clear but has a few minor issues:

  1. The last paragraph contains an incorrect reference that needs to be updated:
-> `config-service.cache.enabled` configuration adjustment requires a restart of the config service to take effect
+> `config-service.incremental.change.enabled` configuration adjustment requires a restart of the config service to take effect
  1. There's a blank line inside the blockquote at line 1678 that should be removed (as flagged by markdownlint).

Otherwise, the documentation provides good information about the new feature, its purpose, and considerations for enabling it.

🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

1678-1678: Blank line inside blockquote
null

(MD028, no-blanks-blockquote)

docs/zh/deployment/distributed-deployment-guide.md (1)

1610-1611: Remove blank line inside blockquote to satisfy Markdown‑lint

markdownlint (rule MD028) warns about empty lines within blockquotes.
The two consecutive quoted lines should be merged into a single contiguous blockquote without a blank line in between.

-> 开启缓存后必须确保应用中配置的`app.id`、`apollo.cluster`
->
+> 开启缓存后必须确保应用中配置的`app.id`、`apollo.cluster`
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/DefaultIncrementalSyncServiceTest.java (2)

118-123: Prefer assertNull over assertEquals(null, …)

Using the dedicated assertNull assertion makes failures clearer and avoids
being flagged by static‑analysis tools.

-    assertEquals(null, result.get(0).getNewValue());
+    assertNull(result.get(0).getNewValue());

102-106: Consider replacing hard‑coded strings with the enum to avoid fragile tests

"ADDED", "MODIFIED", and "DELETED" are repeated string literals.
If the underlying ConfigurationChangeType enum ever changes, these
tests will silently compile but fail at runtime.
Import the enum and assert against ConfigurationChangeType.ADDED.name()
(or expose a getter returning the enum).

apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCacheTest.java (1)

135-137: Duplicate verify statement

verify(releaseService, times(1)).findActiveOne(someId); appears twice
in succession, offering no additional value and slightly slowing the test.

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/DefaultIncrementalSyncService.java (2)

68-70: Remove unused local variable to avoid reader confusion

ReleaseKeyPair key = new ReleaseKeyPair(clientSideReleaseKey, latestMergedReleaseKey);
is created but never used in calcConfigurationChanges. This is a left‑over from an early refactor and can be safely deleted.

-    ReleaseKeyPair key = new ReleaseKeyPair(clientSideReleaseKey, latestMergedReleaseKey);

70-76: Prefer immutable empty maps instead of allocating new HashMap

Creating new, mutable HashMap instances just to represent an empty map introduces unnecessary allocations and allows unintended mutation. Replace with Collections.emptyMap() which is cheaper and signals intent.

-    if (latestReleaseConfigurations == null) {
-      latestReleaseConfigurations = new HashMap<>();
-    }
+    if (latestReleaseConfigurations == null) {
+      latestReleaseConfigurations = java.util.Collections.emptyMap();
+    }
 
-    if (clientSideConfigurations == null) {
-      clientSideConfigurations = new HashMap<>();
-    }
+    if (clientSideConfigurations == null) {
+      clientSideConfigurations = java.util.Collections.emptyMap();
+    }
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java (1)

544-546: State shared bizConfig stub per test to avoid cross‑test leakage

when(bizConfig.isConfigServiceIncrementalChangeEnabled()).thenReturn(true);

Because the same bizConfig mock is reused across tests, set the default in setUp() and override in specific tests if needed. This prevents subtle order‑dependent failures when future tests expect the default (false).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56df3a8 and 9bba12c.

📒 Files selected for processing (18)
  • apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java (1 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java (1 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java (5 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java (2 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java (2 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java (6 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/DefaultConfigService.java (2 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/DefaultIncrementalSyncService.java (1 hunks)
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/IncrementalSyncService.java (1 hunks)
  • apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java (22 hunks)
  • apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCacheAndCacheKeyIgnoreCaseTest.java (1 hunks)
  • apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCacheTest.java (7 hunks)
  • apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/DefaultConfigServiceTest.java (1 hunks)
  • apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/DefaultIncrementalSyncServiceTest.java (1 hunks)
  • changes/changes-2.4.0.md (1 hunks)
  • docs/en/deployment/distributed-deployment-guide.md (1 hunks)
  • docs/zh/deployment/distributed-deployment-guide.md (1 hunks)
  • pom.xml (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/DefaultConfigServiceTest.java
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java
  • apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCacheAndCacheKeyIgnoreCaseTest.java
  • changes/changes-2.4.0.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java
  • apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java (1)
apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/WebUtils.java (1)
  • WebUtils (27-41)
🪛 GitHub Actions: build
pom.xml

[error] 1-1: Failed to execute goal on project apollo-common: Could not resolve dependency com.ctrip.framework.apollo:apollo-core:jar:2.4.0-SNAPSHOT. Artifact not found in sonatype-nexus-snapshots repository.

🪛 markdownlint-cli2 (0.17.2)
docs/en/deployment/distributed-deployment-guide.md

1678-1678: Blank line inside blockquote
null

(MD028, no-blanks-blockquote)

docs/zh/deployment/distributed-deployment-guide.md

1616-1616: Blank line inside blockquote
null

(MD028, no-blanks-blockquote)

🔇 Additional comments (1)
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java (1)

655-662: Use real release keys when asserting incremental diff

incrementalSyncService.getConfigurationChanges("", …, "", …) passes empty strings as release keys, while meaningful keys (someAppServerSideReleaseKey, mergeClientSideReleaseKey …) are available. Supplying real keys strengthens the contract and documents intent.

-when(incrementalSyncService.getConfigurationChanges("", 
+when(incrementalSyncService.getConfigurationChanges(
+        Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR)
+              .join(someAppServerSideReleaseKey, somePublicAppSideReleaseKey),
         gson.fromJson(mergeServerSideConfigurations, configurationTypeReference),
-        "",
+        mergeClientSideReleaseKey,
         gson.fromJson(mergeClientSideConfigurations, configurationTypeReference)))

Comment on lines 63 to 67
<revision>2.5.0-SNAPSHOT</revision>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<apollo-java.version>2.5.0-SNAPSHOT</apollo-java.version>
<apollo-java.version>2.4.0-SNAPSHOT</apollo-java.version>
<spring-boot.version>2.7.11</spring-boot.version>
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Mismatch between project <revision> and <apollo-java.version>
The <apollo-java.version> has been downgraded to 2.4.0-SNAPSHOT, but the project’s <revision> remains at 2.5.0-SNAPSHOT. As a result, the build is looking for apollo-core:2.4.0-SNAPSHOT, which isn’t published in the snapshots repo, causing the pipeline failure.

Please align these versions by either:

  1. Bumping <revision> down to 2.4.0-SNAPSHOT so both properties match, or
  2. Restoring <apollo-java.version> to 2.5.0-SNAPSHOT if you intend to stay on the 2.5.0 line.

Proposed fix (option 1: align to 2.4.0-SNAPSHOT):

-    <revision>2.5.0-SNAPSHOT</revision>
+    <revision>2.4.0-SNAPSHOT</revision>
...
-    <apollo-java.version>2.4.0-SNAPSHOT</apollo-java.version>
+    <apollo-java.version>2.4.0-SNAPSHOT</apollo-java.version>

Or option 2 (restore to 2.5.0-SNAPSHOT):

-    <apollo-java.version>2.4.0-SNAPSHOT</apollo-java.version>
+    <apollo-java.version>2.5.0-SNAPSHOT</apollo-java.version>

To confirm how the apollo-java.version property is used and whether the apollo-core dependency is pulling in the mismatched version, let’s search for its definition and usage:


🏁 Script executed:

#!/bin/bash
# 1. Show where the apollo-java.version property is defined
echo "=== Definition of apollo-java.version property ==="
rg -n "<apollo-java.version>" -n pom.xml

# 2. Find all places that reference the apollo-java.version property
echo -e "\n=== Usage of the apollo-java.version property ==="
rg -n "\${apollo-java.version}" -n .

# 3. Locate any direct declaration of an apollo-core dependency
echo -e "\n=== Declaration of apollo-core dependency ==="
rg -n "<artifactId>apollo-core" -n .

Length of output: 753


Let’s inspect how the apollo-core dependency is declared and verify which <version> it’s using:


🏁 Script executed:

#!/bin/bash
# 1. Show the surrounding lines in the root pom where apollo-core is declared
echo "=== Root pom.xml (lines around apollo-core) ==="
rg -n '<artifactId>apollo-core' -n pom.xml -A3 -B3

# 2. Show the surrounding lines in apollo-common/pom.xml where apollo-core is declared
echo -e "\n=== apollo-common/pom.xml (lines around apollo-core) ==="
rg -n '<artifactId>apollo-core' -n apollo-common/pom.xml -A3 -B3

# 3. Find any references to the apollo-java.version property
echo -e "\n=== References to apollo-java.version ==="
rg -n -F '${apollo-java.version}' -n .

Length of output: 1113


Align <revision> with <apollo-java.version> to fix missing snapshot artifact
The project’s <revision> is still set to 2.5.0‑SNAPSHOT while <apollo-java.version> was lowered to 2.4.0‑SNAPSHOT. This causes Maven to request com.ctrip.framework.apollo:apollo-core:2.4.0‑SNAPSHOT (used in dependencies at lines 114 & 149), which isn’t published in our snapshots repo.

Please choose one of the following:

  1. Restore <apollo-java.version> to 2.5.0‑SNAPSHOT so it matches <revision>, or
  2. Downgrade <revision> to 2.4.0‑SNAPSHOT if you really intend to stay on 2.4.x.

Affected lines in pom.xml:

  • Line 63: <revision>
  • Line 66: <apollo-java.version>

Diff for option 1 (preferred):

-    <apollo-java.version>2.4.0-SNAPSHOT</apollo-java.version>
+    <apollo-java.version>2.5.0-SNAPSHOT</apollo-java.version>
📝 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
<revision>2.5.0-SNAPSHOT</revision>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<apollo-java.version>2.5.0-SNAPSHOT</apollo-java.version>
<apollo-java.version>2.4.0-SNAPSHOT</apollo-java.version>
<spring-boot.version>2.7.11</spring-boot.version>
<revision>2.5.0-SNAPSHOT</revision>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <apollo-java.version>2.4.0-SNAPSHOT</apollo-java.version>
+ <apollo-java.version>2.5.0-SNAPSHOT</apollo-java.version>
<spring-boot.version>2.7.11</spring-boot.version>

Comment on lines +72 to +76
//四种情况
//开启增量更新
if (bizConfig.isConfigServiceIncrementalChangeEnabled()) {

}
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

Implementation needed for incremental change condition.

The code adds a new conditional branch to check for incremental change support, but the branch is currently empty and has no implementation or return statement.

This seems to be a preparatory step for implementing the incremental change feature. Consider either:

  1. Implementing the actual logic inside this branch
  2. Adding a comment describing what will be implemented here in future changes

Also note the previous review comment suggesting that the config-service.cache.enabled and config-service.incremental.change.enabled flags could potentially be combined to simplify the configuration options.

Comment on lines +184 to +199
public void testFindReleasesByReleaseKeysNotFoundMultipleTimes() throws Exception {
String someReleaseKey = "someReleaseKey";
long someId = 1;
Set<String> someReleaseKeys = Sets.newHashSet(someReleaseKey);

when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn(null);
when(releaseService.findActiveOne(someId)).thenReturn(null);



ImmutableMap<String, Release> someReleaseMap = configServiceWithCache.findReleasesByReleaseKeys(
someReleaseKeys);
assertEquals(1, someReleaseMap.size());
assertEquals(someRelease, someReleaseMap.get(someReleaseKey));
verify(releaseService, times(1)).findByReleaseKey(someReleaseKey);
}
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

Failing expectation – asserting a non‑existent release

releaseService.findByReleaseKey(someReleaseKey) is stubbed to return
null, yet the test later asserts that the returned map contains
someRelease. This will always fail.

-    when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn(null);
+    // Expect not‑found; the map should contain a null value
+    when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn(null);
...
-    assertEquals(someRelease, someReleaseMap.get(someReleaseKey));
+    assertNull("When the release is not found the cache should contain null",
+               someReleaseMap.get(someReleaseKey));

Double‑check the intended behaviour of findReleasesByReleaseKeys and
update either the production code or the assertion accordingly.

📝 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 testFindReleasesByReleaseKeysNotFoundMultipleTimes() throws Exception {
String someReleaseKey = "someReleaseKey";
long someId = 1;
Set<String> someReleaseKeys = Sets.newHashSet(someReleaseKey);
when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn(null);
when(releaseService.findActiveOne(someId)).thenReturn(null);
ImmutableMap<String, Release> someReleaseMap = configServiceWithCache.findReleasesByReleaseKeys(
someReleaseKeys);
assertEquals(1, someReleaseMap.size());
assertEquals(someRelease, someReleaseMap.get(someReleaseKey));
verify(releaseService, times(1)).findByReleaseKey(someReleaseKey);
}
public void testFindReleasesByReleaseKeysNotFoundMultipleTimes() throws Exception {
String someReleaseKey = "someReleaseKey";
long someId = 1;
Set<String> someReleaseKeys = Sets.newHashSet(someReleaseKey);
// Expect not‑found; the map should contain a null value
when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn(null);
when(releaseService.findActiveOne(someId)).thenReturn(null);
ImmutableMap<String, Release> someReleaseMap = configServiceWithCache.findReleasesByReleaseKeys(
someReleaseKeys);
assertEquals(1, someReleaseMap.size());
assertNull("When the release is not found the cache should contain null",
someReleaseMap.get(someReleaseKey));
verify(releaseService, times(1)).findByReleaseKey(someReleaseKey);
}

Comment on lines +168 to +202
if (bizConfig.isConfigServiceIncrementalChangeEnabled()) {
LinkedHashSet<String> clientSideReleaseKeys = Sets.newLinkedHashSet(
Arrays.stream(
clientSideReleaseKey.split(Pattern.quote(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR)))
.collect(Collectors.toList()));

Map<String, Release> clientSideReleases = configService.findReleasesByReleaseKeys(
clientSideReleaseKeys);
//find history releases
if (clientSideReleases != null) {
//order by clientSideReleaseKeys
List<Release> historyReleasesWithOrder = new ArrayList<>();
for (String item : clientSideReleaseKeys) {
Release release = clientSideReleases.get(item);
if (release != null) {
historyReleasesWithOrder.add(release);
}
}

Map<String, String> clientSideConfigurations = mergeReleaseConfigurations
(historyReleasesWithOrder);

List<ConfigurationChange> configurationChanges = incrementalSyncService.getConfigurationChanges(
latestMergedReleaseKey,
latestConfigurations, clientSideReleaseKey, clientSideConfigurations);

apolloConfig.setConfigurationChanges(configurationChanges);

apolloConfig.setConfigSyncType(ConfigSyncType.INCREMENTAL_SYNC.getValue());
Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded,
originalNamespace, dataCenter));
return apolloConfig;
}

}
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

Skip incremental diff when client has no valid release key

If the client passes the default sentinel ("-1") or an empty value,
clientSideReleaseKeys will contain a single element "-1".
findReleasesByReleaseKeys will then perform an unnecessary cache/database
lookup and getConfigurationChanges will treat the entire namespace as
ADDED, sending a full payload disguised as “incremental”.

Add a guard clause before starting the incremental‑sync branch:

-    if (bizConfig.isConfigServiceIncrementalChangeEnabled()) {
+    if (bizConfig.isConfigServiceIncrementalChangeEnabled()
+        && !"-1".equals(clientSideReleaseKey)
+        && !Strings.isNullOrEmpty(clientSideReleaseKey)) {

This avoids wasted work and keeps wire‑format semantics intuitive.

📝 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 (bizConfig.isConfigServiceIncrementalChangeEnabled()) {
LinkedHashSet<String> clientSideReleaseKeys = Sets.newLinkedHashSet(
Arrays.stream(
clientSideReleaseKey.split(Pattern.quote(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR)))
.collect(Collectors.toList()));
Map<String, Release> clientSideReleases = configService.findReleasesByReleaseKeys(
clientSideReleaseKeys);
//find history releases
if (clientSideReleases != null) {
//order by clientSideReleaseKeys
List<Release> historyReleasesWithOrder = new ArrayList<>();
for (String item : clientSideReleaseKeys) {
Release release = clientSideReleases.get(item);
if (release != null) {
historyReleasesWithOrder.add(release);
}
}
Map<String, String> clientSideConfigurations = mergeReleaseConfigurations
(historyReleasesWithOrder);
List<ConfigurationChange> configurationChanges = incrementalSyncService.getConfigurationChanges(
latestMergedReleaseKey,
latestConfigurations, clientSideReleaseKey, clientSideConfigurations);
apolloConfig.setConfigurationChanges(configurationChanges);
apolloConfig.setConfigSyncType(ConfigSyncType.INCREMENTAL_SYNC.getValue());
Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded,
originalNamespace, dataCenter));
return apolloConfig;
}
}
if (bizConfig.isConfigServiceIncrementalChangeEnabled()
&& !Strings.isNullOrEmpty(clientSideReleaseKey)
&& !"-1".equals(clientSideReleaseKey)) {
LinkedHashSet<String> clientSideReleaseKeys = Sets.newLinkedHashSet(
Arrays.stream(
clientSideReleaseKey.split(Pattern.quote(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR)))
.collect(Collectors.toList()));
Map<String, Release> clientSideReleases = configService.findReleasesByReleaseKeys(
clientSideReleaseKeys);
//find history releases
if (clientSideReleases != null) {
//order by clientSideReleaseKeys
List<Release> historyReleasesWithOrder = new ArrayList<>();
for (String item : clientSideReleaseKeys) {
Release release = clientSideReleases.get(item);
if (release != null) {
historyReleasesWithOrder.add(release);
}
}
Map<String, String> clientSideConfigurations = mergeReleaseConfigurations
(historyReleasesWithOrder);
List<ConfigurationChange> configurationChanges = incrementalSyncService.getConfigurationChanges(
latestMergedReleaseKey,
latestConfigurations, clientSideReleaseKey, clientSideConfigurations);
apolloConfig.setConfigurationChanges(configurationChanges);
apolloConfig.setConfigSyncType(ConfigSyncType.INCREMENTAL_SYNC.getValue());
Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded,
originalNamespace, dataCenter));
return apolloConfig;
}
}

Comment on lines +88 to +104
changes.add(new ConfigurationChange(newKey, latestReleaseConfigurations.get(newKey),
"ADDED"));
}

for (String removedKey : removedKeys) {
changes.add(new ConfigurationChange(removedKey, null, "DELETED"));
}

for (String commonKey : commonKeys) {
String previousValue = clientSideConfigurations.get(commonKey);
String currentValue = latestReleaseConfigurations.get(commonKey);
if (com.google.common.base.Objects.equal(previousValue, currentValue)) {
continue;
}
changes.add(
new ConfigurationChange(commonKey, currentValue, "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

Avoid hard‑coded change‑type literals – use an enum or constant

Passing raw strings ("ADDED", "DELETED", "MODIFIED") makes the call‑sites brittle and error‑prone. ConfigurationChange already carries semantics; consider:

public enum ChangeType { ADDED, DELETED, MODIFIED }

and overload the constructor (or add a static factory) accepting ChangeType. This prevents typos and enables IDE refactors.

Comment on lines +119 to +135
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ReleaseKeyPair)) {
return false;
}
ReleaseKeyPair that = (ReleaseKeyPair) obj;
return clientSideReleaseKey.equals(that.clientSideReleaseKey) &&
latestMergedReleaseKey.equals(that.latestMergedReleaseKey);
}

@Override
public int hashCode() {
return clientSideReleaseKey.hashCode() * 31 + latestMergedReleaseKey.hashCode();
}
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

ReleaseKeyPair is not null‑safe

equals and hashCode dereference the release‑key fields directly, leading to NullPointerException if either key is ever null (e.g., client sends empty release key on first fetch).

-      return clientSideReleaseKey.equals(that.clientSideReleaseKey) &&
-          latestMergedReleaseKey.equals(that.latestMergedReleaseKey);
+      return java.util.Objects.equals(clientSideReleaseKey, that.clientSideReleaseKey) &&
+             java.util.Objects.equals(latestMergedReleaseKey, that.latestMergedReleaseKey);
...
-      return clientSideReleaseKey.hashCode() * 31 + latestMergedReleaseKey.hashCode();
+      return java.util.Objects.hash(clientSideReleaseKey, latestMergedReleaseKey);

Alternatively, validate non‑nullity in the constructor and throw IllegalArgumentException.

📝 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 boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ReleaseKeyPair)) {
return false;
}
ReleaseKeyPair that = (ReleaseKeyPair) obj;
return clientSideReleaseKey.equals(that.clientSideReleaseKey) &&
latestMergedReleaseKey.equals(that.latestMergedReleaseKey);
}
@Override
public int hashCode() {
return clientSideReleaseKey.hashCode() * 31 + latestMergedReleaseKey.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ReleaseKeyPair)) {
return false;
}
ReleaseKeyPair that = (ReleaseKeyPair) obj;
return java.util.Objects.equals(clientSideReleaseKey, that.clientSideReleaseKey) &&
java.util.Objects.equals(latestMergedReleaseKey, that.latestMergedReleaseKey);
}
@Override
public int hashCode() {
return java.util.Objects.hash(clientSideReleaseKey, latestMergedReleaseKey);
}

Comment on lines +548 to +556
HttpServletResponse someResponse = mock(HttpServletResponse.class);
ImmutableMap<String, Release> someReleaseMap = mock(ImmutableMap.class);

String anotherConfigurations = "{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}";

when(configService.findReleasesByReleaseKeys(Sets.newHashSet(clientSideReleaseKey))).thenReturn(
someReleaseMap);
when(someReleaseMap.get(clientSideReleaseKey)).thenReturn(someRelease);
when(someRelease.getConfigurations()).thenReturn(someConfigurations);
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

Do not mock final/immutable value classes – use real instances

ImmutableMap is final; Mockito cannot mock it without the inline‑mock‑maker plugin, and doing so hides real behaviour.
Build an actual map instead:

-ImmutableMap<String, Release> someReleaseMap = mock(ImmutableMap.class);
+ImmutableMap<String, Release> someReleaseMap =
+    ImmutableMap.<String, Release>builder()
+        .put(clientSideReleaseKey, someRelease)
+        .build();

Then remove the stubbing of get(...), which is now real.

📝 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
HttpServletResponse someResponse = mock(HttpServletResponse.class);
ImmutableMap<String, Release> someReleaseMap = mock(ImmutableMap.class);
String anotherConfigurations = "{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}";
when(configService.findReleasesByReleaseKeys(Sets.newHashSet(clientSideReleaseKey))).thenReturn(
someReleaseMap);
when(someReleaseMap.get(clientSideReleaseKey)).thenReturn(someRelease);
when(someRelease.getConfigurations()).thenReturn(someConfigurations);
HttpServletResponse someResponse = mock(HttpServletResponse.class);
ImmutableMap<String, Release> someReleaseMap =
ImmutableMap.<String, Release>builder()
.put(clientSideReleaseKey, someRelease)
.build();
String anotherConfigurations = "{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}";
when(configService.findReleasesByReleaseKeys(Sets.newHashSet(clientSideReleaseKey))).thenReturn(
someReleaseMap);
when(someReleaseMap.get(clientSideReleaseKey)).thenReturn(someRelease);
when(someRelease.getConfigurations()).thenReturn(someConfigurations);

if (bizConfig.isConfigServiceIncrementalChangeEnabled()) {

}
//开启本地缓存
Copy link
Member

Choose a reason for hiding this comment

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

Prefer English comments in code.

Suggested change
//开启本地缓存
// enable local cache

@@ -69,13 +69,18 @@ public GrayReleaseRulesHolder grayReleaseRulesHolder() {

@Bean
public ConfigService configService() {
//四种情况
//开启增量更新
if (bizConfig.isConfigServiceIncrementalChangeEnabled()) {
Copy link
Member

Choose a reason for hiding this comment

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

Has this part not been submitted yet?

@@ -23,7 +23,10 @@

import com.google.common.base.Strings;

import com.google.common.collect.ImmutableMap;
Copy link
Member

Choose a reason for hiding this comment

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

These changes seem unnecessary.

* @param releaseKeys
* @return the ReleaseMap
*/
ImmutableMap<String, Release> findReleasesByReleaseKeys(Set<String> releaseKeys);
Copy link
Member

Choose a reason for hiding this comment

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

It's better to use the interface type in the interface, while the implementation can return an ImmutableMap.

Suggested change
ImmutableMap<String, Release> findReleasesByReleaseKeys(Set<String> releaseKeys);
Map<String, Release> findReleasesByReleaseKeys(Set<String> releaseKeys);

private static final String TRACER_EVENT_CACHE_GET = "ConfigCache.Get";
private static final String TRACER_EVENT_CACHE_GET_ID = "ConfigCache.GetById";

private final ReleaseService releaseService;
protected final ReleaseService releaseService;
Copy link
Member

Choose a reason for hiding this comment

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

Why change private to protected?

try {
ImmutableMap<String, Long> releaseKeyMap = releaseKeyCache.getAll(releaseKeys);
if (CollectionUtils.isEmpty(releaseKeyMap)) {
return null;
Copy link
Member

Choose a reason for hiding this comment

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

For collection return values, prefer returning an empty collection like Collections.emptyMap() instead of null.

return ImmutableMap.copyOf(releases);
}

} catch (ExecutionException e) {
Copy link
Member

Choose a reason for hiding this comment

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

Would it be better to return the exception to the caller instead of leaving it out?


### 3.2.14 config-service.incremental.change.enabled - 是否开启增量配置同步客户端

> 适用于服务端2.4.0及以上版本 && 客户端2.3.0及以上版本
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
> 适用于服务端2.4.0及以上版本 && 客户端2.3.0及以上版本
> 适用于服务端2.5.0及以上版本 && Java客户端2.4.0及以上版本

@@ -63,7 +63,7 @@
<revision>2.5.0-SNAPSHOT</revision>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<apollo-java.version>2.5.0-SNAPSHOT</apollo-java.version>
<apollo-java.version>2.4.0-SNAPSHOT</apollo-java.version>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<apollo-java.version>2.4.0-SNAPSHOT</apollo-java.version>
<apollo-java.version>2.5.0-SNAPSHOT</apollo-java.version>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size:XL This PR changes 500-999 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants