From c160b090d50de99ecb7b596ec839169268269689 Mon Sep 17 00:00:00 2001 From: mingri31164 <3116430062@qq.com> Date: Tue, 4 Nov 2025 05:22:35 +0800 Subject: [PATCH] Complete multi-version protocol via field version registry --- .../IncrementalFieldMetadataProvider.java | 50 -- .../common/model/ThreadPoolParameterInfo.java | 13 +- .../hippo4j/common/toolkit/ContentUtil.java | 12 - .../common/toolkit/FieldVersionRegistry.java | 107 +++++ .../toolkit/IncrementalContentUtil.java | 277 +++-------- .../common/toolkit/IncrementalMd5Util.java | 75 --- .../hippo4j/common/toolkit/VersionUtil.java | 3 - .../common/toolkit/ExtensibilityTest.java | 285 ----------- .../toolkit/FieldVersionControlTest.java | 443 ------------------ .../toolkit/FieldVersionRegistryTest.java | 239 ++++++++++ .../toolkit/IncrementalContentUtilTest.java | 264 +++++++++++ .../MentorQuestionVerificationTest.java | 299 ++++++++++++ .../common/toolkit/ProtocolRigidityTest.java | 206 -------- .../config/init/FieldVersionInitializer.java | 53 +++ .../cn/hippo4j/config/model/CacheItem.java | 9 +- .../hippo4j/config/model/ConfigInfoBase.java | 16 +- 16 files changed, 1030 insertions(+), 1321 deletions(-) delete mode 100644 infra/common/src/main/java/cn/hippo4j/common/model/IncrementalFieldMetadataProvider.java create mode 100644 infra/common/src/main/java/cn/hippo4j/common/toolkit/FieldVersionRegistry.java delete mode 100644 infra/common/src/test/java/cn/hippo4j/common/toolkit/ExtensibilityTest.java delete mode 100644 infra/common/src/test/java/cn/hippo4j/common/toolkit/FieldVersionControlTest.java create mode 100644 infra/common/src/test/java/cn/hippo4j/common/toolkit/FieldVersionRegistryTest.java create mode 100644 infra/common/src/test/java/cn/hippo4j/common/toolkit/IncrementalContentUtilTest.java create mode 100644 infra/common/src/test/java/cn/hippo4j/common/toolkit/MentorQuestionVerificationTest.java delete mode 100644 infra/common/src/test/java/cn/hippo4j/common/toolkit/ProtocolRigidityTest.java create mode 100644 threadpool/server/config/src/main/java/cn/hippo4j/config/init/FieldVersionInitializer.java 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; }