Remove protocol version mechanism

pull/1611/head
mingri31164 9 months ago
parent d453c37250
commit 44332d8c8a

@ -32,11 +32,6 @@ import java.util.*;
@Slf4j
public class IncrementalContentUtil {
/**
* Version of the incremental protocol
*/
public static final int PROTOCOL_VERSION = 2;
/**
* Core parameters that affect thread pool behavior
*/
@ -89,11 +84,10 @@ public class IncrementalContentUtil {
* versions will be excluded automatically for older clients to avoid unnecessary refresh.
*
* @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 getVersionedContent(ThreadPoolParameter parameter, int protocolVersion, String clientVersion) {
public static String getVersionedContent(ThreadPoolParameter parameter, String clientVersion) {
String fullContent = getFullContent(parameter);
LinkedHashMap<String, Object> raw = JSONUtil.parseObject(fullContent, new TypeReference<LinkedHashMap<String, Object>>() {
});
@ -102,7 +96,7 @@ public class IncrementalContentUtil {
}
String normalizedClientVersion = StringUtil.isNotBlank(clientVersion)
? clientVersion.trim()
: VersionUtil.resolveSemanticVersionForProtocol(protocolVersion);
: VersionUtil.UNKNOWN_VERSION;
Map<String, String> fieldRules = resolveFieldRules(parameter, raw);
LinkedHashMap<String, Object> filtered = new LinkedHashMap<>();
for (String field : IDENTIFIER_FIELDS) {
@ -250,10 +244,10 @@ public class IncrementalContentUtil {
mergeFieldMetadata(fieldRules, extractMetadataFromPayload(raw));
// Assign default version to unconfigured fields (prevents old clients from seeing new fields)
String defaultVisibleVersion = VersionUtil.resolveSemanticVersionForProtocol(PROTOCOL_VERSION);
// Default to "2.0.0" for new fields to maintain backward compatibility
raw.keySet().forEach(field -> {
if (!fieldRules.containsKey(field)) {
fieldRules.put(field, defaultVisibleVersion);
fieldRules.put(field, "2.0.0"); // Default: visible to clients >= 2.0
}
});

@ -49,52 +49,35 @@ public class IncrementalMd5Util {
}
/**
* Get versioned MD5 based on client version
* Get versioned MD5 based on client semantic version.
*
* @param config thread pool parameter
* @param clientVersion client protocol version
* @param config thread pool parameter
* @param clientVersion semantic client version string (can be blank)
* @return versioned MD5 hash
*/
public static String getVersionedMd5(ThreadPoolParameter config, int clientVersion) {
String semanticVersion = VersionUtil.resolveSemanticVersionForProtocol(clientVersion);
return getVersionedMd5(config, clientVersion, semanticVersion);
}
/**
* 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);
public static String getVersionedMd5(ThreadPoolParameter config, String clientVersion) {
String normalizedVersion = StringUtil.isNotBlank(clientVersion)
? clientVersion.trim()
: VersionUtil.UNKNOWN_VERSION;
String versionedContent = IncrementalContentUtil.getVersionedContent(config, normalizedVersion);
String md5 = Md5Util.md5Hex(versionedContent, "UTF-8");
if (log.isDebugEnabled()) {
log.debug("Protocol v{} (clientVersion={}): Using versioned MD5={}",
clientProtocolVersion, clientVersion, md5);
log.debug("ClientVersion={}: Using versioned MD5={}", normalizedVersion, md5);
}
return md5;
}
/**
* Compare MD5 with version support
*
* @param oldConfig old configuration
* @param newConfig new configuration
* @param clientVersion client version
* @return true if configurations are different
* Compare MD5 with version support using semantic version string.
*/
public static boolean isDifferent(ThreadPoolParameter oldConfig, ThreadPoolParameter newConfig, int clientVersion) {
public static boolean isDifferent(ThreadPoolParameter oldConfig, ThreadPoolParameter newConfig, String clientVersion) {
if (oldConfig == null || newConfig == null) {
return true;
}
String semanticVersion = VersionUtil.resolveSemanticVersionForProtocol(clientVersion);
String oldMd5 = getVersionedMd5(oldConfig, clientVersion, semanticVersion);
String newMd5 = getVersionedMd5(newConfig, clientVersion, semanticVersion);
String oldMd5 = getVersionedMd5(oldConfig, clientVersion);
String newMd5 = getVersionedMd5(newConfig, clientVersion);
boolean different = !oldMd5.equals(newMd5);
if (different) {
if (different && log.isDebugEnabled()) {
log.debug("Configuration changed - Old MD5: {}, New MD5: {}, Client Version: {}",
oldMd5, newMd5, clientVersion);
}

@ -35,18 +35,8 @@ 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() {
}
@ -73,69 +63,6 @@ public final class VersionUtil {
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);
}
/**
* Resolve a representative semantic version string for a given protocol version. If the
* protocol version has been registered explicitly, the lowest semantic version mapped to the
* protocol will be returned. Otherwise, a best-effort placeholder in the form of
* {@code <protocol>.0.0} is produced.
*
* @param protocolVersion protocol version number
* @return semantic version string representing the protocol capabilities
*/
public static String resolveSemanticVersionForProtocol(int protocolVersion) {
if (protocolVersion <= 0) {
return UNKNOWN_VERSION;
}
SemanticVersion candidate = null;
for (Map.Entry<SemanticVersion, Integer> entry : PROTOCOL_VERSION_MAPPINGS.entrySet()) {
Integer mapped = entry.getValue();
if (mapped != null && mapped == protocolVersion) {
SemanticVersion semanticVersion = entry.getKey();
if (candidate == null || semanticVersion.compareTo(candidate) < 0) {
candidate = semanticVersion;
}
}
}
if (candidate != null) {
return candidate.toString();
}
return protocolVersion + ".0.0";
}
/**
* Compare two semantic versions using {@link SemanticVersion}. Returns {@code true} if
* {@code version1} is greater than or equal to {@code version2}. Blank or unparsable versions
@ -175,20 +102,6 @@ public final class VersionUtil {
return null;
}
/**
* Register a semantic version to protocol version mapping. This is used during static
* initialization to define which client versions map to which protocol capabilities.
*
* @param version semantic version string (e.g., "2.0.0")
* @param protocolVersion protocol capability number (e.g., 2)
*/
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).
*/

@ -60,17 +60,17 @@ public class FieldVersionControlTest {
serverConfig.setFieldVersionMetadata(Collections.singletonMap("executeTimeOut", "2.1.0"));
// Protocol v1 client content generation
String v1Content = IncrementalContentUtil.getVersionedContent(serverConfig, 1, "1.9.0");
String v1Content = IncrementalContentUtil.getVersionedContent(serverConfig, "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");
String v2Content = IncrementalContentUtil.getVersionedContent(serverConfig, "2.0.0");
LinkedHashMap<String, Object> v2Fields = JSONUtil.parseObject(v2Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
// Protocol v3 client content generation
String v3Content = IncrementalContentUtil.getVersionedContent(serverConfig, 3, "2.1.0");
String v3Content = IncrementalContentUtil.getVersionedContent(serverConfig, "2.1.0");
LinkedHashMap<String, Object> v3Fields = JSONUtil.parseObject(v3Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
@ -109,9 +109,9 @@ public class FieldVersionControlTest {
config.setIsAlarm(1); // Extended field, minimum version = 2.1.0
config.setFieldVersionMetadata(Collections.singletonMap("isAlarm", "2.1.0"));
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");
String v1Content = IncrementalContentUtil.getVersionedContent(config, "1.9.0");
String v2Content = IncrementalContentUtil.getVersionedContent(config, "2.0.0");
String v3Content = IncrementalContentUtil.getVersionedContent(config, "2.1.0");
LinkedHashMap<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
@ -163,8 +163,8 @@ public class FieldVersionControlTest {
newConfig.setExecuteTimeOut(5000L); // Added field (min version = 2.1.0)
newConfig.setFieldVersionMetadata(Collections.singletonMap("executeTimeOut", "2.1.0"));
String oldV2Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 2, "2.0.0");
String newV2Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 2, "2.0.0");
String oldV2Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, "2.0.0");
String newV2Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, "2.0.0");
System.out.println("Old config executeTimeOut: " + oldConfig.getExecuteTimeOut());
System.out.println("New config executeTimeOut: " + newConfig.getExecuteTimeOut());
@ -197,10 +197,10 @@ public class FieldVersionControlTest {
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");
String oldV1Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, "1.9.0");
String newV1Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, "1.9.0");
String oldV2Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, "2.0.0");
String newV2Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, "2.0.0");
System.out.println("Core field changed: corePoolSize 10 -> 15");
System.out.println("Protocol v1: " + (oldV1Md5.equals(newV1Md5) ? "same" : "different"));
@ -258,9 +258,9 @@ public class FieldVersionControlTest {
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");
String v1ClientMd5 = IncrementalMd5Util.getVersionedMd5(clientV19Config, "1.9.0");
String v2ClientWithoutFieldMd5 = IncrementalMd5Util.getVersionedMd5(clientV19Config, "2.0.0");
String v2ServerWithFieldMd5 = IncrementalMd5Util.getVersionedMd5(serverV20Config, "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);
@ -296,9 +296,9 @@ public class FieldVersionControlTest {
serverV21Config.setCapacityAlarm(80); // New field 'yyy' introduced in v2.1 (min version = 2.1.0)
serverV21Config.setFieldVersionMetadata(Collections.singletonMap("capacityAlarm", "2.1.0"));
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");
String v1Content = IncrementalContentUtil.getVersionedContent(serverV21Config, "1.9.0");
String v2Content = IncrementalContentUtil.getVersionedContent(serverV21Config, "2.0.0");
String v3Content = IncrementalContentUtil.getVersionedContent(serverV21Config, "2.1.0");
LinkedHashMap<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
});
@ -362,9 +362,9 @@ public class FieldVersionControlTest {
config3.setExecuteTimeOut(8000L); // Changed from 5000 to 8000
config3.setFieldVersionMetadata(Collections.singletonMap("executeTimeOut", "2.1.0"));
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");
String md51 = IncrementalMd5Util.getVersionedMd5(config1, "2.0.0");
String md52 = IncrementalMd5Util.getVersionedMd5(config2, "2.0.0");
String md53 = IncrementalMd5Util.getVersionedMd5(config3, "2.0.0");
System.out.println("Config 1 executeTimeOut: null");
System.out.println("Config 2 executeTimeOut: 5000");
@ -410,9 +410,9 @@ public class FieldVersionControlTest {
metadata.put("livenessAlarm", "2.1.0");
fullConfig.setFieldVersionMetadata(metadata);
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");
String v1Content = IncrementalContentUtil.getVersionedContent(fullConfig, "1.9.0");
String v2Content = IncrementalContentUtil.getVersionedContent(fullConfig, "2.0.0");
String v3Content = IncrementalContentUtil.getVersionedContent(fullConfig, "2.1.0");
LinkedHashMap<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
});

@ -1,296 +0,0 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
import java.util.Collections;
import java.util.LinkedHashMap;
/**
* Incremental MD5 Utility Boundary Test
* Tests edge cases and boundary conditions for version-aware MD5 calculation
*/
public class IncrementalMd5UtilBoundaryTest {
/**
* Test: null config parameter
* Expected: Graceful handling without NPE
*/
@Test
public void testGetCoreMd5_NullConfig() {
System.out.println("========== Test 1: getCoreMd5 with null config ==========");
try {
String md5 = IncrementalMd5Util.getCoreMd5(null);
System.out.println("Result MD5: " + md5);
System.out.println("Test passed: Handled null config gracefully");
} catch (Exception e) {
System.out.println("Exception caught: " + e.getClass().getSimpleName());
System.out.println("Test passed: NPE expected for null config");
// NPE is acceptable for null input
}
}
/**
* Test: config with all null fields
* Expected: Generates MD5 without error
*/
@Test
public void testGetCoreMd5_AllNullFields() {
System.out.println("\n========== Test 2: getCoreMd5 with all null fields ==========");
ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
// All fields are null
String md5 = IncrementalMd5Util.getCoreMd5(config);
System.out.println("Config: all fields null");
System.out.println("Generated MD5: " + md5);
Assert.assertNotNull("MD5 should not be null", md5);
Assert.assertFalse("MD5 should not be empty", md5.isEmpty());
System.out.println("Test passed: Generated MD5 for null fields");
}
/**
* Test: version number is 0
* Expected: Treats as v1 (any version < 2 is v1)
*/
@Test
public void testGetVersionedMd5_VersionZero() {
System.out.println("\n========== Test 3: getVersionedMd5 with version = 0 ==========");
ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
config.setCorePoolSize(10);
config.setMaximumPoolSize(20);
config.setExecuteTimeOut(5000L);
config.setFieldVersionMetadata(Collections.singletonMap("executeTimeOut", "2.1.0"));
String v0Md5 = IncrementalMd5Util.getVersionedMd5(config, 0);
String v1Md5 = IncrementalMd5Util.getVersionedMd5(config, 1);
System.out.println("Version 0 MD5: " + v0Md5);
System.out.println("Version 1 MD5: " + v1Md5);
System.out.println("Are they equal? " + v0Md5.equals(v1Md5));
Assert.assertEquals("Version 0 should behave like v1", v1Md5, v0Md5);
System.out.println("Test passed: Version 0 uses v1 behavior");
}
/**
* Test: version number is negative
* Expected: Treats as v1 (any version < 2 is v1)
*/
@Test
public void testGetVersionedMd5_NegativeVersion() {
System.out.println("\n========== Test 4: getVersionedMd5 with negative version ==========");
ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
config.setCorePoolSize(10);
config.setMaximumPoolSize(20);
config.setExecuteTimeOut(5000L);
config.setFieldVersionMetadata(Collections.singletonMap("executeTimeOut", "2.1.0"));
String vNegativeMd5 = IncrementalMd5Util.getVersionedMd5(config, -1);
String v1Md5 = IncrementalMd5Util.getVersionedMd5(config, 1);
System.out.println("Version -1 MD5: " + vNegativeMd5);
System.out.println("Version 1 MD5: " + v1Md5);
System.out.println("Are they equal? " + vNegativeMd5.equals(v1Md5));
Assert.assertEquals("Negative version should behave like v1", v1Md5, vNegativeMd5);
System.out.println("Test passed: Negative version uses v1 behavior");
}
/**
* Test: very large version number
* Expected: Treats as v2+ (uses incremental MD5)
*/
@Test
public void testGetVersionedMd5_LargeVersion() {
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.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);
System.out.println("Version 999999 MD5: " + vLargeMd5);
System.out.println("Version 2 MD5: " + v2Md5);
System.out.println("Are they equal? " + vLargeMd5.equals(v2Md5));
Assert.assertEquals("Large version should behave like v2", v2Md5, vLargeMd5);
System.out.println("Test passed: Large version uses v2 behavior");
}
/**
* Test: config with only extended parameters
* Expected: v2 generates MD5 for core params (even if empty)
*/
@Test
public void testGetVersionedMd5_OnlyExtendedParams() {
System.out.println("\n========== Test 6: Config with only extended parameters ==========");
ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
// Only extended parameters
config.setExecuteTimeOut(5000L);
config.setIsAlarm(1);
config.setCapacityAlarm(80);
LinkedHashMap<String, String> metadata = new LinkedHashMap<>();
metadata.put("executeTimeOut", "2.1.0");
metadata.put("isAlarm", "2.1.0");
metadata.put("capacityAlarm", "2.1.0");
config.setFieldVersionMetadata(metadata);
String v1Md5 = IncrementalMd5Util.getVersionedMd5(config, 1);
String v2Md5 = IncrementalMd5Util.getVersionedMd5(config, 2);
String v3Md5 = IncrementalMd5Util.getVersionedMd5(config, 3);
System.out.println("Config: Only extended params (executeTimeOut, isAlarm, capacityAlarm)");
System.out.println("v1 MD5: " + v1Md5);
System.out.println("v2 MD5: " + v2Md5);
System.out.println("Are v1 and v2 same? " + v1Md5.equals(v2Md5));
System.out.println("Does v3 differ? " + !v2Md5.equals(v3Md5));
Assert.assertEquals("Protocols below threshold skip extended params", v1Md5, v2Md5);
Assert.assertNotEquals("Supported protocol should include extended params", v2Md5, v3Md5);
System.out.println("Test passed: Metadata gates extended params by protocol");
}
/**
* Test: isDifferent with same MD5
* Expected: Returns false (no difference)
*/
@Test
public void testIsDifferent_SameMd5() {
System.out.println("\n========== Test 7: isDifferent with same MD5 ==========");
ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
oldConfig.setCorePoolSize(10);
oldConfig.setMaximumPoolSize(20);
ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
newConfig.setCorePoolSize(10);
newConfig.setMaximumPoolSize(20);
boolean isDifferent = IncrementalMd5Util.isDifferent(oldConfig, newConfig, 2);
System.out.println("Old and new configs are identical");
System.out.println("isDifferent result: " + isDifferent);
Assert.assertFalse("Should return false for identical configs", isDifferent);
System.out.println("Test passed: Identical configs correctly identified");
}
/**
* Test: isDifferent with different MD5
* Expected: Returns true (difference detected)
*/
@Test
public void testIsDifferent_DifferentMd5() {
System.out.println("\n========== Test 8: isDifferent with different MD5 ==========");
ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
oldConfig.setCorePoolSize(10);
oldConfig.setMaximumPoolSize(20);
ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
newConfig.setCorePoolSize(15); // Different!
newConfig.setMaximumPoolSize(20);
boolean isDifferent = IncrementalMd5Util.isDifferent(oldConfig, newConfig, 2);
System.out.println("Old corePoolSize: 10, New corePoolSize: 15");
System.out.println("isDifferent result: " + isDifferent);
Assert.assertTrue("Should return true for different configs", isDifferent);
System.out.println("Test passed: Different configs correctly identified");
}
/**
* Test: Empty string vs null for string fields
* Expected: Both generate valid MD5
*/
@Test
public void testGetCoreMd5_EmptyVsNullString() {
System.out.println("\n========== Test 9: Empty string vs null for string fields ==========");
ThreadPoolParameterInfo configWithNull = new ThreadPoolParameterInfo();
configWithNull.setTenantId(null);
configWithNull.setItemId(null);
configWithNull.setTpId(null);
ThreadPoolParameterInfo configWithEmpty = new ThreadPoolParameterInfo();
configWithEmpty.setTenantId("");
configWithEmpty.setItemId("");
configWithEmpty.setTpId("");
String nullMd5 = IncrementalMd5Util.getCoreMd5(configWithNull);
String emptyMd5 = IncrementalMd5Util.getCoreMd5(configWithEmpty);
System.out.println("Null fields MD5: " + nullMd5);
System.out.println("Empty fields MD5: " + emptyMd5);
System.out.println("Are they different? " + !nullMd5.equals(emptyMd5));
Assert.assertNotNull("Null fields MD5 should not be null", nullMd5);
Assert.assertNotNull("Empty fields MD5 should not be null", emptyMd5);
System.out.println("Test passed: Both null and empty strings handled");
}
/**
* Test: Consistency - multiple calls should return same MD5
* Expected: Same MD5 for same config
*/
@Test
public void testGetCoreMd5_Consistency() {
System.out.println("\n========== Test 10: MD5 calculation consistency ==========");
ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
config.setTenantId("test");
config.setItemId("test");
config.setTpId("test");
config.setCorePoolSize(10);
config.setMaximumPoolSize(20);
String md51 = IncrementalMd5Util.getCoreMd5(config);
String md52 = IncrementalMd5Util.getCoreMd5(config);
String md53 = IncrementalMd5Util.getCoreMd5(config);
System.out.println("MD5 call 1: " + md51);
System.out.println("MD5 call 2: " + md52);
System.out.println("MD5 call 3: " + md53);
System.out.println("All equal? " + (md51.equals(md52) && md52.equals(md53)));
Assert.assertEquals("Multiple calls should return same MD5", md51, md52);
Assert.assertEquals("Multiple calls should return same MD5", md52, md53);
System.out.println("Test passed: MD5 calculation is consistent");
}
}

@ -51,8 +51,6 @@ public class CacheData {
@Getter
private final String threadPoolId;
private final int protocolVersion;
private final String clientVersion;
@Setter
@ -60,17 +58,16 @@ public class CacheData {
private final CopyOnWriteArrayList<ManagerListenerWrapper> listeners;
public CacheData(String tenantId, String itemId, String threadPoolId, int protocolVersion, String clientVersion) {
public CacheData(String tenantId, String itemId, String threadPoolId, 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.getVersionedContent(parameterInfo, protocolVersion, clientVersion);
String incrementalContent = IncrementalContentUtil.getVersionedContent(parameterInfo, clientVersion);
this.md5 = getMd5String(incrementalContent);
this.listeners = new CopyOnWriteArrayList<>();
}
@ -112,7 +109,7 @@ public class CacheData {
// Calculate MD5 based on incremental content for version compatibility
try {
ThreadPoolParameterInfo parameterInfo = JSONUtil.parseObject(content, ThreadPoolParameterInfo.class);
String incrementalContent = IncrementalContentUtil.getVersionedContent(parameterInfo, protocolVersion, clientVersion);
String incrementalContent = IncrementalContentUtil.getVersionedContent(parameterInfo, clientVersion);
this.md5 = getMd5String(incrementalContent);
} catch (Exception e) {
// Fallback to full content MD5 if parsing fails

@ -23,7 +23,6 @@ import cn.hippo4j.common.model.ThreadPoolParameterInfo;
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;
@ -71,7 +70,6 @@ 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;
@ -93,8 +91,6 @@ public class ClientWorker implements DisposableBean {
this.identify = identify;
this.timeout = CONFIG_LONG_POLL_TIMEOUT;
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 -> {
@ -210,8 +206,6 @@ public class ClientWorker implements DisposableBean {
headers.put(LONG_PULLING_TIMEOUT_NO_HANGUP, "true");
}
headers.put(CLIENT_VERSION, version);
// Add protocol version header for incremental updates
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);
@ -282,7 +276,7 @@ public class ClientWorker implements DisposableBean {
if (cacheData != null) {
return cacheData;
}
cacheData = new CacheData(namespace, itemId, threadPoolId, protocolVersion, version);
cacheData = new CacheData(namespace, itemId, threadPoolId, version);
CacheData lastCacheData = cacheMap.putIfAbsent(threadPoolId, cacheData);
if (lastCacheData == null) {
String serverConfig;

@ -21,6 +21,8 @@ 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.toolkit.StringUtil;
import cn.hippo4j.common.toolkit.VersionUtil;
import lombok.Getter;
import lombok.Setter;
@ -43,7 +45,7 @@ public class CacheItem {
private SimpleReadWriteLock rwLock = new SimpleReadWriteLock();
private final ConcurrentHashMap<Integer, String> versionMd5Cache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, String> versionMd5Cache = new ConcurrentHashMap<>();
public CacheItem(String groupKey) {
this.groupKey = SingletonRepository.DataIdGroupIdCache.getSingleton(groupKey);
@ -52,36 +54,38 @@ public class CacheItem {
public CacheItem(String groupKey, String md5) {
this.md5 = md5;
this.groupKey = SingletonRepository.DataIdGroupIdCache.getSingleton(groupKey);
this.versionMd5Cache.put(1, md5);
this.versionMd5Cache.put(VersionUtil.UNKNOWN_VERSION, 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);
this.versionMd5Cache.put(VersionUtil.UNKNOWN_VERSION, this.md5);
}
public String getMd5(int protocolVersion) {
if (protocolVersion <= 0) {
return md5;
}
return versionMd5Cache.get(protocolVersion);
public String getMd5(String clientVersion) {
String key = normalizeVersionKey(clientVersion);
return versionMd5Cache.getOrDefault(key, md5);
}
public void setMd5(int protocolVersion, String value) {
if (protocolVersion <= 0) {
this.md5 = value;
return;
}
public void setMd5(String clientVersion, String value) {
String key = normalizeVersionKey(clientVersion);
if (value == null) {
versionMd5Cache.remove(protocolVersion);
versionMd5Cache.remove(key);
} else {
versionMd5Cache.put(protocolVersion, value);
versionMd5Cache.put(key, value);
}
}
public void clearVersionMd5() {
versionMd5Cache.clear();
}
private String normalizeVersionKey(String clientVersion) {
if (StringUtil.isBlank(clientVersion)) {
return VersionUtil.UNKNOWN_VERSION;
}
return clientVersion.trim();
}
}

@ -72,11 +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) {
return isUpdateData(groupKey, md5, clientIdentify, 1, null);
return isUpdateData(groupKey, md5, clientIdentify, VersionUtil.UNKNOWN_VERSION);
}
public static boolean isUpdateData(String groupKey, String md5, String clientIdentify, int clientProtocolVersion, String clientVersion) {
String contentMd5 = ConfigCacheService.getContentMd5IsNullPut(groupKey, clientIdentify, clientProtocolVersion, clientVersion);
public static boolean isUpdateData(String groupKey, String md5, String clientIdentify, String clientVersion) {
String contentMd5 = ConfigCacheService.getContentMd5IsNullPut(groupKey, clientIdentify, clientVersion);
return Objects.equals(contentMd5, md5);
}
@ -106,11 +106,11 @@ public class ConfigCacheService {
* @param clientIdentify
* @return
*/
private static synchronized String getContentMd5IsNullPut(String groupKey, String clientIdentify, int clientProtocolVersion, String clientVersion) {
private static synchronized String getContentMd5IsNullPut(String groupKey, String clientIdentify, 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);
String versionMd5 = cacheItem.getMd5(clientVersion);
if (StringUtil.isNotBlank(versionMd5)) {
return versionMd5;
}
@ -127,11 +127,8 @@ public class ConfigCacheService {
} 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);
}
String versionedMd5 = IncrementalMd5Util.getVersionedMd5(config, clientVersion);
cacheItem.setMd5(clientVersion, versionedMd5);
return versionedMd5;
}
return Constants.NULL;
@ -154,8 +151,7 @@ public class ConfigCacheService {
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);
cache.setMd5(VersionUtil.UNKNOWN_VERSION, md5);
String[] params = groupKey.split(GROUP_KEY_DELIMITER_TRANSLATION);
ConfigAllInfo config = configService.findConfigRecentInfo(params);
cache.setConfigAllInfo(config);

@ -75,10 +75,14 @@ public class Md5ConfigUtil {
public static List<String> compareMd5(HttpServletRequest request, Map<String, String> clientMd5Map) {
List<String> changedGroupKeys = new ArrayList();
String clientVersionHeader = request.getHeader(Constants.CLIENT_VERSION);
int clientProtocolVersion = getClientProtocolVersion(request, clientVersionHeader);
String normalizedClientVersion = VersionUtil.resolveClientVersion(clientVersionHeader, null);
if (StringUtil.isBlank(normalizedClientVersion)) {
normalizedClientVersion = VersionUtil.UNKNOWN_VERSION;
}
final String effectiveClientVersion = normalizedClientVersion; // Make it effectively final
clientMd5Map.forEach((key, val) -> {
String clientIdentify = RequestUtil.getClientIdentify(request);
boolean isUpdateData = ConfigCacheService.isUpdateData(key, val, clientIdentify, clientProtocolVersion, clientVersionHeader);
boolean isUpdateData = ConfigCacheService.isUpdateData(key, val, clientIdentify, effectiveClientVersion);
if (!isUpdateData) {
changedGroupKeys.add(key);
}
@ -86,24 +90,6 @@ public class Md5ConfigUtil {
return changedGroupKeys;
}
/**
* Get client protocol version from request header
*
* @param request HTTP request
* @return client protocol version, default to 1 for backward compatibility
*/
private static int getClientProtocolVersion(HttpServletRequest request, String clientVersionHeader) {
String versionHeader = request.getHeader("X-Hippo4j-Protocol-Version");
if (StringUtil.isNotBlank(versionHeader)) {
try {
return Integer.parseInt(versionHeader.trim());
} catch (NumberFormatException ignored) {
return 1;
}
}
return VersionUtil.resolveProtocolVersion(clientVersionHeader, 1);
}
public static Map<String, String> getClientMd5Map(String configKeysString) {
Map<String, String> md5Map = new HashMap(CLIENT_MD5_MAP_INIT_SIZE);
if (null == configKeysString || "".equals(configKeysString)) {

@ -1,198 +0,0 @@
/*
* 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.config.service;
import cn.hippo4j.common.model.ThreadPoolParameterInfo;
import cn.hippo4j.common.toolkit.IncrementalMd5Util;
import org.junit.Assert;
import org.junit.Test;
import java.util.LinkedHashMap;
/**
* ConfigCacheService Version-Aware Test
* Tests the version-aware MD5 comparison logic for cross-version compatibility
*
* Note: Tests focus on the MD5 calculation logic used by ConfigCacheService
* Integration tests for ConfigCacheService.isUpdateData() are covered separately
* due to dependencies on configuration cache infrastructure
*/
public class ConfigCacheServiceVersionTest {
/**
* Test: Version-aware MD5 comparison logic
* Verifies that v1 and v2 produce different MD5 for same config with extended params
*/
@Test
public void testVersionedMd5_Difference() {
System.out.println("========== Test 1: v1 vs v2 MD5 difference ==========");
ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
config.setTenantId("test-tenant");
config.setItemId("test-item");
config.setTpId("test-pool");
config.setCorePoolSize(10);
config.setMaximumPoolSize(20);
config.setQueueType(2);
config.setCapacity(1024);
config.setKeepAliveTime(60L);
config.setRejectedType(1);
config.setAllowCoreThreadTimeOut(0);
config.setExecuteTimeOut(5000L); // Extended parameter
config.setIsAlarm(1); // Extended parameter
LinkedHashMap<String, String> metadata = new LinkedHashMap<>();
metadata.put("executeTimeOut", "2.1.0");
metadata.put("isAlarm", "2.1.0");
config.setFieldVersionMetadata(metadata);
String v1Md5 = IncrementalMd5Util.getVersionedMd5(config, 1);
String v2Md5 = IncrementalMd5Util.getVersionedMd5(config, 2);
String v3Md5 = IncrementalMd5Util.getVersionedMd5(config, 3);
System.out.println("Config includes extended parameters: executeTimeOut, isAlarm");
System.out.println("v1 MD5 (full): " + v1Md5);
System.out.println("v2 MD5 (incremental): " + v2Md5);
System.out.println("v3 MD5 (supports extended): " + v3Md5);
System.out.println("v1 equals v2? " + v1Md5.equals(v2Md5));
System.out.println("v3 equals v2? " + v3Md5.equals(v2Md5));
Assert.assertEquals("Protocols below field threshold should share same MD5", v1Md5, v2Md5);
Assert.assertNotEquals("Protocol v3 should include extended fields", v2Md5, v3Md5);
System.out.println("Test passed: Older protocols share MD5 while supported protocol diverges");
}
/**
* Test: v2 client should not refresh when only extended params change
* This is the core extensibility improvement
*/
@Test
public void testV2Client_ExtendedParamChange_NoRefresh() {
System.out.println("\n========== Test 2: v2 client - extended param change ==========");
// Old config
ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
oldConfig.setCorePoolSize(10);
oldConfig.setMaximumPoolSize(20);
oldConfig.setQueueType(2);
oldConfig.setCapacity(1024);
oldConfig.setKeepAliveTime(60L);
oldConfig.setRejectedType(1);
oldConfig.setAllowCoreThreadTimeOut(0);
oldConfig.setExecuteTimeOut(3000L); // Extended
LinkedHashMap<String, String> metadata = new LinkedHashMap<>();
metadata.put("executeTimeOut", "2.1.0");
oldConfig.setFieldVersionMetadata(metadata);
// New config (only extended param changed)
ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
newConfig.setCorePoolSize(10);
newConfig.setMaximumPoolSize(20);
newConfig.setQueueType(2);
newConfig.setCapacity(1024);
newConfig.setKeepAliveTime(60L);
newConfig.setRejectedType(1);
newConfig.setAllowCoreThreadTimeOut(0);
newConfig.setExecuteTimeOut(5000L); // Extended param changed!
newConfig.setFieldVersionMetadata(metadata);
String oldV2Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 2);
String newV2Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 2);
System.out.println("Extended param change: executeTimeOut 3000 -> 5000");
System.out.println("Old v2 MD5: " + oldV2Md5);
System.out.println("New v2 MD5: " + newV2Md5);
System.out.println("Are they same? " + oldV2Md5.equals(newV2Md5));
Assert.assertEquals("v2 MD5 should remain same (no core param change)", oldV2Md5, newV2Md5);
System.out.println("Test passed: v2 client won't refresh for extended param change");
}
/**
* Test: v1 client SHOULD refresh when extended params change
* This verifies backward compatibility
*/
@Test
public void testV1Client_ExtendedParamChange_NoRefresh() {
System.out.println("\n========== Test 3: v1 client - extended param change ==========");
// Old config
ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
oldConfig.setCorePoolSize(10);
oldConfig.setMaximumPoolSize(20);
oldConfig.setQueueType(2);
oldConfig.setCapacity(1024);
oldConfig.setExecuteTimeOut(3000L);
LinkedHashMap<String, String> metadata = new LinkedHashMap<>();
metadata.put("executeTimeOut", "2.1.0");
oldConfig.setFieldVersionMetadata(metadata);
// New config (only extended param changed)
ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
newConfig.setCorePoolSize(10);
newConfig.setMaximumPoolSize(20);
newConfig.setQueueType(2);
newConfig.setCapacity(1024);
newConfig.setExecuteTimeOut(5000L); // Extended param changed!
newConfig.setFieldVersionMetadata(metadata);
String oldV1Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 1);
String newV1Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 1);
System.out.println("Extended param change: executeTimeOut 3000 -> 5000");
System.out.println("Old v1 MD5: " + oldV1Md5);
System.out.println("New v1 MD5: " + newV1Md5);
System.out.println("Are they same? " + oldV1Md5.equals(newV1Md5));
Assert.assertEquals("v1 should now leverage metadata to skip unsupported fields", oldV1Md5, newV1Md5);
System.out.println("Test passed: v1 client no longer refreshes for unsupported fields");
}
/**
* Test: Both v1 and v2 should refresh when core params change
* This ensures core functionality still works for both versions
*/
@Test
public void testBothVersions_CoreParamChange_ShouldRefresh() {
System.out.println("\n========== Test 4: Both versions - core param change ==========");
// Old config
ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
oldConfig.setCorePoolSize(10);
oldConfig.setMaximumPoolSize(20);
oldConfig.setQueueType(2);
// New config (core param changed)
ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
newConfig.setCorePoolSize(15); // Core param changed!
newConfig.setMaximumPoolSize(20);
newConfig.setQueueType(2);
String oldV1Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 1);
String newV1Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 1);
String oldV2Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 2);
String newV2Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 2);
System.out.println("Core param change: corePoolSize 10 -> 15");
System.out.println("v1: " + oldV1Md5 + " -> " + newV1Md5 + " (different? " + !oldV1Md5.equals(newV1Md5) + ")");
System.out.println("v2: " + oldV2Md5 + " -> " + newV2Md5 + " (different? " + !oldV2Md5.equals(newV2Md5) + ")");
Assert.assertNotEquals("v1 should detect core param change", oldV1Md5, newV1Md5);
Assert.assertNotEquals("v2 should detect core param change", oldV2Md5, newV2Md5);
System.out.println("Test passed: Both versions detect core param changes");
}
}

@ -1,223 +0,0 @@
/*
* 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.config.toolkit;
import cn.hippo4j.common.constant.Constants;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import static org.mockito.Mockito.when;
/**
* Md5ConfigUtil Version Detection Test
* Tests the client protocol version detection logic for cross-version compatibility
*/
@RunWith(MockitoJUnitRunner.class)
public class Md5ConfigUtilVersionTest {
@Mock
private HttpServletRequest request;
private static final String PROTOCOL_VERSION_HEADER = "X-Hippo4j-Protocol-Version";
/**
* Test: Client sends v2 protocol version header
* Expected: Server correctly identifies client as v2
*/
@Test
public void testGetClientVersion_WithV2Header() throws Exception {
System.out.println("========== Test 1: Client sends v2 version header ==========");
when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("2");
int version = invokeGetClientVersion(request);
System.out.println("Request header: X-Hippo4j-Protocol-Version = 2");
System.out.println("Detected client version: " + version);
Assert.assertEquals("Should detect v2 client", 2, version);
System.out.println("Test passed: Server correctly identifies v2 client");
}
/**
* Test: Old client doesn't send version header
* Expected: Server defaults to v1 for backward compatibility
*/
@Test
public void testGetClientVersion_WithoutHeader() throws Exception {
System.out.println("\n========== Test 2: Old client without version header ==========");
when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn(null);
int version = invokeGetClientVersion(request);
System.out.println("Request header: X-Hippo4j-Protocol-Version = null");
System.out.println("Detected client version: " + version);
Assert.assertEquals("Should default to v1", 1, version);
System.out.println("Test passed: Server defaults to v1 for backward compatibility");
}
/**
* Test: Version header is empty string
* Expected: Server defaults to v1
*/
@Test
public void testGetClientVersion_WithEmptyHeader() throws Exception {
System.out.println("\n========== Test 3: Version header is empty string ==========");
when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("");
int version = invokeGetClientVersion(request);
System.out.println("Request header: X-Hippo4j-Protocol-Version = \"\"");
System.out.println("Detected client version: " + version);
Assert.assertEquals("Should default to v1 for empty header", 1, version);
System.out.println("Test passed: Empty header defaults to v1");
}
/**
* Test: Version header has invalid format (non-numeric)
* Expected: Server gracefully handles error and defaults to v1
*/
@Test
public void testGetClientVersion_WithInvalidFormat() throws Exception {
System.out.println("\n========== Test 4: Version header has invalid format ==========");
when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("invalid");
int version = invokeGetClientVersion(request);
System.out.println("Request header: X-Hippo4j-Protocol-Version = \"invalid\"");
System.out.println("Detected client version: " + version);
Assert.assertEquals("Should default to v1 for invalid format", 1, version);
System.out.println("Test passed: Invalid format gracefully handled");
}
/**
* Test: Version header is v1 explicitly
* Expected: Server correctly identifies as v1
*/
@Test
public void testGetClientVersion_WithV1Header() throws Exception {
System.out.println("\n========== Test 5: Client explicitly sends v1 ==========");
when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("1");
int version = invokeGetClientVersion(request);
System.out.println("Request header: X-Hippo4j-Protocol-Version = 1");
System.out.println("Detected client version: " + version);
Assert.assertEquals("Should detect v1 client", 1, version);
System.out.println("Test passed: Server correctly identifies v1 client");
}
/**
* Test: Version header is future version (v3)
* Expected: Server correctly parses and returns the version
*/
@Test
public void testGetClientVersion_WithFutureVersion() throws Exception {
System.out.println("\n========== Test 6: Client sends future version (v3) ==========");
when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("3");
int version = invokeGetClientVersion(request);
System.out.println("Request header: X-Hippo4j-Protocol-Version = 3");
System.out.println("Detected client version: " + version);
Assert.assertEquals("Should parse future version", 3, version);
System.out.println("Test passed: Future version handled correctly");
}
/**
* Test: compareMd5 method can be called with v1 client
* Note: This is a lightweight test focusing on version detection
* Full integration tests are covered separately
*/
@Test
public void testCompareMd5_VersionDetection_V1Client() throws Exception {
System.out.println("\n========== Test 7: Version detection in compareMd5 (v1) ==========");
when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn(null);
// Verify version detection works
int version = invokeGetClientVersion(request);
System.out.println("Client version detected: v" + version);
Assert.assertEquals("Should detect v1 client", 1, version);
System.out.println("Test passed: compareMd5 will use v1 protocol for this client");
}
/**
* Test: compareMd5 method can be called with v2 client
* Note: This is a lightweight test focusing on version detection
* Full integration tests are covered separately
*/
@Test
public void testCompareMd5_VersionDetection_V2Client() throws Exception {
System.out.println("\n========== Test 8: Version detection in compareMd5 (v2) ==========");
when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("2");
// Verify version detection works
int version = invokeGetClientVersion(request);
System.out.println("Client version detected: v" + version);
Assert.assertEquals("Should detect v2 client", 2, version);
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("getClientProtocolVersion", HttpServletRequest.class, String.class);
method.setAccessible(true);
return (int) method.invoke(null, request, request.getHeader(Constants.CLIENT_VERSION));
}
}
Loading…
Cancel
Save