mirror of https://github.com/longtai-cn/hippo4j
feat: Implement incremental update protocol with multi-version compatibility (#1611)
* Add incremental protocol comparison tool * Add protocol version on the server * Add protocol version on the client * Add protocol upgrade test code * Remove the commented test method * Centralize protocol version constant * Extract helper methods for coreSize and maxSize * Add cross-version compatibility tests for incremental protocol * Fix extended parameter loss in config refresh due to protocol version misuse * Eliminate hardcoded protocol version and implement field-level version control * Support semantic version-aware incremental md5 comparison * Remove protocol version mechanismdevelop
parent
9aa3be0750
commit
565c3f7a95
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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<String, String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,327 @@
|
|||||||
|
/*
|
||||||
|
* 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.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;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incremental content util for thread pool parameter comparison.
|
||||||
|
* Supports version compatibility and incremental updates.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class IncrementalContentUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core parameters that affect thread pool behavior
|
||||||
|
*/
|
||||||
|
private static final String[] CORE_PARAMETERS = {
|
||||||
|
"coreSize", "maxSize", "queueType", "capacity",
|
||||||
|
"keepAliveTime", "rejectedType", "allowCoreThreadTimeOut"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final List<String> IDENTIFIER_FIELDS = Collections.unmodifiableList(Arrays.asList("tenantId", "itemId", "tpId"));
|
||||||
|
|
||||||
|
private static final List<String> CORE_PARAMETER_LIST = Collections.unmodifiableList(Arrays.asList(CORE_PARAMETERS));
|
||||||
|
|
||||||
|
private static final 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)
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
public static String getVersionedContent(ThreadPoolParameter parameter, String clientVersion) {
|
||||||
|
String fullContent = getFullContent(parameter);
|
||||||
|
LinkedHashMap<String, Object> raw = JSONUtil.parseObject(fullContent, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
if (raw == null) {
|
||||||
|
return fullContent;
|
||||||
|
}
|
||||||
|
String normalizedClientVersion = StringUtil.isNotBlank(clientVersion)
|
||||||
|
? clientVersion.trim()
|
||||||
|
: VersionUtil.UNKNOWN_VERSION;
|
||||||
|
Map<String, String> fieldRules = resolveFieldRules(parameter, raw);
|
||||||
|
LinkedHashMap<String, Object> filtered = new LinkedHashMap<>();
|
||||||
|
for (String field : IDENTIFIER_FIELDS) {
|
||||||
|
if (raw.containsKey(field)) {
|
||||||
|
filtered.put(field, raw.get(field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String field : CORE_PARAMETER_LIST) {
|
||||||
|
if (raw.containsKey(field)) {
|
||||||
|
filtered.put(field, raw.get(field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw.forEach((field, value) -> {
|
||||||
|
if (!filtered.containsKey(field) && shouldIncludeField(field, normalizedClientVersion, fieldRules)) {
|
||||||
|
filtered.put(field, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return JSONUtil.toJSONString(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<String, Object> getChangesSummary(ThreadPoolParameter oldParameter, ThreadPoolParameter newParameter) {
|
||||||
|
Map<String, Object> 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.
|
||||||
|
*/
|
||||||
|
private static boolean shouldIncludeField(String field, String clientVersion, Map<String, String> fieldRules) {
|
||||||
|
String minVersion = fieldRules.get(field);
|
||||||
|
if (StringUtil.isBlank(minVersion)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String effectiveClientVersion = StringUtil.isBlank(clientVersion) ? VersionUtil.UNKNOWN_VERSION : clientVersion;
|
||||||
|
return VersionUtil.isVersionGreaterOrEqual(effectiveClientVersion, minVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
private static Map<String, String> resolveFieldRules(ThreadPoolParameter parameter, Map<String, Object> raw) {
|
||||||
|
Map<String, String> fieldRules = new LinkedHashMap<>();
|
||||||
|
// Identifier and core fields visible to all clients (since version 1.0.0)
|
||||||
|
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));
|
||||||
|
|
||||||
|
// 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
|
||||||
|
raw.keySet().forEach(field -> {
|
||||||
|
if (!fieldRules.containsKey(field)) {
|
||||||
|
fieldRules.put(field, "2.0.0"); // Default: visible to clients >= 2.0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fieldRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param parameter thread pool parameter
|
||||||
|
* @return field-to-version mapping, or empty map if not available
|
||||||
|
*/
|
||||||
|
private static Map<String, String> extractMetadataFromParameter(ThreadPoolParameter parameter) {
|
||||||
|
if (parameter instanceof IncrementalFieldMetadataProvider) {
|
||||||
|
Map<String, String> metadata = ((IncrementalFieldMetadataProvider) parameter).getFieldVersionMetadata();
|
||||||
|
if (metadata != null && !metadata.isEmpty()) {
|
||||||
|
Map<String, String> copied = new LinkedHashMap<>();
|
||||||
|
metadata.forEach((field, version) -> {
|
||||||
|
if (StringUtil.isNotBlank(field) && StringUtil.isNotBlank(version)) {
|
||||||
|
copied.put(field, version.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return copied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<String, String> extractMetadataFromPayload(Map<String, Object> raw) {
|
||||||
|
Object metadataObject = raw.remove(FIELD_VERSION_METADATA_KEY);
|
||||||
|
raw.remove(FIELD_METADATA_VERSION_KEY);
|
||||||
|
if (metadataObject instanceof Map<?, ?>) {
|
||||||
|
Map<String, String> 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<String, String> target, Map<String, String> additions) {
|
||||||
|
if (additions == null || additions.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
additions.forEach((field, version) -> {
|
||||||
|
if (StringUtil.isNotBlank(field) && StringUtil.isNotBlank(version)) {
|
||||||
|
target.put(field, version.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* 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.ThreadPoolParameter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incremental MD5 util for thread pool parameter comparison.
|
||||||
|
* Supports version compatibility and reduces unnecessary refreshes.
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* @param config thread pool parameter
|
||||||
|
* @param clientVersion semantic client version string (can be blank)
|
||||||
|
* @return versioned MD5 hash
|
||||||
|
*/
|
||||||
|
public static String getVersionedMd5(ThreadPoolParameter config, String clientVersion) {
|
||||||
|
String normalizedVersion = StringUtil.isNotBlank(clientVersion)
|
||||||
|
? clientVersion.trim()
|
||||||
|
: VersionUtil.UNKNOWN_VERSION;
|
||||||
|
String versionedContent = IncrementalContentUtil.getVersionedContent(config, normalizedVersion);
|
||||||
|
String md5 = Md5Util.md5Hex(versionedContent, "UTF-8");
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("ClientVersion={}: Using versioned MD5={}", normalizedVersion, md5);
|
||||||
|
}
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cn.hippo4j.common.toolkit;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NavigableMap;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version related utility methods.
|
||||||
|
*
|
||||||
|
* <p>This utility centralises how Hippo4j resolves client versions from
|
||||||
|
* different sources (pom, manifest) and how those versions map to the
|
||||||
|
* incremental protocol version used for MD5 comparison.</p>
|
||||||
|
*/
|
||||||
|
public final class VersionUtil {
|
||||||
|
|
||||||
|
public static final String UNKNOWN_VERSION = "0.0.0";
|
||||||
|
|
||||||
|
private static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?.*");
|
||||||
|
|
||||||
|
private VersionUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the client version string. First non blank candidate will be returned;
|
||||||
|
* if all candidates are blank the method falls back to the Implementation-Version
|
||||||
|
* from the provided class' package, and finally to {@code 0.0.0}.
|
||||||
|
*
|
||||||
|
* @param explicitVersion explicit version string provided by caller (can be null)
|
||||||
|
* @param fallbackClass class whose package can provide an Implementation-Version
|
||||||
|
* @return resolved version string, never {@code null}
|
||||||
|
*/
|
||||||
|
public static String resolveClientVersion(String explicitVersion, Class<?> fallbackClass) {
|
||||||
|
String candidate = firstNonBlank(explicitVersion);
|
||||||
|
if (StringUtil.isBlank(candidate) && fallbackClass != null) {
|
||||||
|
Package pkg = fallbackClass.getPackage();
|
||||||
|
if (pkg != null) {
|
||||||
|
candidate = pkg.getImplementationVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StringUtil.isBlank(candidate)) {
|
||||||
|
return UNKNOWN_VERSION;
|
||||||
|
}
|
||||||
|
return candidate.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two semantic versions using {@link SemanticVersion}. Returns {@code true} if
|
||||||
|
* {@code version1} is greater than or equal to {@code version2}. Blank or unparsable versions
|
||||||
|
* are treated conservatively and will return {@code false}.
|
||||||
|
*
|
||||||
|
* @param version1 the client version
|
||||||
|
* @param version2 the minimum version requirement
|
||||||
|
* @return {@code true} if version1 >= version2
|
||||||
|
*/
|
||||||
|
public static boolean isVersionGreaterOrEqual(String version1, String version2) {
|
||||||
|
if (StringUtil.isBlank(version1) || StringUtil.isBlank(version2)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SemanticVersion v1 = SemanticVersion.parse(version1);
|
||||||
|
SemanticVersion v2 = SemanticVersion.parse(version2);
|
||||||
|
if (v1 == null || v2 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return v1.compareTo(v2) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the first non-blank value from the provided arguments.
|
||||||
|
*
|
||||||
|
* @param values variable arguments to check
|
||||||
|
* @return first non-blank value, or {@code null} if all are blank
|
||||||
|
*/
|
||||||
|
private static String firstNonBlank(String... values) {
|
||||||
|
if (values == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String value : values) {
|
||||||
|
if (StringUtil.isNotBlank(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight immutable semantic version implementation (major.minor.patch).
|
||||||
|
*/
|
||||||
|
private static final class SemanticVersion implements Comparable<SemanticVersion> {
|
||||||
|
|
||||||
|
private final int major;
|
||||||
|
private final int minor;
|
||||||
|
private final int patch;
|
||||||
|
|
||||||
|
private SemanticVersion(int major, int minor, int patch) {
|
||||||
|
this.major = major;
|
||||||
|
this.minor = minor;
|
||||||
|
this.patch = patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a semantic version string into a SemanticVersion instance.
|
||||||
|
*
|
||||||
|
* @param version version string (e.g., "2.1.5", "2.0.0-SNAPSHOT")
|
||||||
|
* @return parsed SemanticVersion, or {@code null} if format is invalid
|
||||||
|
*/
|
||||||
|
private static SemanticVersion parse(String version) {
|
||||||
|
Matcher matcher = VERSION_PATTERN.matcher(version.trim());
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int major = parseOrDefault(matcher.group(1));
|
||||||
|
int minor = parseOrDefault(matcher.group(2));
|
||||||
|
int patch = parseOrDefault(matcher.group(3));
|
||||||
|
return new SemanticVersion(major, minor, patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a version component string to integer, defaulting to 0 if blank.
|
||||||
|
*
|
||||||
|
* @param value version component string
|
||||||
|
* @return parsed integer, or 0 if blank
|
||||||
|
*/
|
||||||
|
private static int parseOrDefault(String value) {
|
||||||
|
if (StringUtil.isBlank(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Integer.parseInt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(SemanticVersion other) {
|
||||||
|
if (other == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (major != other.major) {
|
||||||
|
return Integer.compare(major, other.major);
|
||||||
|
}
|
||||||
|
if (minor != other.minor) {
|
||||||
|
return Integer.compare(minor, other.minor);
|
||||||
|
}
|
||||||
|
return Integer.compare(patch, other.patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(obj instanceof SemanticVersion)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SemanticVersion that = (SemanticVersion) obj;
|
||||||
|
return major == that.major && minor == that.minor && patch == that.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(major, minor, patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return major + "." + minor + "." + patch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,285 @@
|
|||||||
|
/*
|
||||||
|
* 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<String, Object> 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<String, Object> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,443 @@
|
|||||||
|
/*
|
||||||
|
* 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<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
// Protocol v2 client content generation
|
||||||
|
String v2Content = IncrementalContentUtil.getVersionedContent(serverConfig, "2.0.0");
|
||||||
|
LinkedHashMap<String, Object> v2Fields = JSONUtil.parseObject(v2Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
// Protocol v3 client content generation
|
||||||
|
String v3Content = IncrementalContentUtil.getVersionedContent(serverConfig, "2.1.0");
|
||||||
|
LinkedHashMap<String, Object> v3Fields = JSONUtil.parseObject(v3Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("Server config has executeTimeOut: " + serverConfig.getExecuteTimeOut());
|
||||||
|
System.out.println("Protocol v1 content: " + v1Content);
|
||||||
|
System.out.println("Protocol v2 content: " + v2Content);
|
||||||
|
System.out.println("Protocol 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<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
LinkedHashMap<String, Object> v2Fields = JSONUtil.parseObject(v2Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
LinkedHashMap<String, Object> v3Fields = JSONUtil.parseObject(v3Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("Protocol v1 content: " + v1Content);
|
||||||
|
System.out.println("Protocol v2 content: " + v2Content);
|
||||||
|
System.out.println("Protocol v3 content: " + v3Content);
|
||||||
|
System.out.println("v1 contains isAlarm: " + v1Fields.containsKey("isAlarm"));
|
||||||
|
System.out.println("v2 contains isAlarm: " + v2Fields.containsKey("isAlarm"));
|
||||||
|
System.out.println("v3 contains isAlarm: " + v3Fields.containsKey("isAlarm"));
|
||||||
|
|
||||||
|
Assert.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<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
LinkedHashMap<String, Object> v2Fields = JSONUtil.parseObject(v2Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
LinkedHashMap<String, Object> v3Fields = JSONUtil.parseObject(v3Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("Server v2.1 has field 'yyy' (capacityAlarm): " + serverV21Config.getCapacityAlarm());
|
||||||
|
System.out.println("Protocol v1 contains capacityAlarm: " + v1Fields.containsKey("capacityAlarm"));
|
||||||
|
System.out.println("Protocol v2 contains capacityAlarm: " + v2Fields.containsKey("capacityAlarm"));
|
||||||
|
System.out.println("Protocol v3 contains capacityAlarm: " + v3Fields.containsKey("capacityAlarm"));
|
||||||
|
|
||||||
|
Assert.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<String, String> 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<String, Object> v1Fields = JSONUtil.parseObject(v1Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
LinkedHashMap<String, Object> v2Fields = JSONUtil.parseObject(v2Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
LinkedHashMap<String, Object> v3Fields = JSONUtil.parseObject(v3Content, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("\nField Visibility Matrix:");
|
||||||
|
System.out.println("Field | Protocol v1 | Protocol v2 | Protocol v3");
|
||||||
|
System.out.println("------------------+-------------+-------------+------------");
|
||||||
|
System.out.println("tenantId | " + v1Fields.containsKey("tenantId") + " | " + v2Fields.containsKey("tenantId") + " | " + v3Fields.containsKey("tenantId"));
|
||||||
|
System.out.println("coreSize | " + v1Fields.containsKey("coreSize") + " | " + v2Fields.containsKey("coreSize") + " | " + v3Fields.containsKey("coreSize"));
|
||||||
|
System.out.println(
|
||||||
|
"executeTimeOut | " + v1Fields.containsKey("executeTimeOut") + " | " + v2Fields.containsKey("executeTimeOut") + " | " + v3Fields.containsKey("executeTimeOut"));
|
||||||
|
System.out.println("isAlarm | " + v1Fields.containsKey("isAlarm") + " | " + v2Fields.containsKey("isAlarm") + " | " + v3Fields.containsKey("isAlarm"));
|
||||||
|
System.out.println("capacityAlarm | " + v1Fields.containsKey("capacityAlarm") + " | " + v2Fields.containsKey("capacityAlarm") + " | " + v3Fields.containsKey("capacityAlarm"));
|
||||||
|
|
||||||
|
// Core assertions
|
||||||
|
Assert.assertTrue("All protocols see core fields", v1Fields.containsKey("coreSize") && v2Fields.containsKey("coreSize") && v3Fields.containsKey("coreSize"));
|
||||||
|
Assert.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue