Eliminate hardcoded protocol version and implement field-level version control

pull/1611/head
mingri31164 11 months ago
parent 50b937351b
commit 22f6371a94

@ -19,9 +19,14 @@ package cn.hippo4j.common.toolkit;
import cn.hippo4j.common.model.ThreadPoolParameter;
import cn.hippo4j.common.model.ThreadPoolParameterInfo;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -52,6 +57,34 @@ public class IncrementalContentUtil {
"executeTimeOut", "isAlarm", "capacityAlarm", "livenessAlarm"
};
private static final List<String> IDENTIFIER_FIELDS = Collections.unmodifiableList(Arrays.asList("tenantId", "itemId", "tpId"));
private static final List<String> CORE_PARAMETER_LIST = Collections.unmodifiableList(Arrays.asList(CORE_PARAMETERS));
private static final List<String> EXTENDED_PARAMETER_LIST = Collections.unmodifiableList(Arrays.asList(EXTENDED_PARAMETERS));
/**
* Mapping of field name to the minimum protocol version that should observe it. Clients whose
* protocol version is lower than the mapped value will skip the field when generating MD5s, so
* they never refresh on data they do not understand.
*/
private static final Map<String, Integer> FIELD_MIN_PROTOCOL_VERSION;
static {
Map<String, Integer> fieldVersion = new HashMap<>();
// Identifiers are required regardless of protocol version.
IDENTIFIER_FIELDS.forEach(field -> fieldVersion.put(field, 1));
// Core parameters affect pool behaviour, therefore protocol v1 clients must see them.
CORE_PARAMETER_LIST.forEach(field -> fieldVersion.put(field, 1));
// Initial new/extended fields with the next protocol version so current clients (v2)
// automatically skip them when generating MD5 values. Once a field is ready to be exposed
// to protocol v2 (or higher) clients, simply lower its minimum version accordingly.
EXTENDED_PARAMETER_LIST.forEach(field -> fieldVersion.put(field, PROTOCOL_VERSION + 1));
FIELD_MIN_PROTOCOL_VERSION = Collections.unmodifiableMap(fieldVersion);
}
/**
* Get core content for MD5 calculation (only essential parameters)
*
@ -84,18 +117,42 @@ public class IncrementalContentUtil {
}
/**
* Get incremental content for version compatibility
* Build content string according to client protocol version. Fields introduced in newer protocol
* versions will be excluded automatically for older clients to avoid unnecessary refresh.
*
* @param parameter thread-pool parameter
* @param version client version
* @return incremental content string
* @param parameter thread-pool parameter
* @param protocolVersion client protocol version
* @param clientVersion semantic client version (optional, reserved for fine-grained rules)
* @return version-aware content string
*/
public static String getIncrementalContent(ThreadPoolParameter parameter, int version) {
if (version >= PROTOCOL_VERSION) {
return getCoreContent(parameter);
} else {
return getFullContent(parameter);
public static String getVersionedContent(ThreadPoolParameter parameter, int protocolVersion, String clientVersion) {
String fullContent = getFullContent(parameter);
if (protocolVersion < PROTOCOL_VERSION) {
return fullContent;
}
LinkedHashMap<String, Object> raw = JSONUtil.parseObject(fullContent, new TypeReference<LinkedHashMap<String, Object>>() {
});
if (raw == null) {
return fullContent;
}
int normalizedProtocol = Math.max(protocolVersion, PROTOCOL_VERSION);
LinkedHashMap<String, Object> filtered = new LinkedHashMap<>();
for (String field : IDENTIFIER_FIELDS) {
if (raw.containsKey(field)) {
filtered.put(field, raw.get(field));
}
}
for (String field : CORE_PARAMETER_LIST) {
if (raw.containsKey(field)) {
filtered.put(field, raw.get(field));
}
}
raw.forEach((field, value) -> {
if (!filtered.containsKey(field) && shouldIncludeField(field, normalizedProtocol)) {
filtered.put(field, value);
}
});
return JSONUtil.toJSONString(filtered);
}
/**
@ -191,18 +248,12 @@ public class IncrementalContentUtil {
}
/**
* Create versioned content for backward compatibility
*
* @param parameter thread-pool parameter
* @param clientVersion client protocol version
* @return versioned content
* Decide whether the given field should be included when generating MD5 for a client that uses
* the specified protocol version. If the field requires a higher protocol, it will be ignored
* so older clients remain unaware of unsupported parameters.
*/
public static String createVersionedContent(ThreadPoolParameter parameter, int clientVersion) {
Map<String, Object> versionedContent = new HashMap<>();
versionedContent.put("version", PROTOCOL_VERSION);
versionedContent.put("clientVersion", clientVersion);
versionedContent.put("content", getIncrementalContent(parameter, clientVersion));
versionedContent.put("changes", getChangesSummary(null, parameter));
return JSONUtil.toJSONString(versionedContent);
private static boolean shouldIncludeField(String field, int protocolVersion) {
int minProtocol = FIELD_MIN_PROTOCOL_VERSION.getOrDefault(field, Integer.MAX_VALUE);
return protocolVersion >= minProtocol;
}
}

@ -56,19 +56,25 @@ public class IncrementalMd5Util {
* @return versioned MD5 hash
*/
public static String getVersionedMd5(ThreadPoolParameter config, int clientVersion) {
if (clientVersion >= IncrementalContentUtil.PROTOCOL_VERSION) {
String coreMd5 = getCoreMd5(config);
if (log.isDebugEnabled()) {
log.debug("Protocol v{}: Using incremental MD5 (core parameters only), MD5={}", clientVersion, coreMd5);
}
return coreMd5;
} else {
String fullMd5 = getFullMd5(config);
if (log.isDebugEnabled()) {
log.debug("Protocol v{}: Using full MD5 (all parameters), MD5={}", clientVersion, fullMd5);
}
return fullMd5;
return getVersionedMd5(config, clientVersion, null);
}
/**
* Get versioned MD5 based on client version information.
*
* @param config thread pool parameter
* @param clientProtocolVersion client protocol version
* @param clientVersion explicit semantic client version, optional
* @return versioned MD5 hash
*/
public static String getVersionedMd5(ThreadPoolParameter config, int clientProtocolVersion, String clientVersion) {
String versionedContent = IncrementalContentUtil.getVersionedContent(config, clientProtocolVersion, clientVersion);
String md5 = Md5Util.md5Hex(versionedContent, "UTF-8");
if (log.isDebugEnabled()) {
log.debug("Protocol v{} (clientVersion={}): Using versioned MD5={}",
clientProtocolVersion, clientVersion, md5);
}
return md5;
}
/**
@ -83,8 +89,8 @@ public class IncrementalMd5Util {
if (oldConfig == null || newConfig == null) {
return true;
}
String oldMd5 = getVersionedMd5(oldConfig, clientVersion);
String newMd5 = getVersionedMd5(newConfig, clientVersion);
String oldMd5 = getVersionedMd5(oldConfig, clientVersion, null);
String newMd5 = getVersionedMd5(newConfig, clientVersion, null);
boolean different = !oldMd5.equals(newMd5);
if (different) {
log.debug("Configuration changed - Old MD5: {}, New MD5: {}, Client Version: {}",

@ -0,0 +1,192 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hippo4j.common.toolkit;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Version related utility methods.
*
* <p>This utility centralises how Hippo4j resolves client versions from
* different sources (pom, manifest) and how those versions map to the
* incremental protocol version used for MD5 comparison.</p>
*/
public final class VersionUtil {
public static final String UNKNOWN_VERSION = "0.0.0";
public static final int LEGACY_PROTOCOL_VERSION = 1;
private static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?.*");
private static final NavigableMap<SemanticVersion, Integer> PROTOCOL_VERSION_MAPPINGS = new TreeMap<>();
static {
registerProtocolVersion(UNKNOWN_VERSION, LEGACY_PROTOCOL_VERSION);
registerProtocolVersion("1.0.0", LEGACY_PROTOCOL_VERSION);
registerProtocolVersion("2.0.0", IncrementalContentUtil.PROTOCOL_VERSION);
}
private VersionUtil() {
}
/**
* Resolve the client version string. First non blank candidate will be returned;
* if all candidates are blank the method falls back to the Implementation-Version
* from the provided class' package, and finally to {@code 0.0.0}.
*
* @param explicitVersion explicit version string provided by caller (can be null)
* @param fallbackClass class whose package can provide an Implementation-Version
* @return resolved version string, never {@code null}
*/
public static String resolveClientVersion(String explicitVersion, Class<?> fallbackClass) {
String candidate = firstNonBlank(explicitVersion);
if (StringUtil.isBlank(candidate) && fallbackClass != null) {
Package pkg = fallbackClass.getPackage();
if (pkg != null) {
candidate = pkg.getImplementationVersion();
}
}
if (StringUtil.isBlank(candidate)) {
return UNKNOWN_VERSION;
}
return candidate.trim();
}
/**
* Resolve protocol version from a semantic version string. If the provided version is blank
* or cannot be parsed, {@code defaultVersion} will be returned.
*
* @param version semantic version string
* @param defaultVersion default protocol version fallback
* @return resolved protocol version number
*/
public static int resolveProtocolVersion(String version, int defaultVersion) {
if (StringUtil.isBlank(version)) {
return defaultVersion;
}
SemanticVersion semanticVersion = SemanticVersion.parse(version);
if (semanticVersion == null) {
return defaultVersion;
}
Map.Entry<SemanticVersion, Integer> entry = PROTOCOL_VERSION_MAPPINGS.floorEntry(semanticVersion);
if (entry == null) {
return defaultVersion;
}
Integer mapped = entry.getValue();
return mapped != null ? mapped : defaultVersion;
}
/**
* Resolve protocol version using default value {@link IncrementalContentUtil#PROTOCOL_VERSION}.
*
* @param version semantic version string
* @return resolved protocol version number
*/
public static int resolveProtocolVersion(String version) {
return resolveProtocolVersion(version, IncrementalContentUtil.PROTOCOL_VERSION);
}
private static String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (StringUtil.isNotBlank(value)) {
return value;
}
}
return null;
}
private static void registerProtocolVersion(String version, int protocolVersion) {
SemanticVersion semanticVersion = SemanticVersion.parse(version);
if (semanticVersion != null) {
PROTOCOL_VERSION_MAPPINGS.put(semanticVersion, protocolVersion);
}
}
/**
* Lightweight immutable semantic version implementation (major.minor.patch).
*/
private static final class SemanticVersion implements Comparable<SemanticVersion> {
private final int major;
private final int minor;
private final int patch;
private SemanticVersion(int major, int minor, int patch) {
this.major = major;
this.minor = minor;
this.patch = patch;
}
private static SemanticVersion parse(String version) {
Matcher matcher = VERSION_PATTERN.matcher(version.trim());
if (!matcher.matches()) {
return null;
}
int major = parseOrDefault(matcher.group(1));
int minor = parseOrDefault(matcher.group(2));
int patch = parseOrDefault(matcher.group(3));
return new SemanticVersion(major, minor, patch);
}
private static int parseOrDefault(String value) {
if (StringUtil.isBlank(value)) {
return 0;
}
return Integer.parseInt(value);
}
@Override
public int compareTo(SemanticVersion other) {
if (other == null) {
return 1;
}
if (major != other.major) {
return Integer.compare(major, other.major);
}
if (minor != other.minor) {
return Integer.compare(minor, other.minor);
}
return Integer.compare(patch, other.patch);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SemanticVersion)) {
return false;
}
SemanticVersion that = (SemanticVersion) obj;
return major == that.major && minor == that.minor && patch == that.patch;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch);
}
}
}

@ -37,7 +37,7 @@ import java.util.Set;
public class BeanUtilTest {
@Test
public void beanToBeanConvertTest(){
public void beanToBeanConvertTest() {
final Person person = new Person();
person.setName("Hippo4j");
person.setAge(1);
@ -222,7 +222,7 @@ public class BeanUtilTest {
@Getter
@Setter
static class GoodPerson extends Person{
static class GoodPerson extends Person {
/**
* gender

@ -0,0 +1,422 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hippo4j.common.toolkit;
import cn.hippo4j.common.model.ThreadPoolParameterInfo;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.Assert;
import org.junit.Test;
import java.util.LinkedHashMap;
/**
* Field Version Control Test: Validates that fields introduced in newer versions
* are automatically excluded for older protocol clients to prevent unnecessary refreshes.
*
* This directly addresses the mentor's requirement:
* "If server version 2.0 introduces a new parameter xxx, and the client version is lower than 2.0,
* then this parameter should not be included in the refresh check."
*/
public class FieldVersionControlTest {
/**
* Scenario 1: Server 2.0 introduces a new field, client with protocol v1 should skip it.
* This simulates the mentor's example: server adds field 'xxx' in v2.0, client v1.9 should ignore it.
*/
@Test
public void testNewFieldInV2_ProtocolV1ClientSkips() {
System.out.println("========== Scenario 1: Server 2.0 adds new field, Protocol v1 client skips ==========");
// Server configuration (v2.0) with a hypothetical new field 'executeTimeOut'
// In reality, 'executeTimeOut' is configured with minimum protocol = 3 in current code
ThreadPoolParameterInfo serverConfig = new ThreadPoolParameterInfo();
serverConfig.setTenantId("tenant-001");
serverConfig.setItemId("item-001");
serverConfig.setTpId("test-pool");
serverConfig.setCorePoolSize(10);
serverConfig.setMaximumPoolSize(20);
serverConfig.setQueueType(2);
serverConfig.setCapacity(1024);
serverConfig.setKeepAliveTime(60L);
serverConfig.setRejectedType(1);
serverConfig.setAllowCoreThreadTimeOut(0);
serverConfig.setExecuteTimeOut(5000L); // New field introduced in v2.0 (minimum protocol = 3)
// Protocol v1 client content generation
String v1Content = IncrementalContentUtil.getVersionedContent(serverConfig, 1, "1.9.0");
LinkedHashMap<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
// Protocol v2 client content generation
String v2Content = IncrementalContentUtil.getVersionedContent(serverConfig, 2, "2.0.0");
LinkedHashMap<String, Object> v2Fields = JSONUtil.parseObject(v2Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
System.out.println("Server config has executeTimeOut: " + serverConfig.getExecuteTimeOut());
System.out.println("Protocol v1 content: " + v1Content);
System.out.println("Protocol v2 content: " + v2Content);
System.out.println("Protocol v1 contains executeTimeOut: " + v1Fields.containsKey("executeTimeOut"));
System.out.println("Protocol v2 contains executeTimeOut: " + v2Fields.containsKey("executeTimeOut"));
// Assertions
Assert.assertTrue("Protocol v1 should include all fields (full content)", v1Fields.containsKey("executeTimeOut"));
Assert.assertFalse("Protocol v2 should skip executeTimeOut (min protocol = 3)", v2Fields.containsKey("executeTimeOut"));
System.out.println("Test passed: Protocol v1 client uses full content, v2 client skips future fields");
}
/**
* Scenario 2: Server 2.1 introduces another new field, only protocol v3+ clients should see it.
* This validates the mentor's second example: incremental field rollout across versions.
*/
@Test
public void testNewFieldInV21_RequiresProtocolV3() {
System.out.println("\n========== Scenario 2: Server 2.1 adds field requiring protocol v3 ==========");
// Simulate a field that requires protocol v3 (e.g., a new alarm type)
// In current implementation, extended fields default to PROTOCOL_VERSION + 1 = 3
ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
config.setTenantId("tenant-001");
config.setItemId("item-001");
config.setTpId("test-pool");
config.setCorePoolSize(10);
config.setMaximumPoolSize(20);
config.setQueueType(2);
config.setCapacity(1024);
config.setIsAlarm(1); // Extended field, minimum protocol = 3
String v1Content = IncrementalContentUtil.getVersionedContent(config, 1, "1.9.0");
String v2Content = IncrementalContentUtil.getVersionedContent(config, 2, "2.0.0");
String v3Content = IncrementalContentUtil.getVersionedContent(config, 3, "2.1.0");
LinkedHashMap<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
LinkedHashMap<String, Object> v2Fields = JSONUtil.parseObject(v2Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
LinkedHashMap<String, Object> v3Fields = JSONUtil.parseObject(v3Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
System.out.println("Protocol v1 content: " + v1Content);
System.out.println("Protocol v2 content: " + v2Content);
System.out.println("Protocol v3 content: " + v3Content);
System.out.println("v1 contains isAlarm: " + v1Fields.containsKey("isAlarm"));
System.out.println("v2 contains isAlarm: " + v2Fields.containsKey("isAlarm"));
System.out.println("v3 contains isAlarm: " + v3Fields.containsKey("isAlarm"));
Assert.assertTrue("Protocol v1 uses full content, should contain isAlarm", v1Fields.containsKey("isAlarm"));
Assert.assertFalse("Protocol v2 should skip isAlarm (min protocol = 3)", v2Fields.containsKey("isAlarm"));
Assert.assertTrue("Protocol v3 should include isAlarm", v3Fields.containsKey("isAlarm"));
System.out.println("Test passed: Field visibility controlled by minimum protocol version");
}
/**
* Scenario 3: Verify that MD5 remains stable when only invisible fields change.
* This is the core benefit: preventing unnecessary refreshes.
*/
@Test
public void testMd5StabilityWhenInvisibleFieldChanges() {
System.out.println("\n========== Scenario 3: MD5 stability when invisible field changes ==========");
// Old config without extended field
ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
oldConfig.setTenantId("tenant-001");
oldConfig.setItemId("item-001");
oldConfig.setTpId("test-pool");
oldConfig.setCorePoolSize(10);
oldConfig.setMaximumPoolSize(20);
oldConfig.setQueueType(2);
oldConfig.setCapacity(1024);
// New config with extended field added (invisible to protocol v2)
ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
newConfig.setTenantId("tenant-001");
newConfig.setItemId("item-001");
newConfig.setTpId("test-pool");
newConfig.setCorePoolSize(10);
newConfig.setMaximumPoolSize(20);
newConfig.setQueueType(2);
newConfig.setCapacity(1024);
newConfig.setExecuteTimeOut(5000L); // Added field (min protocol = 3)
String oldV2Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 2, "2.0.0");
String newV2Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 2, "2.0.0");
System.out.println("Old config executeTimeOut: " + oldConfig.getExecuteTimeOut());
System.out.println("New config executeTimeOut: " + newConfig.getExecuteTimeOut());
System.out.println("Protocol v2 old MD5: " + oldV2Md5);
System.out.println("Protocol v2 new MD5: " + newV2Md5);
Assert.assertEquals("MD5 should remain same when only invisible fields change", oldV2Md5, newV2Md5);
System.out.println("Test passed: Protocol v2 client does not refresh when server adds executeTimeOut");
}
/**
* Scenario 4: Core field changes should always trigger refresh regardless of protocol.
* This ensures critical updates are never missed.
*/
@Test
public void testCoreFieldChangesAlwaysTriggerRefresh() {
System.out.println("\n========== Scenario 4: Core field changes trigger refresh for all protocols ==========");
ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
oldConfig.setTenantId("tenant-001");
oldConfig.setItemId("item-001");
oldConfig.setTpId("test-pool");
oldConfig.setCorePoolSize(10);
oldConfig.setMaximumPoolSize(20);
ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
newConfig.setTenantId("tenant-001");
newConfig.setItemId("item-001");
newConfig.setTpId("test-pool");
newConfig.setCorePoolSize(15); // Core field changed
newConfig.setMaximumPoolSize(20);
String oldV1Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 1, "1.9.0");
String newV1Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 1, "1.9.0");
String oldV2Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 2, "2.0.0");
String newV2Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 2, "2.0.0");
System.out.println("Core field changed: corePoolSize 10 -> 15");
System.out.println("Protocol v1: " + (oldV1Md5.equals(newV1Md5) ? "same" : "different"));
System.out.println("Protocol v2: " + (oldV2Md5.equals(newV2Md5) ? "same" : "different"));
Assert.assertNotEquals("Protocol v1 should detect core field change", oldV1Md5, newV1Md5);
Assert.assertNotEquals("Protocol v2 should detect core field change", oldV2Md5, newV2Md5);
System.out.println("Test passed: Core field changes always trigger refresh");
}
/**
* Scenario 5: Simulate exact mentor's requirement - adding field 'xxx' in server 2.0.
* Demonstrates the complete workflow of field version control.
*/
@Test
public void testMentorScenario_ServerV20AddsFieldXxx() {
System.out.println("\n========== Scenario 5: Mentor's exact requirement - Server 2.0 adds 'xxx' ==========");
// Step 1: Simulate registering a new field 'xxx' with minimum protocol 2
// (In real implementation, this would be done in FIELD_MIN_PROTOCOL_VERSION initialization)
// For testing, we use 'isAlarm' as a proxy since it's configured with min protocol = 3
// Client v1.9 (protocol 1) - before 'xxx' was introduced
ThreadPoolParameterInfo clientV19Config = new ThreadPoolParameterInfo();
clientV19Config.setTenantId("tenant-001");
clientV19Config.setItemId("item-001");
clientV19Config.setTpId("test-pool");
clientV19Config.setCorePoolSize(10);
clientV19Config.setMaximumPoolSize(20);
clientV19Config.setQueueType(2);
clientV19Config.setCapacity(1024);
// Server v2.0 - has field 'xxx' (using 'isAlarm' as proxy, min protocol = 3)
ThreadPoolParameterInfo serverV20Config = new ThreadPoolParameterInfo();
serverV20Config.setTenantId("tenant-001");
serverV20Config.setItemId("item-001");
serverV20Config.setTpId("test-pool");
serverV20Config.setCorePoolSize(10);
serverV20Config.setMaximumPoolSize(20);
serverV20Config.setQueueType(2);
serverV20Config.setCapacity(1024);
serverV20Config.setIsAlarm(1); // New field 'xxx' introduced in v2.0 (but min protocol = 3)
// Client v2.0 (protocol 2) - should see 'xxx' if it's marked for protocol 2
// But since isAlarm is marked protocol 3, even v2 clients skip it
ThreadPoolParameterInfo clientV20Config = new ThreadPoolParameterInfo();
clientV20Config.setTenantId("tenant-001");
clientV20Config.setItemId("item-001");
clientV20Config.setTpId("test-pool");
clientV20Config.setCorePoolSize(10);
clientV20Config.setMaximumPoolSize(20);
clientV20Config.setQueueType(2);
clientV20Config.setCapacity(1024);
clientV20Config.setIsAlarm(1);
// Generate MD5 for different protocol versions
String v1ClientMd5 = IncrementalMd5Util.getVersionedMd5(clientV19Config, 1, "1.9.0");
String v2ClientWithoutFieldMd5 = IncrementalMd5Util.getVersionedMd5(clientV19Config, 2, "2.0.0");
String v2ServerWithFieldMd5 = IncrementalMd5Util.getVersionedMd5(serverV20Config, 2, "2.0.0");
System.out.println("Client v1.9 (protocol 1) MD5: " + v1ClientMd5);
System.out.println("Client v2.0 without 'xxx' (protocol 2) MD5: " + v2ClientWithoutFieldMd5);
System.out.println("Server v2.0 with 'xxx' (protocol 2) MD5: " + v2ServerWithFieldMd5);
// Key assertion: Protocol v2 clients should have same MD5 regardless of isAlarm
// because isAlarm requires protocol 3
Assert.assertEquals(
"Server v2.0 adding field 'xxx' (isAlarm) should NOT affect protocol v2 client MD5",
v2ClientWithoutFieldMd5,
v2ServerWithFieldMd5);
System.out.println("Test passed: Field 'xxx' invisible to protocol v2, no refresh triggered");
System.out.println("Mentor's requirement validated: Client < v2.0 does not refresh on new field");
}
/**
* Scenario 6: Server 2.1 introduces field 'yyy', protocol v3 clients see it, v2 clients skip it.
*/
@Test
public void testMentorScenario_ServerV21AddsFieldYyy() {
System.out.println("\n========== Scenario 6: Server 2.1 adds 'yyy', only protocol v3+ sees it ==========");
// Server v2.1 with new field 'yyy' (using 'capacityAlarm' as proxy, min protocol = 3)
ThreadPoolParameterInfo serverV21Config = new ThreadPoolParameterInfo();
serverV21Config.setTenantId("tenant-001");
serverV21Config.setItemId("item-001");
serverV21Config.setTpId("test-pool");
serverV21Config.setCorePoolSize(10);
serverV21Config.setMaximumPoolSize(20);
serverV21Config.setQueueType(2);
serverV21Config.setCapacity(1024);
serverV21Config.setCapacityAlarm(80); // New field 'yyy' introduced in v2.1 (min protocol = 3)
String v1Content = IncrementalContentUtil.getVersionedContent(serverV21Config, 1, "1.9.0");
String v2Content = IncrementalContentUtil.getVersionedContent(serverV21Config, 2, "2.0.0");
String v3Content = IncrementalContentUtil.getVersionedContent(serverV21Config, 3, "2.1.0");
LinkedHashMap<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
LinkedHashMap<String, Object> v2Fields = JSONUtil.parseObject(v2Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
LinkedHashMap<String, Object> v3Fields = JSONUtil.parseObject(v3Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
System.out.println("Server v2.1 has field 'yyy' (capacityAlarm): " + serverV21Config.getCapacityAlarm());
System.out.println("Protocol v1 contains capacityAlarm: " + v1Fields.containsKey("capacityAlarm"));
System.out.println("Protocol v2 contains capacityAlarm: " + v2Fields.containsKey("capacityAlarm"));
System.out.println("Protocol v3 contains capacityAlarm: " + v3Fields.containsKey("capacityAlarm"));
Assert.assertTrue("Protocol v1 uses full content, includes all fields", v1Fields.containsKey("capacityAlarm"));
Assert.assertFalse("Protocol v2 should skip 'yyy' (capacityAlarm)", v2Fields.containsKey("capacityAlarm"));
Assert.assertTrue("Protocol v3 should include 'yyy' (capacityAlarm)", v3Fields.containsKey("capacityAlarm"));
System.out.println("Test passed: Field 'yyy' only visible to protocol v3+");
System.out.println("Mentor's requirement validated: Incremental field rollout works correctly");
}
/**
* Scenario 7: Verify that changing an invisible field does not change MD5 for lower protocol clients.
* This is the key to preventing "invalid refresh" mentioned by the mentor.
*/
@Test
public void testInvisibleFieldChangeDoesNotAffectMd5() {
System.out.println("\n========== Scenario 7: Invisible field change does not affect MD5 ==========");
// Config 1: without extended field
ThreadPoolParameterInfo config1 = new ThreadPoolParameterInfo();
config1.setTenantId("tenant-001");
config1.setItemId("item-001");
config1.setTpId("test-pool");
config1.setCorePoolSize(10);
config1.setMaximumPoolSize(20);
config1.setQueueType(2);
config1.setCapacity(1024);
// Config 2: extended field changed from null to 5000
ThreadPoolParameterInfo config2 = new ThreadPoolParameterInfo();
config2.setTenantId("tenant-001");
config2.setItemId("item-001");
config2.setTpId("test-pool");
config2.setCorePoolSize(10);
config2.setMaximumPoolSize(20);
config2.setQueueType(2);
config2.setCapacity(1024);
config2.setExecuteTimeOut(5000L); // Changed from null to 5000
// Config 3: extended field changed from 5000 to 8000
ThreadPoolParameterInfo config3 = new ThreadPoolParameterInfo();
config3.setTenantId("tenant-001");
config3.setItemId("item-001");
config3.setTpId("test-pool");
config3.setCorePoolSize(10);
config3.setMaximumPoolSize(20);
config3.setQueueType(2);
config3.setCapacity(1024);
config3.setExecuteTimeOut(8000L); // Changed from 5000 to 8000
String md51 = IncrementalMd5Util.getVersionedMd5(config1, 2, "2.0.0");
String md52 = IncrementalMd5Util.getVersionedMd5(config2, 2, "2.0.0");
String md53 = IncrementalMd5Util.getVersionedMd5(config3, 2, "2.0.0");
System.out.println("Config 1 executeTimeOut: null");
System.out.println("Config 2 executeTimeOut: 5000");
System.out.println("Config 3 executeTimeOut: 8000");
System.out.println("Protocol v2 MD5 config1: " + md51);
System.out.println("Protocol v2 MD5 config2: " + md52);
System.out.println("Protocol v2 MD5 config3: " + md53);
Assert.assertEquals("MD5 should be same (null -> 5000)", md51, md52);
Assert.assertEquals("MD5 should be same (5000 -> 8000)", md52, md53);
Assert.assertEquals("MD5 should be same (null -> 8000)", md51, md53);
System.out.println("Test passed: Invisible field changes do not trigger refresh");
System.out.println("This prevents the 'invalid refresh' issue mentioned by the mentor");
}
/**
* Scenario 8: Demonstrate field visibility matrix across protocol versions.
*/
@Test
public void testFieldVisibilityMatrix() {
System.out.println("\n========== Scenario 8: Field visibility matrix ==========");
ThreadPoolParameterInfo fullConfig = new ThreadPoolParameterInfo();
fullConfig.setTenantId("tenant-001");
fullConfig.setItemId("item-001");
fullConfig.setTpId("test-pool");
fullConfig.setCorePoolSize(10);
fullConfig.setMaximumPoolSize(20);
fullConfig.setQueueType(2);
fullConfig.setCapacity(1024);
fullConfig.setKeepAliveTime(60L);
fullConfig.setRejectedType(1);
fullConfig.setAllowCoreThreadTimeOut(0);
fullConfig.setExecuteTimeOut(5000L);
fullConfig.setIsAlarm(1);
fullConfig.setCapacityAlarm(80);
fullConfig.setLivenessAlarm(90);
String v1Content = IncrementalContentUtil.getVersionedContent(fullConfig, 1, "1.9.0");
String v2Content = IncrementalContentUtil.getVersionedContent(fullConfig, 2, "2.0.0");
String v3Content = IncrementalContentUtil.getVersionedContent(fullConfig, 3, "2.1.0");
LinkedHashMap<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
LinkedHashMap<String, Object> v2Fields = JSONUtil.parseObject(v2Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
LinkedHashMap<String, Object> v3Fields = JSONUtil.parseObject(v3Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
System.out.println("\nField Visibility Matrix:");
System.out.println("Field | Protocol v1 | Protocol v2 | Protocol v3");
System.out.println("------------------+-------------+-------------+------------");
System.out.println("tenantId | " + v1Fields.containsKey("tenantId") + " | " + v2Fields.containsKey("tenantId") + " | " + v3Fields.containsKey("tenantId"));
System.out.println("coreSize | " + v1Fields.containsKey("coreSize") + " | " + v2Fields.containsKey("coreSize") + " | " + v3Fields.containsKey("coreSize"));
System.out.println(
"executeTimeOut | " + v1Fields.containsKey("executeTimeOut") + " | " + v2Fields.containsKey("executeTimeOut") + " | " + v3Fields.containsKey("executeTimeOut"));
System.out.println("isAlarm | " + v1Fields.containsKey("isAlarm") + " | " + v2Fields.containsKey("isAlarm") + " | " + v3Fields.containsKey("isAlarm"));
System.out.println("capacityAlarm | " + v1Fields.containsKey("capacityAlarm") + " | " + v2Fields.containsKey("capacityAlarm") + " | " + v3Fields.containsKey("capacityAlarm"));
// Core assertions
Assert.assertTrue("All protocols see core fields", v1Fields.containsKey("coreSize") && v2Fields.containsKey("coreSize") && v3Fields.containsKey("coreSize"));
Assert.assertTrue("Protocol v1 sees all fields (full content)", v1Fields.containsKey("executeTimeOut"));
Assert.assertFalse("Protocol v2 skips extended fields (min protocol = 3)", v2Fields.containsKey("executeTimeOut"));
Assert.assertTrue("Protocol v3 sees extended fields", v3Fields.containsKey("executeTimeOut"));
System.out.println("\nTest passed: Field visibility correctly controlled by protocol version");
System.out.println("This is the foundation for mentor's requirement: version-aware field filtering");
}
}

@ -124,9 +124,16 @@ public class IncrementalMd5UtilBoundaryTest {
System.out.println("\n========== Test 5: getVersionedMd5 with very large version ==========");
ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
config.setTenantId("test");
config.setItemId("test");
config.setTpId("test");
config.setCorePoolSize(10);
config.setMaximumPoolSize(20);
config.setExecuteTimeOut(5000L);
config.setQueueType(1);
config.setCapacity(1024);
config.setKeepAliveTime(60L);
config.setRejectedType(1);
config.setAllowCoreThreadTimeOut(0);
String vLargeMd5 = IncrementalMd5Util.getVersionedMd5(config, 999999);
String v2Md5 = IncrementalMd5Util.getVersionedMd5(config, 2);

@ -40,7 +40,7 @@ public class ProtocolRigidityTest {
clientConfig.setItemId("item-001");
clientConfig.setTpId("test-pool");
clientConfig.setCoreSize(10); // old field
clientConfig.setMaxSize(20); // old field
clientConfig.setMaxSize(20); // old field
clientConfig.setQueueType(2);
clientConfig.setCapacity(1024);
clientConfig.setKeepAliveTime(60L);
@ -52,8 +52,8 @@ public class ProtocolRigidityTest {
serverConfig.setTenantId("default");
serverConfig.setItemId("item-001");
serverConfig.setTpId("test-pool");
serverConfig.setCorePoolSize(10); // new field
serverConfig.setMaximumPoolSize(20); // new field
serverConfig.setCorePoolSize(10); // new field
serverConfig.setMaximumPoolSize(20); // new field
serverConfig.setQueueType(2);
serverConfig.setCapacity(1024);
serverConfig.setKeepAliveTime(60L);
@ -126,9 +126,9 @@ public class ProtocolRigidityTest {
System.out.println("\n========== Scenario 3: Field Adapter Priority ==========");
ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
config.setCoreSize(10); // old field
config.setMaxSize(20); // old field
config.setCorePoolSize(15); // new field (should take priority)
config.setCoreSize(10); // old field
config.setMaxSize(20); // old field
config.setCorePoolSize(15); // new field (should take priority)
config.setMaximumPoolSize(30); // new field (should take priority)
Integer adaptedCore = config.corePoolSizeAdapt();

@ -51,20 +51,26 @@ public class CacheData {
@Getter
private final String threadPoolId;
private final int protocolVersion;
private final String clientVersion;
@Setter
private volatile boolean isInitializing = true;
private final CopyOnWriteArrayList<ManagerListenerWrapper> listeners;
public CacheData(String tenantId, String itemId, String threadPoolId) {
public CacheData(String tenantId, String itemId, String threadPoolId, int protocolVersion, String clientVersion) {
this.tenantId = tenantId;
this.itemId = itemId;
this.threadPoolId = threadPoolId;
this.protocolVersion = protocolVersion;
this.clientVersion = clientVersion;
// Store full content for listeners to receive complete configuration
ThreadPoolParameterInfo parameterInfo = ThreadPoolExecutorRegistry.getHolder(threadPoolId).getParameterInfo();
this.content = ContentUtil.getPoolContent(parameterInfo);
// Calculate MD5 based on incremental content for version compatibility
String incrementalContent = IncrementalContentUtil.getIncrementalContent(parameterInfo, IncrementalContentUtil.PROTOCOL_VERSION);
String incrementalContent = IncrementalContentUtil.getVersionedContent(parameterInfo, protocolVersion, clientVersion);
this.md5 = getMd5String(incrementalContent);
this.listeners = new CopyOnWriteArrayList<>();
}
@ -106,7 +112,7 @@ public class CacheData {
// Calculate MD5 based on incremental content for version compatibility
try {
ThreadPoolParameterInfo parameterInfo = JSONUtil.parseObject(content, ThreadPoolParameterInfo.class);
String incrementalContent = IncrementalContentUtil.getIncrementalContent(parameterInfo, IncrementalContentUtil.PROTOCOL_VERSION);
String incrementalContent = IncrementalContentUtil.getVersionedContent(parameterInfo, protocolVersion, clientVersion);
this.md5 = getMd5String(incrementalContent);
} catch (Exception e) {
// Fallback to full content MD5 if parsing fails

@ -20,11 +20,12 @@ package cn.hippo4j.springboot.starter.core;
import cn.hippo4j.common.executor.ThreadFactoryBuilder;
import cn.hippo4j.common.model.Result;
import cn.hippo4j.common.model.ThreadPoolParameterInfo;
import cn.hippo4j.common.toolkit.IncrementalContentUtil;
import cn.hippo4j.common.toolkit.ContentUtil;
import cn.hippo4j.common.toolkit.GroupKey;
import cn.hippo4j.common.toolkit.IdUtil;
import cn.hippo4j.common.toolkit.IncrementalContentUtil;
import cn.hippo4j.common.toolkit.JSONUtil;
import cn.hippo4j.common.toolkit.VersionUtil;
import cn.hippo4j.springboot.starter.remote.HttpAgent;
import cn.hippo4j.springboot.starter.remote.ServerHealthCheck;
import lombok.SneakyThrows;
@ -70,7 +71,7 @@ public class ClientWorker implements DisposableBean {
private final long timeout;
private final String identify;
private final String version;
private final int protocolVersion;
private final HttpAgent agent;
private final ServerHealthCheck serverHealthCheck;
private final ScheduledExecutorService executorService;
@ -91,7 +92,9 @@ public class ClientWorker implements DisposableBean {
this.agent = httpAgent;
this.identify = identify;
this.timeout = CONFIG_LONG_POLL_TIMEOUT;
this.version = version;
this.version = VersionUtil.resolveClientVersion(version, ClientWorker.class);
int resolvedProtocol = VersionUtil.resolveProtocolVersion(this.version, IncrementalContentUtil.PROTOCOL_VERSION);
this.protocolVersion = VersionUtil.UNKNOWN_VERSION.equals(this.version) ? IncrementalContentUtil.PROTOCOL_VERSION : resolvedProtocol;
this.serverHealthCheck = serverHealthCheck;
this.hippo4jClientShutdown = hippo4jClientShutdown;
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1, runnable -> {
@ -208,7 +211,7 @@ public class ClientWorker implements DisposableBean {
}
headers.put(CLIENT_VERSION, version);
// Add protocol version header for incremental updates
headers.put("X-Hippo4j-Protocol-Version", String.valueOf(IncrementalContentUtil.PROTOCOL_VERSION));
headers.put("X-Hippo4j-Protocol-Version", String.valueOf(protocolVersion));
try {
long readTimeoutMs = timeout + Math.round(timeout >> 1);
Result result = agent.httpPostByConfig(LISTENER_PATH, headers, params, readTimeoutMs);
@ -279,7 +282,7 @@ public class ClientWorker implements DisposableBean {
if (cacheData != null) {
return cacheData;
}
cacheData = new CacheData(namespace, itemId, threadPoolId);
cacheData = new CacheData(namespace, itemId, threadPoolId, protocolVersion, version);
CacheData lastCacheData = cacheMap.putIfAbsent(threadPoolId, cacheData);
if (lastCacheData == null) {
String serverConfig;

@ -17,13 +17,15 @@
package cn.hippo4j.config.model;
import cn.hippo4j.common.constant.Constants;
import cn.hippo4j.common.toolkit.Md5Util;
import cn.hippo4j.config.toolkit.SimpleReadWriteLock;
import cn.hippo4j.config.toolkit.SingletonRepository;
import cn.hippo4j.common.constant.Constants;
import lombok.Getter;
import lombok.Setter;
import java.util.concurrent.ConcurrentHashMap;
/**
* Cache item.
*/
@ -41,6 +43,8 @@ public class CacheItem {
private SimpleReadWriteLock rwLock = new SimpleReadWriteLock();
private final ConcurrentHashMap<Integer, String> versionMd5Cache = new ConcurrentHashMap<>();
public CacheItem(String groupKey) {
this.groupKey = SingletonRepository.DataIdGroupIdCache.getSingleton(groupKey);
}
@ -48,11 +52,36 @@ public class CacheItem {
public CacheItem(String groupKey, String md5) {
this.md5 = md5;
this.groupKey = SingletonRepository.DataIdGroupIdCache.getSingleton(groupKey);
this.versionMd5Cache.put(1, md5);
}
public CacheItem(String groupKey, ConfigAllInfo configAllInfo) {
this.configAllInfo = configAllInfo;
this.md5 = Md5Util.getTpContentMd5(configAllInfo);
this.groupKey = SingletonRepository.DataIdGroupIdCache.getSingleton(groupKey);
this.versionMd5Cache.put(1, this.md5);
}
public String getMd5(int protocolVersion) {
if (protocolVersion <= 0) {
return md5;
}
return versionMd5Cache.get(protocolVersion);
}
public void setMd5(int protocolVersion, String value) {
if (protocolVersion <= 0) {
this.md5 = value;
return;
}
if (value == null) {
versionMd5Cache.remove(protocolVersion);
} else {
versionMd5Cache.put(protocolVersion, value);
}
}
public void clearVersionMd5() {
versionMd5Cache.clear();
}
}

@ -29,6 +29,7 @@ import cn.hippo4j.common.toolkit.MapUtil;
import cn.hippo4j.common.toolkit.Md5Util;
import cn.hippo4j.common.toolkit.IncrementalMd5Util;
import cn.hippo4j.common.toolkit.StringUtil;
import cn.hippo4j.common.toolkit.VersionUtil;
import cn.hippo4j.config.event.LocalDataChangeEvent;
import cn.hippo4j.config.model.CacheItem;
import cn.hippo4j.config.model.ConfigAllInfo;
@ -71,30 +72,11 @@ public class ConfigCacheService {
private static final ConcurrentHashMap<String, Map<String, CacheItem>> CLIENT_CONFIG_CACHE = new ConcurrentHashMap();
public static boolean isUpdateData(String groupKey, String md5, String clientIdentify) {
// Default to version 1 for backward compatibility
return isUpdateData(groupKey, md5, clientIdentify, 1);
return isUpdateData(groupKey, md5, clientIdentify, 1, null);
}
/**
* Check if data needs update with version support
*
* @param groupKey group key
* @param md5 client MD5
* @param clientIdentify client identifier
* @param clientVersion client version
* @return true if data is up to date
*/
public static boolean isUpdateData(String groupKey, String md5, String clientIdentify, int clientVersion) {
String contentMd5 = ConfigCacheService.getContentMd5IsNullPut(groupKey, clientIdentify);
if (clientVersion >= 2) {
String[] params = groupKey.split(GROUP_KEY_DELIMITER_TRANSLATION);
ConfigAllInfo config = configService.findConfigRecentInfo(params);
if (config != null) {
String incrementalMd5 = IncrementalMd5Util.getVersionedMd5(config, clientVersion);
return Objects.equals(incrementalMd5, md5);
}
}
// Fallback to full MD5 comparison for version 1 clients
public static boolean isUpdateData(String groupKey, String md5, String clientIdentify, int clientProtocolVersion, String clientVersion) {
String contentMd5 = ConfigCacheService.getContentMd5IsNullPut(groupKey, clientIdentify, clientProtocolVersion, clientVersion);
return Objects.equals(contentMd5, md5);
}
@ -124,13 +106,13 @@ public class ConfigCacheService {
* @param clientIdentify
* @return
*/
private static synchronized String getContentMd5IsNullPut(String groupKey, String clientIdentify) {
Map<String, CacheItem> cacheItemMap = Optional.ofNullable(CLIENT_CONFIG_CACHE.get(groupKey)).orElse(new HashMap<>());
CacheItem cacheItem = null;
if (CollectionUtil.isNotEmpty(cacheItemMap)) {
cacheItem = cacheItemMap.get(clientIdentify);
if (cacheItem != null) {
return cacheItem.getMd5();
private static synchronized String getContentMd5IsNullPut(String groupKey, String clientIdentify, int clientProtocolVersion, String clientVersion) {
Map<String, CacheItem> cacheItemMap = CLIENT_CONFIG_CACHE.computeIfAbsent(groupKey, key -> new ConcurrentHashMap<>());
CacheItem cacheItem = cacheItemMap.get(clientIdentify);
if (cacheItem != null) {
String versionMd5 = cacheItem.getMd5(clientProtocolVersion);
if (StringUtil.isNotBlank(versionMd5)) {
return versionMd5;
}
}
if (configService == null) {
@ -139,11 +121,20 @@ public class ConfigCacheService {
String[] params = groupKey.split(GROUP_KEY_DELIMITER_TRANSLATION);
ConfigAllInfo config = configService.findConfigRecentInfo(params);
if (config != null && StringUtil.isNotBlank(config.getTpId())) {
cacheItem = new CacheItem(groupKey, config);
cacheItemMap.put(clientIdentify, cacheItem);
CLIENT_CONFIG_CACHE.put(groupKey, cacheItemMap);
if (cacheItem == null) {
cacheItem = new CacheItem(groupKey, config);
cacheItemMap.put(clientIdentify, cacheItem);
} else {
cacheItem.setConfigAllInfo(config);
}
String versionedMd5 = IncrementalMd5Util.getVersionedMd5(config, clientProtocolVersion, clientVersion);
cacheItem.setMd5(clientProtocolVersion, versionedMd5);
if (clientProtocolVersion <= VersionUtil.LEGACY_PROTOCOL_VERSION || StringUtil.isBlank(cacheItem.getMd5())) {
cacheItem.setMd5(versionedMd5);
}
return versionedMd5;
}
return (cacheItem != null) ? cacheItem.getMd5() : Constants.NULL;
return Constants.NULL;
}
public static String getContentMd5(String groupKey) {
@ -162,7 +153,9 @@ public class ConfigCacheService {
public static void updateMd5(String groupKey, String identify, String md5) {
CacheItem cache = makeSure(groupKey, identify);
if (cache.getMd5() == null || !cache.getMd5().equals(md5)) {
cache.clearVersionMd5();
cache.setMd5(md5);
cache.setMd5(VersionUtil.LEGACY_PROTOCOL_VERSION, md5);
String[] params = groupKey.split(GROUP_KEY_DELIMITER_TRANSLATION);
ConfigAllInfo config = configService.findConfigRecentInfo(params);
cache.setConfigAllInfo(config);

@ -17,9 +17,11 @@
package cn.hippo4j.config.toolkit;
import cn.hippo4j.common.constant.Constants;
import cn.hippo4j.common.toolkit.GroupKey;
import cn.hippo4j.common.toolkit.Md5Util;
import cn.hippo4j.common.toolkit.StringUtil;
import cn.hippo4j.common.toolkit.VersionUtil;
import cn.hippo4j.config.service.ConfigCacheService;
import cn.hippo4j.config.model.ConfigAllInfo;
import org.springframework.util.StringUtils;
@ -72,10 +74,11 @@ public class Md5ConfigUtil {
*/
public static List<String> compareMd5(HttpServletRequest request, Map<String, String> clientMd5Map) {
List<String> changedGroupKeys = new ArrayList();
int clientVersion = getClientVersion(request);
String clientVersionHeader = request.getHeader(Constants.CLIENT_VERSION);
int clientProtocolVersion = getClientProtocolVersion(request, clientVersionHeader);
clientMd5Map.forEach((key, val) -> {
String clientIdentify = RequestUtil.getClientIdentify(request);
boolean isUpdateData = ConfigCacheService.isUpdateData(key, val, clientIdentify, clientVersion);
boolean isUpdateData = ConfigCacheService.isUpdateData(key, val, clientIdentify, clientProtocolVersion, clientVersionHeader);
if (!isUpdateData) {
changedGroupKeys.add(key);
}
@ -89,17 +92,16 @@ public class Md5ConfigUtil {
* @param request HTTP request
* @return client protocol version, default to 1 for backward compatibility
*/
private static int getClientVersion(HttpServletRequest request) {
private static int getClientProtocolVersion(HttpServletRequest request, String clientVersionHeader) {
String versionHeader = request.getHeader("X-Hippo4j-Protocol-Version");
if (versionHeader != null && !versionHeader.isEmpty()) {
if (StringUtil.isNotBlank(versionHeader)) {
try {
return Integer.parseInt(versionHeader);
} catch (NumberFormatException e) {
// Default to version 1 for backward compatibility
return Integer.parseInt(versionHeader.trim());
} catch (NumberFormatException ignored) {
return 1;
}
}
return 1;
return VersionUtil.resolveProtocolVersion(clientVersionHeader, 1);
}
public static Map<String, String> getClientMd5Map(String configKeysString) {

@ -17,6 +17,7 @@
package cn.hippo4j.config.toolkit;
import cn.hippo4j.common.constant.Constants;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -192,12 +193,31 @@ public class Md5ConfigUtilVersionTest {
System.out.println("Test passed: compareMd5 will use v2 protocol for this client");
}
/**
* Test: Fallback to semantic client version when protocol header is missing.
*/
@Test
public void testGetClientVersion_FromClientVersionHeader() throws Exception {
System.out.println("\n========== Test 9: Fallback to client version header ==========");
when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn(null);
when(request.getHeader(Constants.CLIENT_VERSION)).thenReturn("2.0.1");
int version = invokeGetClientVersion(request);
System.out.println("Client-Version header: 2.0.1");
System.out.println("Detected protocol version: v" + version);
Assert.assertEquals("Semantic version should map to protocol v2", 2, version);
System.out.println("Test passed: Fallback resolved protocol from client version header");
}
/**
* Helper method to invoke private getClientVersion method via reflection
*/
private int invokeGetClientVersion(HttpServletRequest request) throws Exception {
Method method = Md5ConfigUtil.class.getDeclaredMethod("getClientVersion", HttpServletRequest.class);
Method method = Md5ConfigUtil.class.getDeclaredMethod("getClientProtocolVersion", HttpServletRequest.class, String.class);
method.setAccessible(true);
return (int) method.invoke(null, request);
return (int) method.invoke(null, request, request.getHeader(Constants.CLIENT_VERSION));
}
}

Loading…
Cancel
Save