diff --git a/infra/common/src/main/java/cn/hippo4j/common/model/IncrementalFieldMetadataProvider.java b/infra/common/src/main/java/cn/hippo4j/common/model/IncrementalFieldMetadataProvider.java
deleted file mode 100644
index 1e0b3f5b..00000000
--- a/infra/common/src/main/java/cn/hippo4j/common/model/IncrementalFieldMetadataProvider.java
+++ /dev/null
@@ -1,50 +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.model;
-
-import java.util.Collections;
-import java.util.Map;
-
-/**
- * Optional provider for field-version metadata.
- *
- * Thread pool parameter models implementing this interface can explicitly declare the
- * relationship between fields and the protocol versions that understand them. This allows
- * the incremental content builder to omit unsupported fields for legacy clients and avoid
- * unnecessary refresh loops triggered by unknown data.
- */
-public interface IncrementalFieldMetadataProvider {
-
- /**
- * Return a mapping of field name to the minimum protocol version that can observe it.
- *
- * @return field -> minimum semantic version; fields not present fall back to defaults
- */
- default Map getFieldVersionMetadata() {
- return Collections.emptyMap();
- }
-
- /**
- * Optional version string of the metadata definition, useful for caching or diagnostics.
- *
- * @return metadata version identifier, or {@code null} if not set
- */
- default String getFieldMetadataVersion() {
- return null;
- }
-}
diff --git a/infra/common/src/main/java/cn/hippo4j/common/model/ThreadPoolParameterInfo.java b/infra/common/src/main/java/cn/hippo4j/common/model/ThreadPoolParameterInfo.java
index 2148468c..1a912c2c 100644
--- a/infra/common/src/main/java/cn/hippo4j/common/model/ThreadPoolParameterInfo.java
+++ b/infra/common/src/main/java/cn/hippo4j/common/model/ThreadPoolParameterInfo.java
@@ -25,7 +25,6 @@ import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
-import java.util.Map;
/**
* Thread pool parameter info.
@@ -35,7 +34,7 @@ import java.util.Map;
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
-public class ThreadPoolParameterInfo implements ThreadPoolParameter, Serializable, IncrementalFieldMetadataProvider {
+public class ThreadPoolParameterInfo implements ThreadPoolParameter, Serializable {
private static final long serialVersionUID = -7123935122108553864L;
@@ -128,16 +127,6 @@ public class ThreadPoolParameterInfo implements ThreadPoolParameter, Serializabl
*/
private Integer allowCoreThreadTimeOut;
- /**
- * Field-to-minimum-version mapping used by clients to filter unsupported fields.
- */
- private Map fieldVersionMetadata;
-
- /**
- * Optional metadata version identifier for diagnostics or caching.
- */
- private String fieldMetadataVersion;
-
public Integer corePoolSizeAdapt() {
return this.corePoolSize == null ? this.coreSize : this.corePoolSize;
}
diff --git a/infra/common/src/main/java/cn/hippo4j/common/toolkit/ContentUtil.java b/infra/common/src/main/java/cn/hippo4j/common/toolkit/ContentUtil.java
index eeca6fd2..b3137407 100644
--- a/infra/common/src/main/java/cn/hippo4j/common/toolkit/ContentUtil.java
+++ b/infra/common/src/main/java/cn/hippo4j/common/toolkit/ContentUtil.java
@@ -18,13 +18,9 @@
package cn.hippo4j.common.toolkit;
import cn.hippo4j.common.constant.Constants;
-import cn.hippo4j.common.model.IncrementalFieldMetadataProvider;
import cn.hippo4j.common.model.ThreadPoolParameter;
import cn.hippo4j.common.model.ThreadPoolParameterInfo;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
/**
* Content util.
*/
@@ -56,14 +52,6 @@ public class ContentUtil {
.setLivenessAlarm(parameter.getLivenessAlarm())
.setAllowCoreThreadTimeOut(parameter.getAllowCoreThreadTimeOut())
.setRejectedType(parameter.getRejectedType());
- if (parameter instanceof IncrementalFieldMetadataProvider) {
- IncrementalFieldMetadataProvider provider = (IncrementalFieldMetadataProvider) parameter;
- Map metadata = provider.getFieldVersionMetadata();
- if (metadata != null && !metadata.isEmpty()) {
- threadPoolParameterInfo.setFieldVersionMetadata(new LinkedHashMap<>(metadata));
- }
- threadPoolParameterInfo.setFieldMetadataVersion(provider.getFieldMetadataVersion());
- }
return JSONUtil.toJSONString(threadPoolParameterInfo);
}
diff --git a/infra/common/src/main/java/cn/hippo4j/common/toolkit/FieldVersionRegistry.java b/infra/common/src/main/java/cn/hippo4j/common/toolkit/FieldVersionRegistry.java
new file mode 100644
index 00000000..ccab3665
--- /dev/null
+++ b/infra/common/src/main/java/cn/hippo4j/common/toolkit/FieldVersionRegistry.java
@@ -0,0 +1,107 @@
+/*
+ * 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 lombok.extern.slf4j.Slf4j;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Global registry for field version metadata.
+ * Maintains persistent mapping of field names to their introduction versions.
+ * This ensures that field versions remain stable across server restarts and upgrades.
+ */
+@Slf4j
+public class FieldVersionRegistry {
+
+ /**
+ * Global field version mapping: fieldName -> introducedVersion
+ * Uses ConcurrentHashMap for thread-safe access without external synchronization.
+ */
+ private static final Map FIELD_VERSIONS = new ConcurrentHashMap<>();
+
+ /**
+ * Register a field with its introduction version.
+ * Uses putIfAbsent to preserve the first registered version (earliest version wins).
+ *
+ * @param fieldName field name
+ * @param version semantic version when this field was introduced (e.g., "2.0.0")
+ */
+ public static void registerField(String fieldName, String version) {
+ if (StringUtil.isBlank(fieldName) || StringUtil.isBlank(version)) {
+ return;
+ }
+ String existing = FIELD_VERSIONS.putIfAbsent(fieldName.trim(), version.trim());
+ if (existing == null) {
+ log.debug("Registered field version: {} -> {}", fieldName, version);
+ }
+ }
+
+ /**
+ * Register multiple fields with their introduction versions.
+ *
+ * @param fieldVersions mapping of field name to introduction version
+ */
+ public static void registerFields(Map fieldVersions) {
+ if (fieldVersions == null || fieldVersions.isEmpty()) {
+ return;
+ }
+ fieldVersions.forEach(FieldVersionRegistry::registerField);
+ }
+
+ /**
+ * Get the introduction version for a field.
+ *
+ * @param fieldName field name
+ * @return introduction version, or null if not registered
+ */
+ public static String getFieldVersion(String fieldName) {
+ if (StringUtil.isBlank(fieldName)) {
+ return null;
+ }
+ return FIELD_VERSIONS.get(fieldName.trim());
+ }
+
+ /**
+ * Get all registered field versions (read-only view).
+ *
+ * @return unmodifiable map of field name to introduction version
+ */
+ public static Map getAllFieldVersions() {
+ return Collections.unmodifiableMap(FIELD_VERSIONS);
+ }
+
+ /**
+ * Check if a field is registered.
+ *
+ * @param fieldName field name
+ * @return true if the field has a registered version
+ */
+ public static boolean isFieldRegistered(String fieldName) {
+ return StringUtil.isNotBlank(fieldName) && FIELD_VERSIONS.containsKey(fieldName.trim());
+ }
+
+ /**
+ * Clear all registered field versions (for testing only).
+ */
+ static void clearForTest() {
+ FIELD_VERSIONS.clear();
+ }
+}
diff --git a/infra/common/src/main/java/cn/hippo4j/common/toolkit/IncrementalContentUtil.java b/infra/common/src/main/java/cn/hippo4j/common/toolkit/IncrementalContentUtil.java
index b433955b..d281c02d 100644
--- a/infra/common/src/main/java/cn/hippo4j/common/toolkit/IncrementalContentUtil.java
+++ b/infra/common/src/main/java/cn/hippo4j/common/toolkit/IncrementalContentUtil.java
@@ -17,9 +17,7 @@
package cn.hippo4j.common.toolkit;
-import cn.hippo4j.common.model.IncrementalFieldMetadataProvider;
import cn.hippo4j.common.model.ThreadPoolParameter;
-import cn.hippo4j.common.model.ThreadPoolParameterInfo;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
@@ -33,62 +31,38 @@ import java.util.*;
public class IncrementalContentUtil {
/**
- * Core parameters that affect thread pool behavior
+ * Default server version used when Implementation-Version cannot be read from MANIFEST.MF.
+ * This typically occurs in development/test environments before packaging.
+ */
+ private static final String DEFAULT_SERVER_VERSION = "2.0.0";
+
+ private static final String[] CORE_IDS = {
+ "tenantId", "itemId", "tpId"
+ };
+
+ /**
+ * Core thread pool parameters that must be visible to all client versions.
+ * These fields are essential for thread pool operation and should not be modified.
*/
private static final String[] CORE_PARAMETERS = {
"coreSize", "maxSize", "queueType", "capacity",
- "keepAliveTime", "rejectedType", "allowCoreThreadTimeOut"
+ "keepAliveTime", "rejectedType"
};
- private static final List IDENTIFIER_FIELDS = Collections.unmodifiableList(Arrays.asList("tenantId", "itemId", "tpId"));
+ private static final List IDENTIFIER_FIELDS = Collections.unmodifiableList(Arrays.asList(CORE_IDS));
private static final List CORE_PARAMETER_LIST = Collections.unmodifiableList(Arrays.asList(CORE_PARAMETERS));
- private static final String FIELD_VERSION_METADATA_KEY = "fieldVersionMetadata";
-
- private static final String FIELD_METADATA_VERSION_KEY = "fieldMetadataVersion";
-
- /**
- * Get core content for MD5 calculation (only essential parameters)
- *
- * @param parameter thread-pool parameter
- * @return core content string for MD5
- */
- public static String getCoreContent(ThreadPoolParameter parameter) {
- ThreadPoolParameterInfo threadPoolParameterInfo = new ThreadPoolParameterInfo();
- threadPoolParameterInfo.setTenantId(parameter.getTenantId())
- .setItemId(parameter.getItemId())
- .setTpId(parameter.getTpId())
- .setCorePoolSize(getCorePoolSize(parameter))
- .setMaximumPoolSize(getMaximumPoolSize(parameter))
- .setQueueType(parameter.getQueueType())
- .setCapacity(parameter.getCapacity())
- .setKeepAliveTime(parameter.getKeepAliveTime())
- .setRejectedType(parameter.getRejectedType())
- .setAllowCoreThreadTimeOut(parameter.getAllowCoreThreadTimeOut());
- return JSONUtil.toJSONString(threadPoolParameterInfo);
- }
-
/**
- * Get full content for MD5 calculation (all parameters)
+ * Build version-aware content string for MD5 calculation.
+ * Fields introduced in newer versions are automatically excluded for older clients.
*
- * @param parameter thread-pool parameter
- * @return full content string for MD5
- */
- public static String getFullContent(ThreadPoolParameter parameter) {
- return ContentUtil.getPoolContent(parameter);
- }
-
- /**
- * 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 clientVersion semantic client version (optional, reserved for fine-grained rules)
- * @return version-aware content string
+ * @param parameter thread pool parameter
+ * @param clientVersion client semantic version (e.g., "1.5.0", null defaults to UNKNOWN_VERSION)
+ * @return filtered content string based on client version
*/
public static String getVersionedContent(ThreadPoolParameter parameter, String clientVersion) {
- String fullContent = getFullContent(parameter);
+ String fullContent = ContentUtil.getPoolContent(parameter);
LinkedHashMap raw = JSONUtil.parseObject(fullContent, new TypeReference>() {
});
if (raw == null) {
@@ -118,101 +92,8 @@ public class IncrementalContentUtil {
}
/**
- * Get core pool size with version compatibility handling
- *
- * @param parameter thread pool parameter
- * @return core pool size
- */
- private static Integer getCorePoolSize(ThreadPoolParameter parameter) {
- if (parameter instanceof ThreadPoolParameterInfo) {
- return ((ThreadPoolParameterInfo) parameter).corePoolSizeAdapt();
- }
- return parameter.getCoreSize();
- }
-
- /**
- * Get maximum pool size with version compatibility handling
- *
- * @param parameter thread pool parameter
- * @return maximum pool size
- */
- private static Integer getMaximumPoolSize(ThreadPoolParameter parameter) {
- if (parameter instanceof ThreadPoolParameterInfo) {
- return ((ThreadPoolParameterInfo) parameter).maximumPoolSizeAdapt();
- }
- return parameter.getMaxSize();
- }
-
- /**
- * Check if parameters have core changes that require thread pool refresh
- *
- * @param oldParameter old parameter
- * @param newParameter new parameter
- * @return true if core parameters changed
- */
- public static boolean hasCoreChanges(ThreadPoolParameter oldParameter, ThreadPoolParameter newParameter) {
- if (oldParameter == null || newParameter == null) {
- return true;
- }
- return !Objects.equals(getCorePoolSize(oldParameter), getCorePoolSize(newParameter)) ||
- !Objects.equals(getMaximumPoolSize(oldParameter), getMaximumPoolSize(newParameter)) ||
- !Objects.equals(oldParameter.getQueueType(), newParameter.getQueueType()) ||
- !Objects.equals(oldParameter.getCapacity(), newParameter.getCapacity()) ||
- !Objects.equals(oldParameter.getKeepAliveTime(), newParameter.getKeepAliveTime()) ||
- !Objects.equals(oldParameter.getRejectedType(), newParameter.getRejectedType()) ||
- !Objects.equals(oldParameter.getAllowCoreThreadTimeOut(), newParameter.getAllowCoreThreadTimeOut());
- }
-
- /**
- * Check if parameters have extended changes (non-core)
- *
- * @param oldParameter old parameter
- * @param newParameter new parameter
- * @return true if extended parameters changed
- */
- public static boolean hasExtendedChanges(ThreadPoolParameter oldParameter, ThreadPoolParameter newParameter) {
- if (oldParameter == null || newParameter == null) {
- return true;
- }
- return !Objects.equals(oldParameter.getExecuteTimeOut(), newParameter.getExecuteTimeOut()) ||
- !Objects.equals(oldParameter.getIsAlarm(), newParameter.getIsAlarm()) ||
- !Objects.equals(oldParameter.getCapacityAlarm(), newParameter.getCapacityAlarm()) ||
- !Objects.equals(oldParameter.getLivenessAlarm(), newParameter.getLivenessAlarm());
- }
-
- /**
- * Get parameter changes summary
- *
- * @param oldParameter old parameter
- * @param newParameter new parameter
- * @return changes summary map
- */
- public static Map getChangesSummary(ThreadPoolParameter oldParameter, ThreadPoolParameter newParameter) {
- Map changes = new HashMap<>();
- if (oldParameter == null || newParameter == null) {
- changes.put("type", "full");
- changes.put("reason", "initial_load");
- return changes;
- }
- boolean coreChanges = hasCoreChanges(oldParameter, newParameter);
- boolean extendedChanges = hasExtendedChanges(oldParameter, newParameter);
- if (coreChanges) {
- changes.put("type", "core");
- changes.put("reason", "core_parameters_changed");
- } else if (extendedChanges) {
- changes.put("type", "extended");
- changes.put("reason", "extended_parameters_changed");
- } else {
- changes.put("type", "none");
- changes.put("reason", "no_changes");
- }
- return changes;
- }
-
- /**
- * 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.
+ * Check if a field should be included for the given client version.
+ * Returns false if the field requires a higher version than the client supports.
*/
private static boolean shouldIncludeField(String field, String clientVersion, Map fieldRules) {
String minVersion = fieldRules.get(field);
@@ -224,30 +105,39 @@ public class IncrementalContentUtil {
}
/**
- * Resolve field-level version rules by combining default baseline, runtime metadata from the
- * parameter object, and metadata embedded in the JSON payload. Fields without explicit metadata
- * are assigned a default minimum version based on the current protocol.
+ * Build field-to-version mapping by priority:
+ * 1. Core/identifier fields → UNKNOWN_VERSION (visible to all clients)
+ * 2. Registry fields → pre-registered version in FieldVersionInitializer
+ * 3. Runtime metadata → for testing or manual override
+ * 4. Unregistered fields → current server version (with warning)
*
- * @param parameter thread pool parameter (may carry metadata)
- * @param raw parsed JSON payload (may contain fieldVersionMetadata)
- * @return mapping of field name to minimum semantic version
+ * @param parameter thread pool parameter
+ * @param raw parsed JSON map
+ * @return field name to minimum required version mapping
*/
private static Map resolveFieldRules(ThreadPoolParameter parameter, Map raw) {
Map fieldRules = new LinkedHashMap<>();
- // Identifier and core fields visible to all clients (since version 1.0.0)
+
+ // Core and identifier fields (visible to all versions)
IDENTIFIER_FIELDS.forEach(field -> fieldRules.put(field, VersionUtil.UNKNOWN_VERSION));
CORE_PARAMETER_LIST.forEach(field -> fieldRules.put(field, VersionUtil.UNKNOWN_VERSION));
- // Merge metadata from parameter object (e.g., Server-side configuration)
- mergeFieldMetadata(fieldRules, extractMetadataFromParameter(parameter));
- // Merge metadata from JSON payload (e.g., Client receiving Server's dynamic metadata)
- mergeFieldMetadata(fieldRules, extractMetadataFromPayload(raw));
+ // Load from global registry
+ Map registryVersions = FieldVersionRegistry.getAllFieldVersions();
+ registryVersions.forEach((field, version) -> {
+ if (!fieldRules.containsKey(field)) {
+ fieldRules.put(field, version);
+ }
+ });
- // Assign default version to unconfigured fields (prevents old clients from seeing new fields)
- // Default to "2.0.0" for new fields to maintain backward compatibility
+ // Handle unregistered fields
+ String currentServerVersion = getCurrentServerVersion();
raw.keySet().forEach(field -> {
if (!fieldRules.containsKey(field)) {
- fieldRules.put(field, "2.0.0"); // Default: visible to clients >= 2.0
+ log.warn("Unregistered field '{}' detected. Binding to current server version '{}'. " +
+ "To fix: Add to FieldVersionInitializer or CORE_PARAMETERS if essential.",
+ field, currentServerVersion);
+ fieldRules.put(field, currentServerVersion);
}
});
@@ -255,73 +145,22 @@ public class IncrementalContentUtil {
}
/**
- * Extract field version metadata from the parameter object if it implements
- * {@link IncrementalFieldMetadataProvider}. This is typically used on the Server side where
- * configuration objects can dynamically declare which fields were introduced in which version.
+ * Get current server version from package metadata (set by Maven during build).
+ * Falls back to DEFAULT_SERVER_VERSION if not available (e.g., in development/test environments).
*
- * @param parameter thread pool parameter
- * @return field-to-version mapping, or empty map if not available
+ * @return server semantic version string
*/
- private static Map extractMetadataFromParameter(ThreadPoolParameter parameter) {
- if (parameter instanceof IncrementalFieldMetadataProvider) {
- Map metadata = ((IncrementalFieldMetadataProvider) parameter).getFieldVersionMetadata();
- if (metadata != null && !metadata.isEmpty()) {
- Map copied = new LinkedHashMap<>();
- metadata.forEach((field, version) -> {
- if (StringUtil.isNotBlank(field) && StringUtil.isNotBlank(version)) {
- copied.put(field, version.trim());
- }
- });
- return copied;
+ private static String getCurrentServerVersion() {
+ Package pkg = IncrementalContentUtil.class.getPackage();
+ if (pkg != null) {
+ String implVersion = pkg.getImplementationVersion();
+ if (StringUtil.isNotBlank(implVersion)) {
+ return implVersion.trim();
}
}
- return Collections.emptyMap();
- }
-
- /**
- * Extract field version metadata from the JSON payload and remove metadata keys from the raw map
- * so they do not participate in MD5 calculation. This is typically used on the Client side to
- * receive dynamic metadata from the Server.
- *
- * @param raw parsed JSON map (will be modified: metadata keys removed)
- * @return field-to-version mapping extracted from the payload, or empty map if not present
- */
- @SuppressWarnings("unchecked")
- private static Map extractMetadataFromPayload(Map raw) {
- Object metadataObject = raw.remove(FIELD_VERSION_METADATA_KEY);
- raw.remove(FIELD_METADATA_VERSION_KEY);
- if (metadataObject instanceof Map, ?>) {
- Map metadata = new LinkedHashMap<>();
- ((Map, ?>) metadataObject).forEach((key, value) -> {
- if (key == null || value == null) {
- return;
- }
- String field = String.valueOf(key);
- String version = String.valueOf(value).trim();
- if (StringUtil.isNotBlank(field) && StringUtil.isNotBlank(version)) {
- metadata.put(field, version);
- }
- });
- return metadata;
- }
- return Collections.emptyMap();
- }
-
- /**
- * Merge additional field version metadata into the target map. Existing entries in the target
- * will be overwritten by additions. This enables layered metadata resolution (base → parameter → payload).
- *
- * @param target target map to merge into
- * @param additions additional metadata to merge (may be null or empty)
- */
- private static void mergeFieldMetadata(Map target, Map additions) {
- if (additions == null || additions.isEmpty()) {
- return;
- }
- additions.forEach((field, version) -> {
- if (StringUtil.isNotBlank(field) && StringUtil.isNotBlank(version)) {
- target.put(field, version.trim());
- }
- });
+ log.warn("Unable to read Implementation-Version from MANIFEST.MF. " +
+ "Falling back to default version '{}'. " +
+ "Please ensure maven-jar-plugin is properly configured.", DEFAULT_SERVER_VERSION);
+ return DEFAULT_SERVER_VERSION;
}
}
diff --git a/infra/common/src/main/java/cn/hippo4j/common/toolkit/IncrementalMd5Util.java b/infra/common/src/main/java/cn/hippo4j/common/toolkit/IncrementalMd5Util.java
index c95a33d7..8b4dfc52 100644
--- a/infra/common/src/main/java/cn/hippo4j/common/toolkit/IncrementalMd5Util.java
+++ b/infra/common/src/main/java/cn/hippo4j/common/toolkit/IncrementalMd5Util.java
@@ -27,27 +27,6 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class IncrementalMd5Util {
- /**
- * Get core MD5 for essential parameters only
- *
- * @param config thread pool parameter
- * @return core MD5 hash
- */
- public static String getCoreMd5(ThreadPoolParameter config) {
- String coreContent = IncrementalContentUtil.getCoreContent(config);
- return Md5Util.md5Hex(coreContent, "UTF-8");
- }
-
- /**
- * Get full MD5 for all parameters (legacy compatibility)
- *
- * @param config thread pool parameter
- * @return full MD5 hash
- */
- public static String getFullMd5(ThreadPoolParameter config) {
- return Md5Util.getTpContentMd5(config);
- }
-
/**
* Get versioned MD5 based on client semantic version.
*
@@ -67,58 +46,4 @@ public class IncrementalMd5Util {
return md5;
}
- /**
- * Compare MD5 with version support using semantic version string.
- */
- public static boolean isDifferent(ThreadPoolParameter oldConfig, ThreadPoolParameter newConfig, String clientVersion) {
- if (oldConfig == null || newConfig == null) {
- return true;
- }
- String oldMd5 = getVersionedMd5(oldConfig, clientVersion);
- String newMd5 = getVersionedMd5(newConfig, clientVersion);
- boolean different = !oldMd5.equals(newMd5);
- if (different && log.isDebugEnabled()) {
- log.debug("Configuration changed - Old MD5: {}, New MD5: {}, Client Version: {}",
- oldMd5, newMd5, clientVersion);
- }
- return different;
- }
-
- /**
- * Check if only extended parameters changed (non-core)
- *
- * @param oldConfig old configuration
- * @param newConfig new configuration
- * @return true if only extended parameters changed
- */
- public static boolean onlyExtendedChanged(ThreadPoolParameter oldConfig, ThreadPoolParameter newConfig) {
- if (oldConfig == null || newConfig == null) {
- return false;
- }
- boolean coreChanged = IncrementalContentUtil.hasCoreChanges(oldConfig, newConfig);
- boolean extendedChanged = IncrementalContentUtil.hasExtendedChanges(oldConfig, newConfig);
- return !coreChanged && extendedChanged;
- }
-
- /**
- * Get change type for logging and monitoring
- *
- * @param oldConfig old configuration
- * @param newConfig new configuration
- * @return change type string
- */
- public static String getChangeType(ThreadPoolParameter oldConfig, ThreadPoolParameter newConfig) {
- if (oldConfig == null || newConfig == null) {
- return "INITIAL";
- }
- boolean coreChanged = IncrementalContentUtil.hasCoreChanges(oldConfig, newConfig);
- boolean extendedChanged = IncrementalContentUtil.hasExtendedChanges(oldConfig, newConfig);
- if (coreChanged) {
- return "CORE";
- } else if (extendedChanged) {
- return "EXTENDED";
- } else {
- return "NONE";
- }
- }
}
diff --git a/infra/common/src/main/java/cn/hippo4j/common/toolkit/VersionUtil.java b/infra/common/src/main/java/cn/hippo4j/common/toolkit/VersionUtil.java
index 3284924d..98739921 100644
--- a/infra/common/src/main/java/cn/hippo4j/common/toolkit/VersionUtil.java
+++ b/infra/common/src/main/java/cn/hippo4j/common/toolkit/VersionUtil.java
@@ -17,10 +17,7 @@
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;
diff --git a/infra/common/src/test/java/cn/hippo4j/common/toolkit/ExtensibilityTest.java b/infra/common/src/test/java/cn/hippo4j/common/toolkit/ExtensibilityTest.java
deleted file mode 100644
index 2865a74a..00000000
--- a/infra/common/src/test/java/cn/hippo4j/common/toolkit/ExtensibilityTest.java
+++ /dev/null
@@ -1,285 +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.Map;
-
-/**
- * Extensibility issue test: verify that adding new extended parameters does not trigger invalid refresh.
- */
-public class ExtensibilityTest {
-
- /**
- * Scenario 1: Server adds an extended parameter (executeTimeOut), Client does not have this parameter.
- * Expected result: Under v2 protocol, MD5 remains the same, no refresh triggered.
- */
- @Test
- public void testExtendedParameterAddition_ExecuteTimeOut() {
- System.out.println("========== Scenario 1: Server adds executeTimeOut extended parameter ==========");
-
- // Client configuration (without executeTimeOut)
- ThreadPoolParameterInfo clientConfig = new ThreadPoolParameterInfo();
- clientConfig.setTenantId("default");
- clientConfig.setItemId("item-001");
- clientConfig.setTpId("test-pool");
- clientConfig.setCorePoolSize(10);
- clientConfig.setMaximumPoolSize(20);
- clientConfig.setQueueType(2);
- clientConfig.setCapacity(1024);
- clientConfig.setKeepAliveTime(60L);
- clientConfig.setRejectedType(1);
- clientConfig.setAllowCoreThreadTimeOut(0);
- // Note: executeTimeOut not set
-
- // Server configuration (with executeTimeOut added)
- ThreadPoolParameterInfo serverConfig = new ThreadPoolParameterInfo();
- serverConfig.setTenantId("default");
- 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 extended parameter
-
- // v2 protocol: only core parameters are included in MD5 calculation
- String clientContent = IncrementalContentUtil.getCoreContent(clientConfig);
- String clientMd5 = Md5Util.md5Hex(clientContent, "UTF-8");
-
- String serverContent = IncrementalContentUtil.getCoreContent(serverConfig);
- String serverMd5 = Md5Util.md5Hex(serverContent, "UTF-8");
-
- System.out.println("Client config: executeTimeOut=" + clientConfig.getExecuteTimeOut());
- System.out.println("Server config: executeTimeOut=" + serverConfig.getExecuteTimeOut());
- System.out.println("Client incremental content: " + clientContent);
- System.out.println("Server incremental content: " + serverContent);
- System.out.println("Client MD5: " + clientMd5);
- System.out.println("Server MD5: " + serverMd5);
- System.out.println("Does server incremental content contain executeTimeOut: " + serverContent.contains("executeTimeOut"));
-
- Assert.assertFalse("Incremental content should not contain extended parameter executeTimeOut", serverContent.contains("executeTimeOut"));
- Assert.assertEquals("Adding extended parameter should not affect incremental MD5 and should not trigger refresh", serverMd5, clientMd5);
- System.out.println("Test passed: Adding executeTimeOut does not trigger invalid refresh");
- }
-
- /**
- * Scenario 2: Server adds multiple extended parameters (executeTimeOut, isAlarm, capacityAlarm).
- * Expected result: Under v2 protocol, MD5 remains the same, no refresh triggered.
- */
- @Test
- public void testMultipleExtendedParametersAddition() {
- System.out.println("\n========== Scenario 2: Server adds multiple extended parameters ==========");
-
- // Client configuration (without extended parameters)
- ThreadPoolParameterInfo clientConfig = new ThreadPoolParameterInfo();
- clientConfig.setTenantId("default");
- clientConfig.setItemId("item-001");
- clientConfig.setTpId("test-pool");
- clientConfig.setCorePoolSize(10);
- clientConfig.setMaximumPoolSize(20);
- clientConfig.setQueueType(2);
- clientConfig.setCapacity(1024);
-
- // Server configuration (with multiple extended parameters)
- ThreadPoolParameterInfo serverConfig = new ThreadPoolParameterInfo();
- serverConfig.setTenantId("default");
- serverConfig.setItemId("item-001");
- serverConfig.setTpId("test-pool");
- serverConfig.setCorePoolSize(10);
- serverConfig.setMaximumPoolSize(20);
- serverConfig.setQueueType(2);
- serverConfig.setCapacity(1024);
- serverConfig.setExecuteTimeOut(5000L); // extended parameter 1
- serverConfig.setIsAlarm(1); // extended parameter 2
- serverConfig.setCapacityAlarm(80); // extended parameter 3
- serverConfig.setLivenessAlarm(90); // extended parameter 4
-
- String clientMd5 = Md5Util.md5Hex(IncrementalContentUtil.getCoreContent(clientConfig), "UTF-8");
- String serverMd5 = Md5Util.md5Hex(IncrementalContentUtil.getCoreContent(serverConfig), "UTF-8");
-
- System.out.println("Server added extended parameters: executeTimeOut=" + serverConfig.getExecuteTimeOut()
- + ", isAlarm=" + serverConfig.getIsAlarm()
- + ", capacityAlarm=" + serverConfig.getCapacityAlarm()
- + ", livenessAlarm=" + serverConfig.getLivenessAlarm());
- System.out.println("Client MD5: " + clientMd5);
- System.out.println("Server MD5: " + serverMd5);
-
- Assert.assertEquals("Adding multiple extended parameters should not trigger refresh", serverMd5, clientMd5);
- System.out.println("Test passed: Adding multiple extended parameters does not trigger invalid refresh");
- }
-
- /**
- * Scenario 3: Only extended parameters changed, core parameters remain unchanged.
- * Expected result: hasCoreChanges returns false, hasExtendedChanges returns true.
- */
- @Test
- public void testOnlyExtendedParametersChanged() {
- System.out.println("\n========== Scenario 3: Only extended parameters changed ==========");
-
- ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
- oldConfig.setTenantId("default");
- oldConfig.setItemId("item-001");
- oldConfig.setTpId("test-pool");
- oldConfig.setCorePoolSize(10);
- oldConfig.setMaximumPoolSize(20);
- oldConfig.setQueueType(2);
- oldConfig.setCapacity(1024);
- oldConfig.setExecuteTimeOut(3000L);
- oldConfig.setIsAlarm(0);
-
- ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
- newConfig.setTenantId("default");
- newConfig.setItemId("item-001");
- newConfig.setTpId("test-pool");
- newConfig.setCorePoolSize(10);
- newConfig.setMaximumPoolSize(20);
- newConfig.setQueueType(2);
- newConfig.setCapacity(1024);
- newConfig.setExecuteTimeOut(5000L);
- newConfig.setIsAlarm(1);
-
- boolean hasCoreChanges = IncrementalContentUtil.hasCoreChanges(oldConfig, newConfig);
- boolean hasExtendedChanges = IncrementalContentUtil.hasExtendedChanges(oldConfig, newConfig);
-
- System.out.println("Core parameter changes: " + hasCoreChanges);
- System.out.println("Extended parameter changes: " + hasExtendedChanges);
-
- Assert.assertFalse("Core parameters should not change", hasCoreChanges);
- Assert.assertTrue("Extended parameters should change", hasExtendedChanges);
- System.out.println("Test passed: Correctly identifies only extended parameters changed");
- }
-
- /**
- * Scenario 4: Both core and extended parameters changed.
- * Expected result: hasCoreChanges returns true, hasExtendedChanges returns true.
- */
- @Test
- public void testBothCoreAndExtendedParametersChanged() {
- System.out.println("\n========== Scenario 4: Both core and extended parameters changed ==========");
-
- ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
- oldConfig.setCorePoolSize(10);
- oldConfig.setMaximumPoolSize(20);
- oldConfig.setQueueType(2);
- oldConfig.setExecuteTimeOut(3000L);
-
- ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
- newConfig.setCorePoolSize(15);
- newConfig.setMaximumPoolSize(30);
- newConfig.setQueueType(2);
- newConfig.setExecuteTimeOut(5000L);
-
- boolean hasCoreChanges = IncrementalContentUtil.hasCoreChanges(oldConfig, newConfig);
- boolean hasExtendedChanges = IncrementalContentUtil.hasExtendedChanges(oldConfig, newConfig);
-
- System.out.println("Core parameter changes: " + hasCoreChanges);
- System.out.println("Extended parameter changes: " + hasExtendedChanges);
-
- Assert.assertTrue("Core parameters should change", hasCoreChanges);
- Assert.assertTrue("Extended parameters should change", hasExtendedChanges);
- System.out.println("Test passed: Correctly identifies both core and extended parameters changed");
- }
-
- /**
- * Scenario 5: v1 client refreshes due to extended parameter changes, v2 client does not.
- * Expected result: v1 MD5 differs, v2 MD5 remains the same.
- */
- @Test
- public void testProtocolVersionBehaviorDifference() {
- System.out.println("\n========== Scenario 5: v1 and v2 protocol differences in handling extended parameters ==========");
-
- ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo();
- oldConfig.setTenantId("default");
- oldConfig.setItemId("item-001");
- oldConfig.setTpId("test-pool");
- oldConfig.setCorePoolSize(10);
- oldConfig.setMaximumPoolSize(20);
- oldConfig.setQueueType(2);
- oldConfig.setExecuteTimeOut(3000L);
-
- ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo();
- newConfig.setTenantId("default");
- newConfig.setItemId("item-001");
- newConfig.setTpId("test-pool");
- newConfig.setCorePoolSize(10);
- newConfig.setMaximumPoolSize(20);
- newConfig.setQueueType(2);
- newConfig.setExecuteTimeOut(5000L);
-
- // v1 protocol: full MD5
- String oldV1Md5 = Md5Util.md5Hex(IncrementalContentUtil.getFullContent(oldConfig), "UTF-8");
- String newV1Md5 = Md5Util.md5Hex(IncrementalContentUtil.getFullContent(newConfig), "UTF-8");
-
- // v2 protocol: incremental MD5 (only core parameters)
- String oldV2Md5 = Md5Util.md5Hex(IncrementalContentUtil.getCoreContent(oldConfig), "UTF-8");
- String newV2Md5 = Md5Util.md5Hex(IncrementalContentUtil.getCoreContent(newConfig), "UTF-8");
-
- System.out.println("Extended parameter changed: executeTimeOut " + oldConfig.getExecuteTimeOut() + " -> " + newConfig.getExecuteTimeOut());
- System.out.println("v1 protocol: oldMd5=" + oldV1Md5 + ", newMd5=" + newV1Md5 + ", equal=" + oldV1Md5.equals(newV1Md5));
- System.out.println("v2 protocol: oldMd5=" + oldV2Md5 + ", newMd5=" + newV2Md5 + ", equal=" + oldV2Md5.equals(newV2Md5));
-
- Assert.assertNotEquals("v1 protocol: extended parameter changes should trigger refresh (MD5 differs)", oldV1Md5, newV1Md5);
- Assert.assertEquals("v2 protocol: extended parameter changes should not trigger refresh (MD5 same)", oldV2Md5, newV2Md5);
- System.out.println("Test passed: v2 protocol correctly isolates extended parameter changes");
- }
-
- /**
- * Scenario 6: getChangesSummary correctly identifies change types.
- */
- @Test
- public void testChangesSummary() {
- System.out.println("\n========== Scenario 6: Change summary identification ==========");
-
- ThreadPoolParameterInfo baseConfig = new ThreadPoolParameterInfo();
- baseConfig.setCorePoolSize(10);
- baseConfig.setMaximumPoolSize(20);
- baseConfig.setQueueType(2);
- baseConfig.setExecuteTimeOut(3000L);
-
- // Case 1: Only extended parameter changed
- ThreadPoolParameterInfo extendedOnlyConfig = new ThreadPoolParameterInfo();
- extendedOnlyConfig.setCorePoolSize(10);
- extendedOnlyConfig.setMaximumPoolSize(20);
- extendedOnlyConfig.setQueueType(2);
- extendedOnlyConfig.setExecuteTimeOut(5000L);
-
- Map extendedOnlySummary = IncrementalContentUtil.getChangesSummary(baseConfig, extendedOnlyConfig);
- System.out.println("Only extended parameter change summary: " + extendedOnlySummary);
- Assert.assertEquals("extended", extendedOnlySummary.get("type"));
-
- // Case 2: Core parameters changed
- ThreadPoolParameterInfo coreChangedConfig = new ThreadPoolParameterInfo();
- coreChangedConfig.setCorePoolSize(15);
- coreChangedConfig.setMaximumPoolSize(20);
- coreChangedConfig.setQueueType(2);
- coreChangedConfig.setExecuteTimeOut(3000L);
-
- Map coreChangedSummary = IncrementalContentUtil.getChangesSummary(baseConfig, coreChangedConfig);
- System.out.println("Core parameter change summary: " + coreChangedSummary);
- Assert.assertEquals("core", coreChangedSummary.get("type"));
-
- System.out.println("Test passed: Change summary correctly identified");
- }
-}
\ No newline at end of file
diff --git a/infra/common/src/test/java/cn/hippo4j/common/toolkit/FieldVersionControlTest.java b/infra/common/src/test/java/cn/hippo4j/common/toolkit/FieldVersionControlTest.java
deleted file mode 100644
index df64e6a5..00000000
--- a/infra/common/src/test/java/cn/hippo4j/common/toolkit/FieldVersionControlTest.java
+++ /dev/null
@@ -1,443 +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 com.fasterxml.jackson.core.type.TypeReference;
-import org.junit.Assert;
-import org.junit.Test;
-
-import java.util.Collections;
-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'
- // Explicitly mark that the field is only recognized by clients from protocol v3 onward
- 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 version = 2.1.0)
- serverConfig.setFieldVersionMetadata(Collections.singletonMap("executeTimeOut", "2.1.0"));
-
- // Protocol v1 client content generation
- String v1Content = IncrementalContentUtil.getVersionedContent(serverConfig, "1.9.0");
- LinkedHashMap v1Fields = JSONUtil.parseObject(v1Content, new TypeReference>() {
- });
-
- // Protocol v2 client content generation
- String v2Content = IncrementalContentUtil.getVersionedContent(serverConfig, "2.0.0");
- LinkedHashMap v2Fields = JSONUtil.parseObject(v2Content, new TypeReference>() {
- });
-
- // Protocol v3 client content generation
- String v3Content = IncrementalContentUtil.getVersionedContent(serverConfig, "2.1.0");
- LinkedHashMap v3Fields = JSONUtil.parseObject(v3Content, new TypeReference>() {
- });
-
- 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 v3 content: " + v3Content);
- System.out.println("Protocol v1 contains executeTimeOut: " + v1Fields.containsKey("executeTimeOut"));
- System.out.println("Protocol v2 contains executeTimeOut: " + v2Fields.containsKey("executeTimeOut"));
- System.out.println("Protocol v3 contains executeTimeOut: " + v3Fields.containsKey("executeTimeOut"));
-
- // Assertions
- Assert.assertFalse("Protocol v1 should skip executeTimeOut (min protocol = 3)", v1Fields.containsKey("executeTimeOut"));
- Assert.assertFalse("Protocol v2 should skip executeTimeOut (min protocol = 3)", v2Fields.containsKey("executeTimeOut"));
- Assert.assertTrue("Protocol v3 should include executeTimeOut", v3Fields.containsKey("executeTimeOut"));
- System.out.println("Test passed: Protocol v1/v2 clients skip new field, v3 client observes it");
- }
-
- /**
- * 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)
- 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 version = 2.1.0
- config.setFieldVersionMetadata(Collections.singletonMap("isAlarm", "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 v1Fields = JSONUtil.parseObject(v1Content, new TypeReference>() {
- });
- LinkedHashMap v2Fields = JSONUtil.parseObject(v2Content, new TypeReference>() {
- });
- LinkedHashMap v3Fields = JSONUtil.parseObject(v3Content, new TypeReference>() {
- });
-
- 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.assertFalse("Protocol v1 should skip isAlarm (min protocol = 3)", 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 version = 2.1.0)
- newConfig.setFieldVersionMetadata(Collections.singletonMap("executeTimeOut", "2.1.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());
- 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.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"));
- 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 version = 2.1.0)
- serverV20Config.setFieldVersionMetadata(Collections.singletonMap("isAlarm", "2.1.0"));
-
- // 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.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);
- 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 version = 2.1.0)
- serverV21Config.setFieldVersionMetadata(Collections.singletonMap("capacityAlarm", "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 v1Fields = JSONUtil.parseObject(v1Content, new TypeReference>() {
- });
- LinkedHashMap v2Fields = JSONUtil.parseObject(v2Content, new TypeReference>() {
- });
- LinkedHashMap v3Fields = JSONUtil.parseObject(v3Content, new TypeReference>() {
- });
-
- 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.assertFalse("Protocol v1 should skip 'yyy' (capacityAlarm)", 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
- config2.setFieldVersionMetadata(Collections.singletonMap("executeTimeOut", "2.1.0"));
-
- // 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
- config3.setFieldVersionMetadata(Collections.singletonMap("executeTimeOut", "2.1.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");
- 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);
- LinkedHashMap metadata = new LinkedHashMap<>();
- metadata.put("executeTimeOut", "2.1.0");
- metadata.put("isAlarm", "2.1.0");
- metadata.put("capacityAlarm", "2.1.0");
- metadata.put("livenessAlarm", "2.1.0");
- fullConfig.setFieldVersionMetadata(metadata);
-
- 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 v1Fields = JSONUtil.parseObject(v1Content, new TypeReference>() {
- });
- LinkedHashMap v2Fields = JSONUtil.parseObject(v2Content, new TypeReference>() {
- });
- LinkedHashMap v3Fields = JSONUtil.parseObject(v3Content, new TypeReference>() {
- });
-
- 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.assertFalse("Protocol v1 (1.9.0) should skip executeTimeOut", v1Fields.containsKey("executeTimeOut"));
- Assert.assertFalse("Protocol v2 (2.0.0) should skip executeTimeOut", v2Fields.containsKey("executeTimeOut"));
- Assert.assertTrue("Protocol v3 (2.1.0) should include executeTimeOut", v3Fields.containsKey("executeTimeOut"));
-
- System.out.println("\nTest passed: Field visibility correctly controlled by semantic version");
- System.out.println("This is the foundation for mentor's requirement: version-aware field filtering");
- }
-}
diff --git a/infra/common/src/test/java/cn/hippo4j/common/toolkit/FieldVersionRegistryTest.java b/infra/common/src/test/java/cn/hippo4j/common/toolkit/FieldVersionRegistryTest.java
new file mode 100644
index 00000000..0e1b1548
--- /dev/null
+++ b/infra/common/src/test/java/cn/hippo4j/common/toolkit/FieldVersionRegistryTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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 org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Test for field version registry.
+ */
+public class FieldVersionRegistryTest {
+
+ @Before
+ public void setUp() {
+ // Clear registry before each test
+ FieldVersionRegistry.clearForTest();
+ }
+
+ /**
+ * Test basic field registration
+ */
+ @Test
+ public void testBasicFieldRegistration() {
+ FieldVersionRegistry.registerField("executeTimeOut", "2.0.0");
+ FieldVersionRegistry.registerField("isAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("newField", "2.1.0");
+
+ String version1 = FieldVersionRegistry.getFieldVersion("executeTimeOut");
+ String version2 = FieldVersionRegistry.getFieldVersion("isAlarm");
+ String version3 = FieldVersionRegistry.getFieldVersion("newField");
+
+ Assert.isTrue("2.0.0".equals(version1), "executeTimeOut should be version 2.0.0");
+ Assert.isTrue("2.0.0".equals(version2), "isAlarm should be version 2.0.0");
+ Assert.isTrue("2.1.0".equals(version3), "newField should be version 2.1.0");
+
+ System.out.println("Basic field registration test passed");
+ }
+
+ /**
+ * Test that first registration wins (putIfAbsent behavior)
+ */
+ @Test
+ public void testFirstRegistrationWins() {
+ // First registration
+ FieldVersionRegistry.registerField("testField", "1.0.0");
+
+ // Try to register again with different version
+ FieldVersionRegistry.registerField("testField", "2.0.0");
+
+ // Should still be 1.0.0 (first registration wins)
+ String version = FieldVersionRegistry.getFieldVersion("testField");
+ Assert.isTrue("1.0.0".equals(version), "First registration should win (putIfAbsent)");
+
+ System.out.println("First registration wins test passed");
+ }
+
+ /**
+ * Test batch field registration
+ */
+ @Test
+ public void testBatchFieldRegistration() {
+ Map fields = new HashMap<>();
+ fields.put("field1", "2.0.0");
+ fields.put("field2", "2.0.0");
+ fields.put("field3", "2.1.0");
+
+ FieldVersionRegistry.registerFields(fields);
+
+ Assert.isTrue("2.0.0".equals(FieldVersionRegistry.getFieldVersion("field1")), "field1 version correct");
+ Assert.isTrue("2.0.0".equals(FieldVersionRegistry.getFieldVersion("field2")), "field2 version correct");
+ Assert.isTrue("2.1.0".equals(FieldVersionRegistry.getFieldVersion("field3")), "field3 version correct");
+
+ System.out.println("Batch field registration test passed");
+ }
+
+ /**
+ * Test getting all field versions
+ */
+ @Test
+ public void testGetAllFieldVersions() {
+ FieldVersionRegistry.registerField("field1", "2.0.0");
+ FieldVersionRegistry.registerField("field2", "2.1.0");
+ FieldVersionRegistry.registerField("field3", "2.2.0");
+
+ Map allVersions = FieldVersionRegistry.getAllFieldVersions();
+
+ Assert.isTrue(allVersions.size() == 3, "Should have 3 registered fields");
+ Assert.isTrue("2.0.0".equals(allVersions.get("field1")), "field1 version correct");
+ Assert.isTrue("2.1.0".equals(allVersions.get("field2")), "field2 version correct");
+ Assert.isTrue("2.2.0".equals(allVersions.get("field3")), "field3 version correct");
+
+ System.out.println("Get all field versions test passed");
+ }
+
+ /**
+ * Test that returned map is unmodifiable
+ */
+ @Test
+ public void testReturnedMapIsUnmodifiable() {
+ FieldVersionRegistry.registerField("testField", "2.0.0");
+
+ Map allVersions = FieldVersionRegistry.getAllFieldVersions();
+
+ boolean exceptionThrown = false;
+ try {
+ allVersions.put("newField", "2.1.0");
+ } catch (UnsupportedOperationException e) {
+ exceptionThrown = true;
+ }
+
+ Assert.isTrue(exceptionThrown, "Returned map should be unmodifiable");
+
+ System.out.println("Unmodifiable map test passed");
+ }
+
+ /**
+ * Test getting version of unregistered field
+ */
+ @Test
+ public void testGetVersionOfUnregisteredField() {
+ String version = FieldVersionRegistry.getFieldVersion("nonExistentField");
+
+ Assert.isTrue(version == null, "Unregistered field should return null");
+
+ System.out.println("Unregistered field test passed");
+ }
+
+ /**
+ * Test clear functionality
+ */
+ @Test
+ public void testClearRegistry() {
+ FieldVersionRegistry.registerField("field1", "2.0.0");
+ FieldVersionRegistry.registerField("field2", "2.0.0");
+
+ Map allVersions = FieldVersionRegistry.getAllFieldVersions();
+ Assert.isTrue(allVersions.size() == 2, "Should have 2 fields before clear");
+
+ FieldVersionRegistry.clearForTest();
+
+ allVersions = FieldVersionRegistry.getAllFieldVersions();
+ Assert.isTrue(allVersions.size() == 0, "Should have 0 fields after clear");
+
+ System.out.println("Clear registry test passed");
+ }
+
+ /**
+ * Test null and blank field name handling
+ */
+ @Test
+ public void testNullAndBlankFieldNames() {
+ // Register null field name (should be ignored)
+ FieldVersionRegistry.registerField(null, "2.0.0");
+
+ // Register blank field name (should be ignored)
+ FieldVersionRegistry.registerField("", "2.0.0");
+ FieldVersionRegistry.registerField(" ", "2.0.0");
+
+ Map allVersions = FieldVersionRegistry.getAllFieldVersions();
+ Assert.isTrue(allVersions.size() == 0, "Null/blank field names should be ignored");
+
+ System.out.println("Null/blank field names test passed");
+ }
+
+ /**
+ * Test null and blank version handling
+ */
+ @Test
+ public void testNullAndBlankVersions() {
+ // Register field with null version (should be ignored)
+ FieldVersionRegistry.registerField("field1", null);
+
+ // Register field with blank version (should be ignored)
+ FieldVersionRegistry.registerField("field2", "");
+ FieldVersionRegistry.registerField("field3", " ");
+
+ Map allVersions = FieldVersionRegistry.getAllFieldVersions();
+ Assert.isTrue(allVersions.size() == 0, "Fields with null/blank versions should be ignored");
+
+ System.out.println("Null/blank versions test passed");
+ }
+
+ /**
+ * Test version trimming
+ */
+ @Test
+ public void testVersionTrimming() {
+ FieldVersionRegistry.registerField(" field1 ", " 2.0.0 ");
+
+ String version = FieldVersionRegistry.getFieldVersion("field1");
+ Assert.isTrue("2.0.0".equals(version), "Version should be trimmed");
+
+ System.out.println("Version trimming test passed");
+ }
+
+ /**
+ * Test thread-safety (ConcurrentHashMap behavior)
+ */
+ @Test
+ public void testConcurrentRegistration() throws InterruptedException {
+ final int threadCount = 10;
+ Thread[] threads = new Thread[threadCount];
+
+ for (int i = 0; i < threadCount; i++) {
+ final int index = i;
+ threads[i] = new Thread(() -> {
+ FieldVersionRegistry.registerField("field" + index, "2." + index + ".0");
+ });
+ threads[i].start();
+ }
+
+ for (Thread thread : threads) {
+ thread.join();
+ }
+
+ Map allVersions = FieldVersionRegistry.getAllFieldVersions();
+ Assert.isTrue(allVersions.size() == threadCount, "All threads should register successfully");
+
+ System.out.println("Concurrent registration test passed");
+ }
+}
diff --git a/infra/common/src/test/java/cn/hippo4j/common/toolkit/IncrementalContentUtilTest.java b/infra/common/src/test/java/cn/hippo4j/common/toolkit/IncrementalContentUtilTest.java
new file mode 100644
index 00000000..ef9e4890
--- /dev/null
+++ b/infra/common/src/test/java/cn/hippo4j/common/toolkit/IncrementalContentUtilTest.java
@@ -0,0 +1,264 @@
+/*
+ * 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.Before;
+import org.junit.Test;
+
+/**
+ * Test for incremental content util with version-aware filtering.
+ */
+public class IncrementalContentUtilTest {
+
+ @Before
+ public void setUp() {
+ // Clear and setup field version registry
+ FieldVersionRegistry.clearForTest();
+
+ // Register fields for testing (simulating FieldVersionInitializer)
+ FieldVersionRegistry.registerField("executeTimeOut", "2.0.0");
+ FieldVersionRegistry.registerField("isAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("capacityAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("livenessAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("allowCoreThreadTimeOut", "2.0.0");
+ }
+
+ /**
+ * Test that old clients (1.5.0) cannot see fields introduced in 2.0.0
+ */
+ @Test
+ public void testOldClientFilteringNewFields() {
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCoreSize(10);
+ param.setMaxSize(20);
+ param.setQueueType(1);
+ param.setCapacity(1024);
+ param.setKeepAliveTime(60L);
+ param.setRejectedType(1);
+ // 2.0.0 fields
+ param.setExecuteTimeOut(5000L);
+ param.setIsAlarm(1);
+ param.setCapacityAlarm(80);
+ param.setLivenessAlarm(80);
+ param.setAllowCoreThreadTimeOut(0);
+
+ // Test old client (1.5.0) - should NOT see 2.0.0 fields
+ String content15 = IncrementalContentUtil.getVersionedContent(param, "1.5.0");
+
+ // Assert: core fields should be present
+ Assert.isTrue(content15.contains("\"tenantId\":\"tenant-001\""), "Should contain tenantId");
+ Assert.isTrue(content15.contains("\"tpId\":\"test-pool\""), "Should contain tpId");
+ Assert.isTrue(content15.contains("\"coreSize\":10"), "Should contain coreSize");
+ Assert.isTrue(content15.contains("\"maxSize\":20"), "Should contain maxSize");
+
+ // Assert: 2.0.0 fields should NOT be present for 1.5.0 client
+ Assert.isTrue(!content15.contains("executeTimeOut"), "Client 1.5.0 should NOT see executeTimeOut");
+ Assert.isTrue(!content15.contains("isAlarm"), "Client 1.5.0 should NOT see isAlarm");
+ Assert.isTrue(!content15.contains("capacityAlarm"), "Client 1.5.0 should NOT see capacityAlarm");
+ Assert.isTrue(!content15.contains("livenessAlarm"), "Client 1.5.0 should NOT see livenessAlarm");
+ Assert.isTrue(!content15.contains("allowCoreThreadTimeOut"), "Client 1.5.0 should NOT see allowCoreThreadTimeOut");
+
+ System.out.println("Old client (1.5.0) content: " + content15);
+ }
+
+ /**
+ * Test that new clients (2.0.0) can see all fields including 2.0.0 fields
+ */
+ @Test
+ public void testNewClientSeeingAllFields() {
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCoreSize(10);
+ param.setMaxSize(20);
+ param.setQueueType(1);
+ param.setCapacity(1024);
+ param.setKeepAliveTime(60L);
+ param.setRejectedType(1);
+ // 2.0.0 fields
+ param.setExecuteTimeOut(5000L);
+ param.setIsAlarm(1);
+ param.setCapacityAlarm(80);
+ param.setLivenessAlarm(80);
+ param.setAllowCoreThreadTimeOut(0);
+
+ // Test new client (2.0.0) - should see ALL fields
+ String content20 = IncrementalContentUtil.getVersionedContent(param, "2.0.0");
+
+ // Assert: core fields should be present
+ Assert.isTrue(content20.contains("\"tenantId\":\"tenant-001\""), "Should contain tenantId");
+ Assert.isTrue(content20.contains("\"tpId\":\"test-pool\""), "Should contain tpId");
+ Assert.isTrue(content20.contains("\"coreSize\":10"), "Should contain coreSize");
+
+ // Assert: 2.0.0 fields SHOULD be present for 2.0.0 client
+ Assert.isTrue(content20.contains("executeTimeOut"), "Client 2.0.0 should see executeTimeOut");
+ Assert.isTrue(content20.contains("isAlarm"), "Client 2.0.0 should see isAlarm");
+ Assert.isTrue(content20.contains("capacityAlarm"), "Client 2.0.0 should see capacityAlarm");
+ Assert.isTrue(content20.contains("livenessAlarm"), "Client 2.0.0 should see livenessAlarm");
+ Assert.isTrue(content20.contains("allowCoreThreadTimeOut"), "Client 2.0.0 should see allowCoreThreadTimeOut");
+
+ System.out.println("New client (2.0.0) content: " + content20);
+ }
+
+ /**
+ * Test that UNKNOWN_VERSION clients (0.0.0) can see core fields but not 2.0.0 fields
+ */
+ @Test
+ public void testUnknownVersionClientSeesOnlyCoreFields() {
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCoreSize(10);
+ param.setMaxSize(20);
+ param.setExecuteTimeOut(5000L);
+
+ // Test UNKNOWN_VERSION client (0.0.0)
+ String contentUnknown = IncrementalContentUtil.getVersionedContent(param, null);
+
+ // Assert: should see core fields but NOT 2.0.0 fields (0.0.0 < 2.0.0)
+ Assert.isTrue(contentUnknown.contains("\"coreSize\":10"), "Should contain core field coreSize");
+ Assert.isTrue(!contentUnknown.contains("executeTimeOut"), "UNKNOWN_VERSION (0.0.0) should NOT see 2.0.0 fields");
+
+ System.out.println("UNKNOWN_VERSION client content: " + contentUnknown);
+ }
+
+ /**
+ * Test incremental version compatibility (2.1.0 adds new field)
+ */
+ @Test
+ public void testIncrementalVersionCompatibility() {
+ // Register a 2.1.0 field
+ FieldVersionRegistry.registerField("newFeatureField", "2.1.0");
+
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCoreSize(10);
+ param.setMaxSize(20);
+ param.setExecuteTimeOut(5000L); // 2.0.0 field
+
+ // Client 1.5.0: should only see core fields
+ String content15 = IncrementalContentUtil.getVersionedContent(param, "1.5.0");
+ Assert.isTrue(!content15.contains("executeTimeOut"), "1.5.0 should NOT see 2.0.0 fields");
+
+ // Client 2.0.0: should see core + 2.0.0 fields, but NOT 2.1.0 fields
+ String content20 = IncrementalContentUtil.getVersionedContent(param, "2.0.0");
+ Assert.isTrue(content20.contains("executeTimeOut"), "2.0.0 should see 2.0.0 fields");
+ Assert.isTrue(!content20.contains("newFeatureField"), "2.0.0 should NOT see 2.1.0 fields");
+
+ // Client 2.1.0: should see all fields
+ String content21 = IncrementalContentUtil.getVersionedContent(param, "2.1.0");
+ Assert.isTrue(content21.contains("executeTimeOut"), "2.1.0 should see 2.0.0 fields");
+
+ System.out.println("Incremental version test passed");
+ }
+
+ /**
+ * Test that core parameters are visible to all versions
+ */
+ @Test
+ public void testCoreParametersVisibleToAllVersions() {
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCoreSize(5);
+ param.setMaxSize(10);
+ param.setQueueType(2);
+ param.setCapacity(512);
+ param.setKeepAliveTime(30L);
+ param.setRejectedType(2);
+
+ // Test very old client (0.1.0)
+ String content01 = IncrementalContentUtil.getVersionedContent(param, "0.1.0");
+
+ // Assert: all core parameters should be visible
+ Assert.isTrue(content01.contains("\"tenantId\":\"tenant-001\""), "Core field tenantId visible to 0.1.0");
+ Assert.isTrue(content01.contains("\"itemId\":\"item-001\""), "Core field itemId visible to 0.1.0");
+ Assert.isTrue(content01.contains("\"tpId\":\"test-pool\""), "Core field tpId visible to 0.1.0");
+ Assert.isTrue(content01.contains("\"coreSize\":5"), "Core field coreSize visible to 0.1.0");
+ Assert.isTrue(content01.contains("\"maxSize\":10"), "Core field maxSize visible to 0.1.0");
+ Assert.isTrue(content01.contains("\"queueType\":2"), "Core field queueType visible to 0.1.0");
+ Assert.isTrue(content01.contains("\"capacity\":512"), "Core field capacity visible to 0.1.0");
+ Assert.isTrue(content01.contains("\"keepAliveTime\":30"), "Core field keepAliveTime visible to 0.1.0");
+ Assert.isTrue(content01.contains("\"rejectedType\":2"), "Core field rejectedType visible to 0.1.0");
+
+ System.out.println("Core parameters test passed for version 0.1.0");
+ }
+
+ /**
+ * Test field adapter compatibility (corePoolSize vs coreSize)
+ */
+ @Test
+ public void testFieldAdapterInVersionedContent() {
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCorePoolSize(15); // Use new field name
+ param.setMaximumPoolSize(30); // Use new field name
+
+ String content = IncrementalContentUtil.getVersionedContent(param, "1.5.0");
+
+ // Assert: should use old field names in output (via adapter)
+ Assert.isTrue(content.contains("\"coreSize\":15"), "Should use coreSize (old name) via adapter");
+ Assert.isTrue(content.contains("\"maxSize\":30"), "Should use maxSize (old name) via adapter");
+ Assert.isTrue(!content.contains("corePoolSize"), "Should NOT contain corePoolSize (new name)");
+ Assert.isTrue(!content.contains("maximumPoolSize"), "Should NOT contain maximumPoolSize (new name)");
+
+ System.out.println("Field adapter test passed: " + content);
+ }
+
+ /**
+ * Test null and empty version handling
+ */
+ @Test
+ public void testNullAndEmptyVersionHandling() {
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCoreSize(10);
+ param.setMaxSize(20);
+ param.setExecuteTimeOut(5000L);
+
+ // Test null version (should act as UNKNOWN_VERSION = 0.0.0, cannot see 2.0.0 fields)
+ String contentNull = IncrementalContentUtil.getVersionedContent(param, null);
+ Assert.isTrue(contentNull.contains("\"coreSize\":10"), "Null version should see core fields");
+ Assert.isTrue(!contentNull.contains("executeTimeOut"), "Null version (0.0.0) should NOT see 2.0.0 fields");
+
+ // Test empty version (should act as UNKNOWN_VERSION = 0.0.0)
+ String contentEmpty = IncrementalContentUtil.getVersionedContent(param, "");
+ Assert.isTrue(contentEmpty.contains("\"coreSize\":10"), "Empty version should see core fields");
+ Assert.isTrue(!contentEmpty.contains("executeTimeOut"), "Empty version (0.0.0) should NOT see 2.0.0 fields");
+
+ // Test blank version (should act as UNKNOWN_VERSION = 0.0.0)
+ String contentBlank = IncrementalContentUtil.getVersionedContent(param, " ");
+ Assert.isTrue(contentBlank.contains("\"coreSize\":10"), "Blank version should see core fields");
+ Assert.isTrue(!contentBlank.contains("executeTimeOut"), "Blank version (0.0.0) should NOT see 2.0.0 fields");
+
+ System.out.println("Null/empty/blank version test passed");
+ }
+}
diff --git a/infra/common/src/test/java/cn/hippo4j/common/toolkit/MentorQuestionVerificationTest.java b/infra/common/src/test/java/cn/hippo4j/common/toolkit/MentorQuestionVerificationTest.java
new file mode 100644
index 00000000..cb6d6a25
--- /dev/null
+++ b/infra/common/src/test/java/cn/hippo4j/common/toolkit/MentorQuestionVerificationTest.java
@@ -0,0 +1,299 @@
+/*
+ * 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.Before;
+import org.junit.Test;
+
+/**
+ * Verification test for mentor's two key questions:
+ * Q1: How to combine multiple fields in a single version for comparison?
+ * Q2: How to compare when Server is multiple versions ahead of Client?
+ */
+public class MentorQuestionVerificationTest {
+
+ @Before
+ public void setUp() {
+ // Clear registry and set up test environment
+ FieldVersionRegistry.clearForTest();
+
+ // Simulate FieldVersionInitializer - Register fields for different versions
+ // Version 1.5.0: Initial release (only core fields)
+
+ // Version 2.0.0: Add 5 new fields (simulating single version with multiple fields)
+ FieldVersionRegistry.registerField("executeTimeOut", "2.0.0");
+ FieldVersionRegistry.registerField("isAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("capacityAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("livenessAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("allowCoreThreadTimeOut", "2.0.0");
+
+ // Version 2.1.0: Add hypothetical fields (for demonstration)
+ FieldVersionRegistry.registerField("futureField21A", "2.1.0");
+ FieldVersionRegistry.registerField("futureField21B", "2.1.0");
+
+ // Version 2.2.0: Add hypothetical fields (for demonstration)
+ FieldVersionRegistry.registerField("futureField22A", "2.2.0");
+ FieldVersionRegistry.registerField("futureField22B", "2.2.0");
+ FieldVersionRegistry.registerField("futureField22C", "2.2.0");
+ }
+
+ /**
+ * Q1 Verification: Single version (2.0.0) introduces 5 fields - how to combine for comparison?
+ *
+ * Expected behavior:
+ * - Client 1.5.0: Cannot see any 2.0.0 fields (all 5 filtered out)
+ * - Client 2.0.0: Can see all 5 fields (all 5 included)
+ * - All 5 fields are treated equally (no precedence/priority among them)
+ */
+ @Test
+ public void testQ1_MultipleFieldsInSingleVersion() {
+ System.out.println("\n========== Q1 Verification: Multiple Fields in Single Version ==========");
+
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ // Core fields
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCoreSize(10);
+ param.setMaxSize(20);
+ param.setQueueType(1);
+ param.setCapacity(1024);
+ param.setKeepAliveTime(60L);
+ param.setRejectedType(1);
+
+ // Version 2.0.0 fields (5 fields introduced together)
+ param.setExecuteTimeOut(5000L);
+ param.setIsAlarm(1);
+ param.setCapacityAlarm(80);
+ param.setLivenessAlarm(80);
+ param.setAllowCoreThreadTimeOut(0);
+
+ // Test 1: Client 1.5.0 - should NOT see any 2.0.0 fields
+ String content15 = IncrementalContentUtil.getVersionedContent(param, "1.5.0");
+ System.out.println("\n[Client 1.5.0] Content:");
+ System.out.println(content15);
+
+ Assert.isTrue(!content15.contains("executeTimeOut"), "1.5.0 should NOT see executeTimeOut");
+ Assert.isTrue(!content15.contains("isAlarm"), "1.5.0 should NOT see isAlarm");
+ Assert.isTrue(!content15.contains("capacityAlarm"), "1.5.0 should NOT see capacityAlarm");
+ Assert.isTrue(!content15.contains("livenessAlarm"), "1.5.0 should NOT see livenessAlarm");
+ Assert.isTrue(!content15.contains("allowCoreThreadTimeOut"), "1.5.0 should NOT see allowCoreThreadTimeOut");
+
+ // Test 2: Client 2.0.0 - should see ALL 5 fields
+ String content20 = IncrementalContentUtil.getVersionedContent(param, "2.0.0");
+ System.out.println("\n[Client 2.0.0] Content:");
+ System.out.println(content20);
+
+ Assert.isTrue(content20.contains("executeTimeOut"), "2.0.0 should see executeTimeOut");
+ Assert.isTrue(content20.contains("isAlarm"), "2.0.0 should see isAlarm");
+ Assert.isTrue(content20.contains("capacityAlarm"), "2.0.0 should see capacityAlarm");
+ Assert.isTrue(content20.contains("livenessAlarm"), "2.0.0 should see livenessAlarm");
+ Assert.isTrue(content20.contains("allowCoreThreadTimeOut"), "2.0.0 should see allowCoreThreadTimeOut");
+
+ // Verify MD5 difference
+ String md5_15 = Md5Util.md5Hex(content15, "UTF-8");
+ String md5_20 = Md5Util.md5Hex(content20, "UTF-8");
+
+ System.out.println("\n[MD5 Comparison]:");
+ System.out.println(" Client 1.5.0 MD5: " + md5_15);
+ System.out.println(" Client 2.0.0 MD5: " + md5_20);
+ System.out.println(" MD5 Different: " + !md5_15.equals(md5_20));
+
+ Assert.isTrue(!md5_15.equals(md5_20), "MD5 should be different for different client versions");
+
+ System.out.println("\n✅ Q1 Answer: Multiple fields in a single version are combined by:");
+ System.out.println(" 1. Each field has the SAME minimum version requirement (2.0.0)");
+ System.out.println(" 2. Client version comparison: clientVersion >= fieldVersion");
+ System.out.println(" 3. ALL fields pass/fail the version check TOGETHER");
+ System.out.println(" 4. Filtered content generates different MD5 for different client versions");
+ }
+
+ /**
+ * Q2 Verification: Server is multiple versions ahead (Server 2.2.0, Client 1.5.0)
+ *
+ * Expected behavior:
+ * - Client 1.5.0 skips 2.0.0, 2.1.0, 2.2.0 fields (跨3个版本)
+ * - Client 2.0.0 sees 2.0.0 fields but skips 2.1.0, 2.2.0 fields (跨2个版本)
+ * - Client 2.1.0 sees 2.0.0 + 2.1.0 fields but skips 2.2.0 fields (跨1个版本)
+ * - Client 2.2.0 sees all fields (同版本)
+ */
+ @Test
+ public void testQ2_ServerMultipleVersionsAhead() {
+ System.out.println("\n========== Q2 Verification: Server Multiple Versions Ahead ==========");
+
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ // Core fields
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCoreSize(10);
+ param.setMaxSize(20);
+
+ // Version 2.0.0 fields (5 fields)
+ param.setExecuteTimeOut(5000L);
+ param.setIsAlarm(1);
+ param.setCapacityAlarm(80);
+ param.setLivenessAlarm(80);
+ param.setAllowCoreThreadTimeOut(0);
+
+ // Note: Version 2.1.0 and 2.2.0 fields are hypothetical (registered but not on param object)
+ // The test demonstrates version filtering logic without actual field values
+
+ System.out.println("\n[Scenario] Server Version: 2.2.0 (has 2.0.0 + 2.1.0 + 2.2.0 fields)");
+ System.out.println(" Testing clients: 1.5.0, 2.0.0, 2.1.0, 2.2.0\n");
+
+ // Test 1: Client 1.5.0 (跨3个版本 - skips 2.0.0, 2.1.0, 2.2.0)
+ String content15 = IncrementalContentUtil.getVersionedContent(param, "1.5.0");
+ System.out.println("[Client 1.5.0] (3 versions behind)");
+ System.out.println(" Content: " + content15);
+ System.out.println(" Field Count: " + countFields(content15));
+
+ Assert.isTrue(!content15.contains("executeTimeOut"), "1.5.0 should NOT see 2.0.0 fields");
+
+ // Test 2: Client 2.0.0 (跨2个版本 - sees 2.0.0, skips hypothetical 2.1.0, 2.2.0)
+ String content20 = IncrementalContentUtil.getVersionedContent(param, "2.0.0");
+ System.out.println("\n[Client 2.0.0] (2 versions behind)");
+ System.out.println(" Content: " + content20);
+ System.out.println(" Field Count: " + countFields(content20));
+
+ Assert.isTrue(content20.contains("executeTimeOut"), "2.0.0 should see 2.0.0 fields");
+
+ // Test 3: Client 2.1.0 (跨1个版本 - sees 2.0.0, skips hypothetical 2.2.0)
+ String content21 = IncrementalContentUtil.getVersionedContent(param, "2.1.0");
+ System.out.println("\n[Client 2.1.0] (1 version behind)");
+ System.out.println(" Content: " + content21);
+ System.out.println(" Field Count: " + countFields(content21));
+
+ Assert.isTrue(content21.contains("executeTimeOut"), "2.1.0 should see 2.0.0 fields");
+
+ // Test 4: Client 2.2.0 (同版本 - sees all actual fields)
+ String content22 = IncrementalContentUtil.getVersionedContent(param, "2.2.0");
+ System.out.println("\n[Client 2.2.0] (same version)");
+ System.out.println(" Content: " + content22);
+ System.out.println(" Field Count: " + countFields(content22));
+
+ Assert.isTrue(content22.contains("executeTimeOut"), "2.2.0 should see 2.0.0 fields");
+
+ // Verify MD5 progression
+ String md5_15 = Md5Util.md5Hex(content15, "UTF-8");
+ String md5_20 = Md5Util.md5Hex(content20, "UTF-8");
+ String md5_21 = Md5Util.md5Hex(content21, "UTF-8");
+ String md5_22 = Md5Util.md5Hex(content22, "UTF-8");
+
+ System.out.println("\n[MD5 Comparison Across Versions]:");
+ System.out.println(" Client 1.5.0 MD5: " + md5_15);
+ System.out.println(" Client 2.0.0 MD5: " + md5_20 + " (different: " + !md5_15.equals(md5_20) + ")");
+ System.out.println(" Client 2.1.0 MD5: " + md5_21 + " (different: " + !md5_20.equals(md5_21) + ")");
+ System.out.println(" Client 2.2.0 MD5: " + md5_22 + " (different: " + !md5_21.equals(md5_22) + ")");
+
+ Assert.isTrue(!md5_15.equals(md5_20), "1.5.0 and 2.0.0 should have different MD5");
+ // Note: md5_20, md5_21, md5_22 are same because we don't have actual 2.1.0/2.2.0 fields in param
+ // In production with real fields, each version would have different MD5
+
+ System.out.println("\n✅ Q2 Answer: When Server is multiple versions ahead:");
+ System.out.println(" 1. Each field independently checks: clientVersion >= fieldIntroducedVersion");
+ System.out.println(" 2. Incremental visibility: Client sees all fields from its version and below");
+ System.out.println(" 3. Transitive compatibility: 1.5.0 → 2.0.0 → 2.1.0 → 2.2.0 forms a version chain");
+ System.out.println(" 4. Each client gets a stable, version-appropriate MD5");
+ System.out.println(" 5. No 'skip version' issue - comparison is per-field, not per-version");
+ }
+
+ /**
+ * Edge case: Client version between two server versions (e.g., Client 2.0.5)
+ */
+ @Test
+ public void testEdgeCase_ClientBetweenServerVersions() {
+ System.out.println("\n========== Edge Case: Client Between Server Versions ==========");
+
+ ThreadPoolParameterInfo param = new ThreadPoolParameterInfo();
+ param.setTenantId("tenant-001");
+ param.setItemId("item-001");
+ param.setTpId("test-pool");
+ param.setCoreSize(10);
+
+ param.setExecuteTimeOut(5000L); // 2.0.0
+
+ // Client 2.0.5 (between 2.0.0 and 2.1.0)
+ String content205 = IncrementalContentUtil.getVersionedContent(param, "2.0.5");
+ System.out.println("\n[Client 2.0.5] (between 2.0.0 and 2.1.0)");
+ System.out.println(" Content: " + content205);
+
+ // 2.0.5 >= 2.0.0 → should see executeTimeOut
+ Assert.isTrue(content205.contains("executeTimeOut"), "2.0.5 >= 2.0.0, should see 2.0.0 fields");
+
+ System.out.println("\n✅ Edge Case Handled: Semantic version comparison ensures correct filtering");
+ }
+
+ /**
+ * Real-world scenario: Configuration update from old client
+ */
+ @Test
+ public void testRealWorld_OldClientUpdatesConfig() {
+ System.out.println("\n========== Real-World Scenario: Old Client Updates Config ==========");
+
+ // Scenario: Server 2.2.0, Client 1.5.0 calls save_or_update
+ ThreadPoolParameterInfo paramFromClient = new ThreadPoolParameterInfo();
+ paramFromClient.setTenantId("tenant-001");
+ paramFromClient.setItemId("item-001");
+ paramFromClient.setTpId("test-pool");
+ paramFromClient.setCoreSize(15); // Client only knows about core fields
+ paramFromClient.setMaxSize(30);
+
+ // Server has additional fields from newer versions (which client doesn't send)
+ ThreadPoolParameterInfo paramOnServer = new ThreadPoolParameterInfo();
+ paramOnServer.setTenantId("tenant-001");
+ paramOnServer.setItemId("item-001");
+ paramOnServer.setTpId("test-pool");
+ paramOnServer.setCoreSize(15);
+ paramOnServer.setMaxSize(30);
+ paramOnServer.setExecuteTimeOut(5000L); // Server 2.0.0 field
+ // Note: Hypothetical 2.1.0 and 2.2.0 fields are registered but not on param object
+
+ // Generate MD5 for Client 1.5.0 view
+ String clientContent = IncrementalContentUtil.getVersionedContent(paramFromClient, "1.5.0");
+ String serverContentForClient15 = IncrementalContentUtil.getVersionedContent(paramOnServer, "1.5.0");
+
+ String clientMd5 = Md5Util.md5Hex(clientContent, "UTF-8");
+ String serverMd5 = Md5Util.md5Hex(serverContentForClient15, "UTF-8");
+
+ System.out.println("\n[Client 1.5.0 View]:");
+ System.out.println(" Client sent content: " + clientContent);
+ System.out.println(" Server content (filtered for 1.5.0): " + serverContentForClient15);
+ System.out.println(" Client MD5: " + clientMd5);
+ System.out.println(" Server MD5 (for 1.5.0): " + serverMd5);
+ System.out.println(" MD5 Match: " + clientMd5.equals(serverMd5));
+
+ Assert.isTrue(clientMd5.equals(serverMd5), "Client and Server MD5 should match when viewed through same version lens");
+
+ System.out.println("\n✅ Real-World Scenario Verified:");
+ System.out.println(" - Old client updates config → no invalid refresh triggered");
+ System.out.println(" - Server's newer fields are invisible to old client");
+ System.out.println(" - MD5 comparison is version-aware and stable");
+ }
+
+ private int countFields(String jsonContent) {
+ int count = 0;
+ for (char c : jsonContent.toCharArray()) {
+ if (c == ':') {
+ count++;
+ }
+ }
+ return count;
+ }
+}
diff --git a/infra/common/src/test/java/cn/hippo4j/common/toolkit/ProtocolRigidityTest.java b/infra/common/src/test/java/cn/hippo4j/common/toolkit/ProtocolRigidityTest.java
deleted file mode 100644
index ec7e502d..00000000
--- a/infra/common/src/test/java/cn/hippo4j/common/toolkit/ProtocolRigidityTest.java
+++ /dev/null
@@ -1,206 +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;
-
-/**
- * Protocol Rigidity Test: Verifies that field renaming does not trigger unnecessary refreshes.
- */
-public class ProtocolRigidityTest {
-
- /**
- * Scenario 1: Client uses old field names (coreSize/maxSize), Server uses new field names (corePoolSize/maximumPoolSize).
- * Expected Result: MD5 should be the same, no refresh triggered.
- */
- @Test
- public void testFieldRenamingCompatibility_OldClientNewServer() {
- System.out.println("========== Scenario 1: Field Renaming Compatibility (Old Client vs New Server) ==========");
-
- // Simulate Client configuration (using old field names)
- ThreadPoolParameterInfo clientConfig = new ThreadPoolParameterInfo();
- clientConfig.setTenantId("default");
- clientConfig.setItemId("item-001");
- clientConfig.setTpId("test-pool");
- clientConfig.setCoreSize(10); // old field
- clientConfig.setMaxSize(20); // old field
- clientConfig.setQueueType(2);
- clientConfig.setCapacity(1024);
- clientConfig.setKeepAliveTime(60L);
- clientConfig.setRejectedType(1);
- clientConfig.setAllowCoreThreadTimeOut(0);
-
- // Simulate Server configuration (using new field names)
- ThreadPoolParameterInfo serverConfig = new ThreadPoolParameterInfo();
- serverConfig.setTenantId("default");
- serverConfig.setItemId("item-001");
- serverConfig.setTpId("test-pool");
- serverConfig.setCorePoolSize(10); // new field
- serverConfig.setMaximumPoolSize(20); // new field
- serverConfig.setQueueType(2);
- serverConfig.setCapacity(1024);
- serverConfig.setKeepAliveTime(60L);
- serverConfig.setRejectedType(1);
- serverConfig.setAllowCoreThreadTimeOut(0);
-
- // Client-side (v2 protocol) incremental MD5
- String clientContent = IncrementalContentUtil.getCoreContent(clientConfig);
- String clientMd5 = Md5Util.md5Hex(clientContent, "UTF-8");
-
- // Server-side (v2 protocol) incremental MD5
- String serverContent = IncrementalContentUtil.getCoreContent(serverConfig);
- String serverMd5 = Md5Util.md5Hex(serverContent, "UTF-8");
-
- System.out.println("Client config (old fields): coreSize=" + clientConfig.getCoreSize() + ", maxSize=" + clientConfig.getMaxSize());
- System.out.println("Server config (new fields): corePoolSize=" + serverConfig.getCorePoolSize() + ", maximumPoolSize=" + serverConfig.getMaximumPoolSize());
- System.out.println("Client incremental content: " + clientContent);
- System.out.println("Server incremental content: " + serverContent);
- System.out.println("Client MD5: " + clientMd5);
- System.out.println("Server MD5: " + serverMd5);
-
- // Assertion: Even with different field names, MD5 should be the same (adapter unifies fields)
- Assert.assertEquals("MD5 should be the same after field renaming, no refresh triggered", serverMd5, clientMd5);
- System.out.println("Test passed: Field renaming does not trigger unnecessary refresh");
- }
-
- /**
- * Scenario 2: Client and Server both use new field names.
- * Expected Result: MD5 should be the same.
- */
- @Test
- public void testNewFieldNamesConsistency() {
- System.out.println("\n========== Scenario 2: New Field Name Consistency ==========");
-
- // Both Client and Server use new field names
- ThreadPoolParameterInfo clientConfig = new ThreadPoolParameterInfo();
- clientConfig.setTenantId("default");
- clientConfig.setItemId("item-001");
- clientConfig.setTpId("test-pool");
- clientConfig.setCorePoolSize(15);
- clientConfig.setMaximumPoolSize(30);
- clientConfig.setQueueType(3);
- clientConfig.setCapacity(2048);
-
- ThreadPoolParameterInfo serverConfig = new ThreadPoolParameterInfo();
- serverConfig.setTenantId("default");
- serverConfig.setItemId("item-001");
- serverConfig.setTpId("test-pool");
- serverConfig.setCorePoolSize(15);
- serverConfig.setMaximumPoolSize(30);
- serverConfig.setQueueType(3);
- serverConfig.setCapacity(2048);
-
- String clientMd5 = Md5Util.md5Hex(IncrementalContentUtil.getCoreContent(clientConfig), "UTF-8");
- String serverMd5 = Md5Util.md5Hex(IncrementalContentUtil.getCoreContent(serverConfig), "UTF-8");
-
- System.out.println("Client MD5: " + clientMd5);
- System.out.println("Server MD5: " + serverMd5);
-
- Assert.assertEquals("MD5 should be the same when new field names are consistent", serverMd5, clientMd5);
- System.out.println("Test passed: New field names produce consistent MD5");
- }
-
- /**
- * Scenario 3: Both old and new fields exist, new fields should take priority.
- * Expected Result: Adapter returns new field values.
- */
- @Test
- public void testFieldAdapterPriority() {
- 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.setMaximumPoolSize(30); // new field (should take priority)
-
- Integer adaptedCore = config.corePoolSizeAdapt();
- Integer adaptedMax = config.maximumPoolSizeAdapt();
-
- System.out.println("Old field values: coreSize=" + config.getCoreSize() + ", maxSize=" + config.getMaxSize());
- System.out.println("New field values: corePoolSize=" + config.getCorePoolSize() + ", maximumPoolSize=" + config.getMaximumPoolSize());
- System.out.println("Adapter returned values: core=" + adaptedCore + ", max=" + adaptedMax);
-
- Assert.assertEquals("Adapter should return new field value first", Integer.valueOf(15), adaptedCore);
- Assert.assertEquals("Adapter should return new field value first", Integer.valueOf(30), adaptedMax);
- System.out.println("Test passed: Adapter correctly prioritizes new fields");
- }
-
- /**
- * Scenario 4: Only old fields exist, adapter should correctly return old values.
- */
- @Test
- public void testFieldAdapterFallback() {
- System.out.println("\n========== Scenario 4: Field Adapter Fallback ==========");
-
- ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
- config.setCoreSize(10); // only old fields
- config.setMaxSize(20);
-
- Integer adaptedCore = config.corePoolSizeAdapt();
- Integer adaptedMax = config.maximumPoolSizeAdapt();
-
- System.out.println("Old field values: coreSize=" + config.getCoreSize() + ", maxSize=" + config.getMaxSize());
- System.out.println("New field values: corePoolSize=" + config.getCorePoolSize() + ", maximumPoolSize=" + config.getMaximumPoolSize());
- System.out.println("Adapter returned values: core=" + adaptedCore + ", max=" + adaptedMax);
-
- Assert.assertEquals("Adapter should fall back to old field value", Integer.valueOf(10), adaptedCore);
- Assert.assertEquals("Adapter should fall back to old field value", Integer.valueOf(20), adaptedMax);
- System.out.println("Test passed: Adapter correctly falls back to old fields");
- }
-
- /**
- * Scenario 5: v1 client (full MD5) vs v2 client (incremental MD5).
- * Expected Result: v1 and v2 use different comparison strategies.
- */
- @Test
- public void testProtocolVersionDifference() {
- System.out.println("\n========== Scenario 5: Protocol Version Difference ==========");
-
- ThreadPoolParameterInfo config = new ThreadPoolParameterInfo();
- config.setTenantId("default");
- config.setItemId("item-001");
- config.setTpId("test-pool");
- config.setCorePoolSize(10);
- config.setMaximumPoolSize(20);
- config.setQueueType(2);
- config.setCapacity(1024);
- config.setExecuteTimeOut(5000L); // extended parameter
- config.setIsAlarm(1); // extended parameter
-
- // v1 protocol: full MD5
- String v1Content = IncrementalContentUtil.getFullContent(config);
- String v1Md5 = Md5Util.md5Hex(v1Content, "UTF-8");
-
- // v2 protocol: incremental MD5 (core parameters only)
- String v2Content = IncrementalContentUtil.getCoreContent(config);
- String v2Md5 = Md5Util.md5Hex(v2Content, "UTF-8");
-
- System.out.println("v1 protocol (full) content length: " + v1Content.length());
- System.out.println("v2 protocol (incremental) content length: " + v2Content.length());
- System.out.println("v1 MD5: " + v1Md5);
- System.out.println("v2 MD5: " + v2Md5);
- System.out.println("Does v2 content contain executeTimeOut: " + v2Content.contains("executeTimeOut"));
-
- Assert.assertNotEquals("MD5 should differ between v1 and v2 protocols", v1Md5, v2Md5);
- Assert.assertFalse("v2 incremental content should not include extended parameters", v2Content.contains("executeTimeOut"));
- System.out.println("Test passed: v1 and v2 protocols use different strategies");
- }
-}
diff --git a/threadpool/server/config/src/main/java/cn/hippo4j/config/init/FieldVersionInitializer.java b/threadpool/server/config/src/main/java/cn/hippo4j/config/init/FieldVersionInitializer.java
new file mode 100644
index 00000000..baae932e
--- /dev/null
+++ b/threadpool/server/config/src/main/java/cn/hippo4j/config/init/FieldVersionInitializer.java
@@ -0,0 +1,53 @@
+/*
+ * 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.init;
+
+import cn.hippo4j.common.toolkit.FieldVersionRegistry;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * Initialize field version registry at server startup.
+ * Pre-registers known fields with their introduction versions.
+ */
+@Slf4j
+@Component
+@Order(Integer.MIN_VALUE)
+public class FieldVersionInitializer implements ApplicationRunner {
+
+ @Override
+ public void run(ApplicationArguments args) {
+ log.info("Initializing field version registry...");
+
+ // Register fields introduced in version 2.0.0
+ FieldVersionRegistry.registerField("executeTimeOut", "2.0.0");
+ FieldVersionRegistry.registerField("isAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("capacityAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("livenessAlarm", "2.0.0");
+ FieldVersionRegistry.registerField("allowCoreThreadTimeOut", "2.0.0");
+
+ // Register fields for future versions here:
+ // FieldVersionRegistry.registerField("newField", "2.1.0");
+
+ log.info("Field version registry initialized with {} fields",
+ FieldVersionRegistry.getAllFieldVersions().size());
+ }
+}
diff --git a/threadpool/server/config/src/main/java/cn/hippo4j/config/model/CacheItem.java b/threadpool/server/config/src/main/java/cn/hippo4j/config/model/CacheItem.java
index 21fcb8c8..be1acf4b 100644
--- a/threadpool/server/config/src/main/java/cn/hippo4j/config/model/CacheItem.java
+++ b/threadpool/server/config/src/main/java/cn/hippo4j/config/model/CacheItem.java
@@ -64,9 +64,16 @@ public class CacheItem {
this.versionMd5Cache.put(VersionUtil.UNKNOWN_VERSION, this.md5);
}
+ /**
+ * Get MD5 for specific client version.
+ * Returns cached version-specific MD5 if available, or null to trigger recalculation.
+ *
+ * @param clientVersion client semantic version
+ * @return version-specific MD5, or null if not cached (caller should recalculate)
+ */
public String getMd5(String clientVersion) {
String key = normalizeVersionKey(clientVersion);
- return versionMd5Cache.getOrDefault(key, md5);
+ return versionMd5Cache.get(key);
}
public void setMd5(String clientVersion, String value) {
diff --git a/threadpool/server/config/src/main/java/cn/hippo4j/config/model/ConfigInfoBase.java b/threadpool/server/config/src/main/java/cn/hippo4j/config/model/ConfigInfoBase.java
index 47e8dcad..08612edd 100644
--- a/threadpool/server/config/src/main/java/cn/hippo4j/config/model/ConfigInfoBase.java
+++ b/threadpool/server/config/src/main/java/cn/hippo4j/config/model/ConfigInfoBase.java
@@ -17,7 +17,6 @@
package cn.hippo4j.config.model;
-import cn.hippo4j.common.model.IncrementalFieldMetadataProvider;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
@@ -27,13 +26,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.io.Serializable;
-import java.util.Map;
/**
* Config info base.
*/
@Data
-public class ConfigInfoBase implements Serializable, IncrementalFieldMetadataProvider {
+public class ConfigInfoBase implements Serializable {
private static final long serialVersionUID = -1892597426099265730L;
@@ -127,16 +125,4 @@ public class ConfigInfoBase implements Serializable, IncrementalFieldMetadataPro
*/
@JsonIgnore
private String content;
-
- /**
- * Field-to-minimum-version mapping (transient field).
- */
- @TableField(exist = false)
- private Map fieldVersionMetadata;
-
- /**
- * Metadata version identifier (transient field).
- */
- @TableField(exist = false)
- private String fieldMetadataVersion;
}