From 1bdc7ea574ff377c0cb36068156462727616a5fd Mon Sep 17 00:00:00 2001 From: Aiden Date: Sat, 16 Aug 2025 23:13:35 +0800 Subject: [PATCH 1/6] feat(biz-ops): implement BizOpsPodCoordinator for managing biz identities Add BizOpsPodCoordinator class to handle saving, removing, and accessing biz identities and their model versions. Update InstallBizHandler and UninstallBizHandler to utilize the new coordinator for managing biz identity access and lifecycle. Also, introduce bizModelVersion in ArkBizMeta for better tracking of biz versions. --- .../builtin/handler/InstallBizHandler.java | 27 +++--- .../builtin/handler/UninstallBizHandler.java | 15 +++- .../coordinate/BizOpsPodCoordinator.java | 87 +++++++++++++++++++ .../core/command/meta/bizops/ArkBizMeta.java | 19 ++++ 4 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java index 0419e3917..157ea41a5 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java @@ -19,11 +19,13 @@ import com.alipay.sofa.ark.api.ArkClient; import com.alipay.sofa.ark.api.ClientResponse; import com.alipay.sofa.ark.api.ResponseCode; +import com.alipay.sofa.ark.common.util.BizIdentityUtils; import com.alipay.sofa.ark.common.util.FileUtils; import com.alipay.sofa.ark.common.util.StringUtils; import com.alipay.sofa.ark.spi.model.Biz; import com.alipay.sofa.ark.spi.service.biz.BizFactoryService; import com.alipay.sofa.koupleless.arklet.core.command.builtin.BuiltinCommand; +import com.alipay.sofa.koupleless.arklet.core.command.coordinate.BizOpsPodCoordinator; import com.alipay.sofa.koupleless.arklet.core.command.meta.AbstractCommandHandler; import com.alipay.sofa.koupleless.arklet.core.command.meta.Command; import com.alipay.sofa.koupleless.arklet.core.command.meta.Output; @@ -54,8 +56,8 @@ * @version 1.0.0 */ public class InstallBizHandler extends - AbstractCommandHandler - implements ArkBizOps { + AbstractCommandHandler + implements ArkBizOps { private static final ArkletLogger LOGGER = ArkletLoggerFactory.getDefaultLogger(); /** {@inheritDoc} */ @@ -65,10 +67,13 @@ public Output handle(Input input) { long startSpace = metaSpaceMXBean.getUsage().getUsed(); try { InstallBizClientResponse installBizClientResponse = convertClientResponse( - getOperationService().install(convertInstallRequest(input))); + getOperationService().install(convertInstallRequest(input))); installBizClientResponse - .setElapsedSpace(metaSpaceMXBean.getUsage().getUsed() - startSpace); + .setElapsedSpace(metaSpaceMXBean.getUsage().getUsed() - startSpace); if (ResponseCode.SUCCESS.equals(installBizClientResponse.getCode())) { + String bizIdentity = BizIdentityUtils.generateBizIdentity(input.getBizName(), input.getBizVersion()); + String bizModelVersion = input.getBizModelVersion(); + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion); return Output.ofSuccess(installBizClientResponse); } else { return Output.ofFailed(installBizClientResponse, "install biz not success!"); @@ -80,8 +85,8 @@ public Output handle(Input input) { private InstallRequest convertInstallRequest(Input input) { return InstallRequest.builder().bizName(input.getBizName()) - .bizVersion(input.getBizVersion()).bizUrl(input.getBizUrl()).args(input.getArgs()) - .envs(input.getEnvs()).installStrategy(input.getInstallStrategy()).build(); + .bizVersion(input.getBizVersion()).bizUrl(input.getBizUrl()).args(input.getArgs()) + .envs(input.getEnvs()).installStrategy(input.getInstallStrategy()).build(); } private InstallBizClientResponse convertClientResponse(ClientResponse res) { @@ -106,12 +111,12 @@ public void validate(Input input) throws CommandValidationException { public static void validateInput(Input input) throws CommandValidationException { isTrue(!input.isAsync() || !StringUtils.isEmpty(input.getRequestId()), - "requestId should not be blank when async is true"); + "requestId should not be blank when async is true"); notBlank(input.getBizUrl(), "bizUrl should not be blank"); if (StringUtils.isEmpty(input.getBizName()) || StringUtils.isEmpty(input.getBizVersion())) { LOGGER.warn( - "biz name and version should not be empty, or it will reduce the performance."); + "biz name and version should not be empty, or it will reduce the performance."); } if (StringUtils.isEmpty(input.getBizName()) && StringUtils.isEmpty(input.getBizVersion())) { @@ -120,16 +125,16 @@ public static void validateInput(Input input) throws CommandValidationException refreshBizInfoFromJar(input); } catch (IOException e) { throw new CommandValidationException( - String.format("refresh biz info from jar failed: %s", e.getMessage())); + String.format("refresh biz info from jar failed: %s", e.getMessage())); } } else if (!StringUtils.isEmpty(input.getBizName()) - && !StringUtils.isEmpty(input.getBizVersion())) { + && !StringUtils.isEmpty(input.getBizVersion())) { // if bizName and bizVersion is not blank, it means that we should install the biz with the given bizName and bizVersion. // do nothing. } else { // if bizName or bizVersion is blank, it is invalid, throw exception. throw new CommandValidationException( - "bizName and bizVersion should be both blank or both not blank."); + "bizName and bizVersion should be both blank or both not blank."); } } diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java index ce18b1732..db40a5a6d 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java @@ -18,9 +18,11 @@ import com.alipay.sofa.ark.api.ClientResponse; import com.alipay.sofa.ark.api.ResponseCode; +import com.alipay.sofa.ark.common.util.BizIdentityUtils; import com.alipay.sofa.ark.common.util.StringUtils; import com.alipay.sofa.koupleless.arklet.core.command.builtin.BuiltinCommand; import com.alipay.sofa.koupleless.arklet.core.command.builtin.handler.UninstallBizHandler.Input; +import com.alipay.sofa.koupleless.arklet.core.command.coordinate.BizOpsPodCoordinator; import com.alipay.sofa.koupleless.arklet.core.command.meta.AbstractCommandHandler; import com.alipay.sofa.koupleless.arklet.core.command.meta.Command; import com.alipay.sofa.koupleless.arklet.core.command.meta.Output; @@ -37,15 +39,20 @@ * @version 1.0.0 */ public class UninstallBizHandler extends AbstractCommandHandler - implements ArkBizOps { + implements ArkBizOps { /** {@inheritDoc} */ @Override public Output handle(Input input) { try { - ClientResponse res = getOperationService().uninstall(input.getBizName(), - input.getBizVersion()); + String bizIdentity = BizIdentityUtils.generateBizIdentity(input.getBizName(), input.getBizVersion()); + String bizModelVersion = input.getBizModelVersion(); + if (!BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)) { + return Output.ofFailed("can not access biz because the command is expired"); + } + ClientResponse res = getOperationService().uninstall(input.getBizName(), input.getBizVersion()); if (ResponseCode.SUCCESS.equals(res.getCode())) { + BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion); return Output.ofSuccess(res); } else { return Output.ofFailed(res, "uninstall biz not success!"); @@ -67,7 +74,7 @@ public void validate(Input input) throws CommandValidationException { notBlank(input.getBizName(), "bizName should not be blank"); notBlank(input.getBizVersion(), "bizVersion should not be blank"); isTrue(!input.isAsync() || !StringUtils.isEmpty(input.getRequestId()), - "requestId should not be blank when async is true"); + "requestId should not be blank when async is true"); } public static class Input extends ArkBizMeta { diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java new file mode 100644 index 000000000..2735c8a40 --- /dev/null +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java @@ -0,0 +1,87 @@ +/* + * 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 com.alipay.sofa.koupleless.arklet.core.command.coordinate; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.alipay.sofa.ark.common.util.StringUtils; + +/** + *

+ * BizOpsPodCoordinator class. + *

+ * + * @author liuzhuoheng + * @since 2025/7/15 + * @version 1.0.0 + */ +public class BizOpsPodCoordinator { + + /** + * bizIdentityLockMap + * key: bizIdentity, value: bizModelVersion + */ + private static final Map bizIdentityLockMap = new ConcurrentHashMap<>(); + + /** + *

+ * save. + *

+ * + * @param bizIdentity a {@link java.lang.String} object + * @param bizModelVersion a {@link java.lang.String} object + * @return + */ + public static void save(String bizIdentity, String bizModelVersion) { + if (StringUtils.isEmpty(bizModelVersion)) { + bizModelVersion = StringUtils.EMPTY_STRING; + } + bizIdentityLockMap.put(bizIdentity, bizModelVersion); + } + + /** + *

+ * remove. + *

+ * + * @param bizIdentity a {@link java.lang.String} object + * @param bizModelVersion a {@link java.lang.String} object + * @return + */ + public static void remove(String bizIdentity, String bizModelVersion) { + bizIdentityLockMap.remove(bizIdentity, bizModelVersion); + } + + /** + *

+ * canAccess. + *

+ * + * @param bizIdentity a {@link java.lang.String} object + * @param bizModelVersion a {@link java.lang.String} object + * @return a boolean + */ + public static boolean canAccess(String bizIdentity, String bizModelVersion) { + return StringUtils.isEmpty(bizModelVersion) + || StringUtils.isEmpty(bizIdentityLockMap.get(bizIdentity)) + || bizIdentityLockMap.get(bizIdentity).equals(bizModelVersion); + } + +} diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/meta/bizops/ArkBizMeta.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/meta/bizops/ArkBizMeta.java index bf2c5fac7..2ae661e66 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/meta/bizops/ArkBizMeta.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/meta/bizops/ArkBizMeta.java @@ -29,6 +29,7 @@ public class ArkBizMeta extends InputMeta { private String bizName; private String bizVersion; private String requestId; + private String bizModelVersion; private boolean async; /** @@ -85,6 +86,24 @@ public void setRequestId(String requestId) { this.requestId = requestId; } + /** + *

Getter for the field bizModelVersion.

+ * + * @return a {@link java.lang.String} object + */ + public String getBizModelVersion() { + return bizModelVersion; + } + + /** + *

Setter for the field bizModelVersion.

+ * + * @param bizModelVersion a {@link java.lang.String} object + */ + public void setBizModelVersion(String bizModelVersion) { + this.bizModelVersion = bizModelVersion; + } + /** *

isAsync.

* From afdd6ff53d928fc9a0d4105d481b7d4a3847fee0 Mon Sep 17 00:00:00 2001 From: Aiden Date: Sun, 31 Aug 2025 15:40:10 +0800 Subject: [PATCH 2/6] feat(logging): add logging for access control in UninstallBizHandler - Introduced ArkletLogger to log access control failures. - Cleaned up unused imports in BizOpsPodCoordinator. - Added unit tests for BizOpsPodCoordinator functionality. --- .../builtin/handler/UninstallBizHandler.java | 7 + .../coordinate/BizOpsPodCoordinator.java | 2 - .../coordinate/BizOpsPodCoordinatorTest.java | 379 ++++++++++++++++++ 3 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 arklet-core/src/test/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinatorTest.java diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java index db40a5a6d..0a4b550c8 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java @@ -30,6 +30,8 @@ import com.alipay.sofa.koupleless.arklet.core.command.meta.bizops.ArkBizOps; import com.alipay.sofa.koupleless.arklet.core.common.exception.ArkletRuntimeException; import com.alipay.sofa.koupleless.arklet.core.common.exception.CommandValidationException; +import com.alipay.sofa.koupleless.common.log.ArkletLogger; +import com.alipay.sofa.koupleless.common.log.ArkletLoggerFactory; /** *

UninstallBizHandler class.

@@ -41,6 +43,8 @@ public class UninstallBizHandler extends AbstractCommandHandler implements ArkBizOps { + private static final ArkletLogger LOGGER = ArkletLoggerFactory.getDefaultLogger(); + /** {@inheritDoc} */ @Override public Output handle(Input input) { @@ -48,6 +52,9 @@ public Output handle(Input input) { String bizIdentity = BizIdentityUtils.generateBizIdentity(input.getBizName(), input.getBizVersion()); String bizModelVersion = input.getBizModelVersion(); if (!BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)) { + LOGGER.error( + "can not access biz because the command is expired. bizIdentity: {}, bizModelVersion: {}", + bizIdentity, bizModelVersion); return Output.ofFailed("can not access biz because the command is expired"); } ClientResponse res = getOperationService().uninstall(input.getBizName(), input.getBizVersion()); diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java index 2735c8a40..8c50fadc0 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java @@ -17,8 +17,6 @@ package com.alipay.sofa.koupleless.arklet.core.command.coordinate; import java.util.Map; -import java.util.Objects; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import com.alipay.sofa.ark.common.util.StringUtils; diff --git a/arklet-core/src/test/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinatorTest.java b/arklet-core/src/test/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinatorTest.java new file mode 100644 index 000000000..91208e8ad --- /dev/null +++ b/arklet-core/src/test/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinatorTest.java @@ -0,0 +1,379 @@ +/* + * 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 com.alipay.sofa.koupleless.arklet.core.command.coordinate; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Test cases for BizOpsPodCoordinator + * + * @author liuzhuoheng + * @since 2025/7/15 + * @version 1.0.0 + */ +public class BizOpsPodCoordinatorTest { + + @Before + public void setUp() throws Exception { + // 清理静态Map,确保测试隔离 + clearBizIdentityLockMap(); + } + + @After + public void tearDown() throws Exception { + // 清理静态Map,确保测试隔离 + clearBizIdentityLockMap(); + } + + /** + * 通过反射清理静态Map + */ + @SuppressWarnings("unchecked") + private void clearBizIdentityLockMap() throws Exception { + Field field = BizOpsPodCoordinator.class.getDeclaredField("bizIdentityLockMap"); + field.setAccessible(true); + Map map = (Map) field.get(null); + map.clear(); + } + + /** + * 测试save方法 - 正常情况 + */ + @Test + public void testSave_Normal() { + String bizIdentity = "user-service:1.0.0-SNAPSHOT"; + String bizModelVersion = "default/user-service-abc123def456-xyz78"; + + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion); + + // 验证通过canAccess方法来检查是否保存成功 + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)); + } + + /** + * 测试save方法 - 使用真实的业务数据格式 + */ + @Test + public void testSave_RealBusinessFormat() { + String bizIdentity = "biz1-web-single-host:0.0.1-SNAPSHOT"; + String bizModelVersion = "default/biz1-web-single-host-786dfc476f-rt28q"; + + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion); + + // 验证通过canAccess方法来检查是否保存成功 + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)); + } + + /** + * 测试save方法 - bizModelVersion为null + */ + @Test + public void testSave_NullBizModelVersion() { + String bizIdentity = "payment-service:2.1.0-SNAPSHOT"; + String bizModelVersion = null; + + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion); + + // 验证null会被转换为空字符串 + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, "")); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, null)); + } + + /** + * 测试save方法 - bizModelVersion为空字符串 + */ + @Test + public void testSave_EmptyBizModelVersion() { + String bizIdentity = "order-service:1.5.3-SNAPSHOT"; + String bizModelVersion = ""; + + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion); + + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, "")); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, null)); + } + + /** + * 测试save方法 - 覆盖已存在的数据 + */ + @Test + public void testSave_Override() { + String bizIdentity = "inventory-service:3.2.1-SNAPSHOT"; + String bizModelVersion1 = "default/inventory-service-7f8d9c2e1a-old98"; + String bizModelVersion2 = "default/inventory-service-7f8d9c2e1a-new12"; + + // 先保存第一个版本 + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion1); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion1)); + + // 覆盖为第二个版本 + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion2); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion2)); + Assert.assertFalse(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion1)); + } + + /** + * 测试remove方法 - 正常情况 + */ + @Test + public void testRemove_Normal() { + String bizIdentity = "notification-service:0.8.9-SNAPSHOT"; + String bizModelVersion = "default/notification-service-5a6b7c8d9e-f0123"; + + // 先保存 + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)); + + // 删除 + BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion); + + // 验证删除后可以访问任何版本(因为Map中不存在该key) + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)); + Assert.assertTrue( + BizOpsPodCoordinator.canAccess(bizIdentity, "default/any-other-pod-version")); + } + + /** + * 测试remove方法 - 删除不存在的数据 + */ + @Test + public void testRemove_NotExists() { + String bizIdentity = "auth-service:2.0.5-SNAPSHOT"; + String bizModelVersion = "default/auth-service-9e8d7c6b5a-4f321"; + + // 直接删除不存在的数据,不应该抛异常 + BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion); + + // 验证可以正常访问 + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)); + } + + /** + * 测试remove方法 - 版本不匹配 + */ + @Test + public void testRemove_VersionMismatch() { + String bizIdentity = "product-service:1.3.7-SNAPSHOT"; + String bizModelVersion1 = "default/product-service-4d5e6f7g8h-i9j0k"; + String bizModelVersion2 = "default/product-service-1a2b3c4d5e-f6g7h"; + + // 保存版本1 + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion1); + + // 尝试删除版本2(不匹配) + BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion2); + + // 验证版本1仍然存在 + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion1)); + Assert.assertFalse(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion2)); + } + + /** + * 测试canAccess方法 - bizModelVersion为null或空 + */ + @Test + public void testCanAccess_NullOrEmptyBizModelVersion() { + String bizIdentity = "config-service:0.5.2-SNAPSHOT"; + + // 无论Map中是否存在数据,null或空的bizModelVersion都应该返回true + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, null)); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, "")); + + // 先保存一个版本 + BizOpsPodCoordinator.save(bizIdentity, "default/config-service-8h9i0j1k2l-m3n4o"); + + // null或空的bizModelVersion仍然应该返回true + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, null)); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, "")); + } + + /** + * 测试canAccess方法 - 不同版本的业务模块 + */ + @Test + public void testCanAccess_DifferentBizVersions() { + String bizIdentity1 = "biz1-web-single-host:0.0.1-SNAPSHOT"; + String bizIdentity2 = "biz1-web-single-host:0.0.2-SNAPSHOT"; + String podVersion1 = "default/biz1-web-single-host-6d66bd6955-p59v8"; + String podVersion2 = "default/biz1-web-single-host-786dfc476f-rt28q"; + + // 保存不同版本的业务模块 + BizOpsPodCoordinator.save(bizIdentity1, podVersion1); + BizOpsPodCoordinator.save(bizIdentity2, podVersion2); + + // 验证各自的版本访问权限 + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity1, podVersion1)); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity2, podVersion2)); + + // 验证交叉访问权限(应该失败) + Assert.assertFalse(BizOpsPodCoordinator.canAccess(bizIdentity1, podVersion2)); + Assert.assertFalse(BizOpsPodCoordinator.canAccess(bizIdentity2, podVersion1)); + } + + /** + * 测试canAccess方法 - Map中对应的value为空 + */ + @Test + public void testCanAccess_EmptyValueInMap() { + String bizIdentity = "gateway-service:4.1.0-SNAPSHOT"; + String bizModelVersion = "default/gateway-service-6c7d8e9f0a-b1c2d"; + + // 保存空版本 + BizOpsPodCoordinator.save(bizIdentity, ""); + + // 无论传入什么版本都应该返回true(因为Map中的值为空) + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)); + Assert + .assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, "default/another-gateway-pod")); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, "")); + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, null)); + } + + /** + * 测试canAccess方法 - 版本匹配 + */ + @Test + public void testCanAccess_VersionMatch() { + String bizIdentity = "log-service:2.3.4-SNAPSHOT"; + String bizModelVersion = "default/log-service-a1b2c3d4e5-f6g7h"; + + // 保存版本 + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion); + + // 相同版本应该返回true + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)); + } + + /** + * 测试canAccess方法 - 版本不匹配 + */ + @Test + public void testCanAccess_VersionMismatch() { + String bizIdentity = "search-service:1.8.2-SNAPSHOT"; + String bizModelVersion1 = "default/search-service-e5f6g7h8i9-j0k1l"; + String bizModelVersion2 = "default/search-service-m2n3o4p5q6-r7s8t"; + + // 保存版本1 + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion1); + + // 版本2应该返回false + Assert.assertFalse(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion2)); + } + + /** + * 测试canAccess方法 - bizIdentity不存在于Map中 + */ + @Test + public void testCanAccess_BizIdentityNotExists() { + String bizIdentity = "metrics-service:0.7.1-SNAPSHOT"; + String bizModelVersion = "default/metrics-service-u9v0w1x2y3-z4a5b"; + + // 不存在的bizIdentity应该返回true + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)); + } + + /** + * 测试并发场景下的线程安全性 + */ + @Test + public void testConcurrency() throws InterruptedException { + int threadCount = 50; + int operationsPerThread = 100; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + for (int i = 0; i < threadCount; i++) { + final int threadIndex = i; + executor.execute(() -> { + try { + for (int j = 0; j < operationsPerThread; j++) { + String bizIdentity = "concurrent-service-" + threadIndex + + ":1.0.0-SNAPSHOT"; + String bizModelVersion = "default/concurrent-service-" + threadIndex + + "-pod-" + j; + + // 保存 + BizOpsPodCoordinator.save(bizIdentity, bizModelVersion); + + // 检查访问权限 + BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion); + + // 删除 + BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion); + } + } finally { + latch.countDown(); + } + }); + } + + executor.shutdown(); + Assert.assertTrue("并发测试超时", latch.await(30, TimeUnit.SECONDS)); + + // 验证没有抛出异常,并且最终状态正确 + Assert.assertTrue("并发测试完成", executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + /** + * 测试多个不同bizIdentity的并发操作 + */ + @Test + public void testMultipleBizIdentityConcurrency() throws InterruptedException { + int bizCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(bizCount); + CountDownLatch latch = new CountDownLatch(bizCount); + + for (int i = 0; i < bizCount; i++) { + final String bizIdentity = "multi-service-" + i + ":1.0.0-SNAPSHOT"; + executor.execute(() -> { + try { + // 保存 + BizOpsPodCoordinator.save(bizIdentity, "default/multi-service-pod-v1-abc123"); + + // 验证可以访问 + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, + "default/multi-service-pod-v1-abc123")); + + // 更新版本 + BizOpsPodCoordinator.save(bizIdentity, "default/multi-service-pod-v2-def456"); + + // 验证新版本可以访问,旧版本不能访问 + Assert.assertTrue(BizOpsPodCoordinator.canAccess(bizIdentity, + "default/multi-service-pod-v2-def456")); + Assert.assertFalse(BizOpsPodCoordinator.canAccess(bizIdentity, + "default/multi-service-pod-v1-abc123")); + + } finally { + latch.countDown(); + } + }); + } + + executor.shutdown(); + Assert.assertTrue("多Biz并发测试超时", latch.await(10, TimeUnit.SECONDS)); + } +} From a74e808520edf1c4e538736418f9affb56ab5f03 Mon Sep 17 00:00:00 2001 From: Aiden Date: Sun, 7 Sep 2025 18:36:25 +0800 Subject: [PATCH 3/6] feat(BizOpsPodCoordinator): enhance canAccess method with detailed logic Improve the access control logic in the canAccess method to handle various cases for bizModelVersion and bizIdentity. This change ensures compatibility with older versions and prevents the execution of outdated commands. Case 1: Allow access if bizModelVersion is empty. Case 2: Allow access if no record exists for bizIdentity. Case 3: Allow access if the version matches the current request. Only deny access when bizModelVersion is not empty and does not match. --- .../command/coordinate/BizOpsPodCoordinator.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java index 8c50fadc0..7b1ab6b33 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java @@ -71,15 +71,19 @@ public static void remove(String bizIdentity, String bizModelVersion) { *

* canAccess. *

- * - * @param bizIdentity a {@link java.lang.String} object - * @param bizModelVersion a {@link java.lang.String} object - * @return a boolean + * 判断是否可以访问指定的业务模块,基于业务模块版本的协调机制 + * @param bizIdentity 业务模块标识 (bizName:bizVersion) + * @param bizModelVersion 业务模块模型版本,用于命令协调和防止过期命令执行 + * @return 是否允许访问该业务模块 */ public static boolean canAccess(String bizIdentity, String bizModelVersion) { + // 判断逻辑说明: + // Case 1: bizModelVersion 为空 - 兼容性处理,允许访问(兼容旧版本 module-controller,arktcl, pod-not-exist 和 pod 紧急删除场景) + // Case 2: bizIdentityLockMap 中没有该 bizIdentity 的记录,允许访问(安装时不带 BizModelVersion,卸载时带上 BizModelVersion) + // Case 3: bizIdentityLockMap 中的版本与当前请求的版本匹配 - 版本一致,确认卸载的是该 Biz,允许访问 + // 只有当 bizModelVersion 不为空且存在 bizModelVersion 且不匹配时,才拒绝访问(防止旧的卸载命令执行) return StringUtils.isEmpty(bizModelVersion) || StringUtils.isEmpty(bizIdentityLockMap.get(bizIdentity)) || bizIdentityLockMap.get(bizIdentity).equals(bizModelVersion); } - } From e6aee62a6ec9adc3f584dfebf50c9c85ac3d586e Mon Sep 17 00:00:00 2001 From: Aiden Date: Mon, 15 Sep 2025 16:06:59 +0800 Subject: [PATCH 4/6] fix(pom): update revision to 1.4.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dadb4a8d2..dd8bfb017 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ pom - 1.4.2-SNAPSHOT + 1.4.3 2.2.16 ${revision} 2.7.15 From b2b350804baf5cee1fe0f0474d08c3b4952cb949 Mon Sep 17 00:00:00 2001 From: Aiden Date: Mon, 15 Sep 2025 16:49:58 +0800 Subject: [PATCH 5/6] fix(BizOpsPodCoordinator): add null checks for save and remove methods --- .../coordinate/BizOpsPodCoordinator.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java index 7b1ab6b33..511e4b1f0 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java @@ -48,6 +48,9 @@ public class BizOpsPodCoordinator { * @return */ public static void save(String bizIdentity, String bizModelVersion) { + if (StringUtils.isEmpty(bizIdentity)) { + return; + } if (StringUtils.isEmpty(bizModelVersion)) { bizModelVersion = StringUtils.EMPTY_STRING; } @@ -64,6 +67,13 @@ public static void save(String bizIdentity, String bizModelVersion) { * @return */ public static void remove(String bizIdentity, String bizModelVersion) { + if (StringUtils.isEmpty(bizIdentity)) { + return; + } + if (StringUtils.isEmpty(bizModelVersion)) { + bizIdentityLockMap.remove(bizIdentity); + return; + } bizIdentityLockMap.remove(bizIdentity, bizModelVersion); } @@ -72,14 +82,17 @@ public static void remove(String bizIdentity, String bizModelVersion) { * canAccess. *

* 判断是否可以访问指定的业务模块,基于业务模块版本的协调机制 + * * @param bizIdentity 业务模块标识 (bizName:bizVersion) * @param bizModelVersion 业务模块模型版本,用于命令协调和防止过期命令执行 * @return 是否允许访问该业务模块 */ public static boolean canAccess(String bizIdentity, String bizModelVersion) { // 判断逻辑说明: - // Case 1: bizModelVersion 为空 - 兼容性处理,允许访问(兼容旧版本 module-controller,arktcl, pod-not-exist 和 pod 紧急删除场景) - // Case 2: bizIdentityLockMap 中没有该 bizIdentity 的记录,允许访问(安装时不带 BizModelVersion,卸载时带上 BizModelVersion) + // Case 1: bizModelVersion 为空 - 兼容性处理,允许访问(兼容旧版本 module-controller,arktcl, + // pod-not-exist 和 pod 紧急删除场景) + // Case 2: bizIdentityLockMap 中没有该 bizIdentity 的记录,允许访问(安装时不带 + // BizModelVersion,卸载时带上 BizModelVersion) // Case 3: bizIdentityLockMap 中的版本与当前请求的版本匹配 - 版本一致,确认卸载的是该 Biz,允许访问 // 只有当 bizModelVersion 不为空且存在 bizModelVersion 且不匹配时,才拒绝访问(防止旧的卸载命令执行) return StringUtils.isEmpty(bizModelVersion) From 55f2fb7bdaba9a78be893e918918c442edfa3823 Mon Sep 17 00:00:00 2001 From: Aiden Date: Fri, 19 Sep 2025 15:13:45 +0800 Subject: [PATCH 6/6] style: format code for better readability in InstallBizHandler and UninstallBizHandler --- .../builtin/handler/InstallBizHandler.java | 25 ++++++++++--------- .../builtin/handler/UninstallBizHandler.java | 10 +++++--- .../coordinate/BizOpsPodCoordinator.java | 4 +-- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java index 157ea41a5..28ac25bf8 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java @@ -56,8 +56,8 @@ * @version 1.0.0 */ public class InstallBizHandler extends - AbstractCommandHandler - implements ArkBizOps { + AbstractCommandHandler + implements ArkBizOps { private static final ArkletLogger LOGGER = ArkletLoggerFactory.getDefaultLogger(); /** {@inheritDoc} */ @@ -67,11 +67,12 @@ public Output handle(Input input) { long startSpace = metaSpaceMXBean.getUsage().getUsed(); try { InstallBizClientResponse installBizClientResponse = convertClientResponse( - getOperationService().install(convertInstallRequest(input))); + getOperationService().install(convertInstallRequest(input))); installBizClientResponse - .setElapsedSpace(metaSpaceMXBean.getUsage().getUsed() - startSpace); + .setElapsedSpace(metaSpaceMXBean.getUsage().getUsed() - startSpace); if (ResponseCode.SUCCESS.equals(installBizClientResponse.getCode())) { - String bizIdentity = BizIdentityUtils.generateBizIdentity(input.getBizName(), input.getBizVersion()); + String bizIdentity = BizIdentityUtils.generateBizIdentity(input.getBizName(), + input.getBizVersion()); String bizModelVersion = input.getBizModelVersion(); BizOpsPodCoordinator.save(bizIdentity, bizModelVersion); return Output.ofSuccess(installBizClientResponse); @@ -85,8 +86,8 @@ public Output handle(Input input) { private InstallRequest convertInstallRequest(Input input) { return InstallRequest.builder().bizName(input.getBizName()) - .bizVersion(input.getBizVersion()).bizUrl(input.getBizUrl()).args(input.getArgs()) - .envs(input.getEnvs()).installStrategy(input.getInstallStrategy()).build(); + .bizVersion(input.getBizVersion()).bizUrl(input.getBizUrl()).args(input.getArgs()) + .envs(input.getEnvs()).installStrategy(input.getInstallStrategy()).build(); } private InstallBizClientResponse convertClientResponse(ClientResponse res) { @@ -111,12 +112,12 @@ public void validate(Input input) throws CommandValidationException { public static void validateInput(Input input) throws CommandValidationException { isTrue(!input.isAsync() || !StringUtils.isEmpty(input.getRequestId()), - "requestId should not be blank when async is true"); + "requestId should not be blank when async is true"); notBlank(input.getBizUrl(), "bizUrl should not be blank"); if (StringUtils.isEmpty(input.getBizName()) || StringUtils.isEmpty(input.getBizVersion())) { LOGGER.warn( - "biz name and version should not be empty, or it will reduce the performance."); + "biz name and version should not be empty, or it will reduce the performance."); } if (StringUtils.isEmpty(input.getBizName()) && StringUtils.isEmpty(input.getBizVersion())) { @@ -125,16 +126,16 @@ public static void validateInput(Input input) throws CommandValidationException refreshBizInfoFromJar(input); } catch (IOException e) { throw new CommandValidationException( - String.format("refresh biz info from jar failed: %s", e.getMessage())); + String.format("refresh biz info from jar failed: %s", e.getMessage())); } } else if (!StringUtils.isEmpty(input.getBizName()) - && !StringUtils.isEmpty(input.getBizVersion())) { + && !StringUtils.isEmpty(input.getBizVersion())) { // if bizName and bizVersion is not blank, it means that we should install the biz with the given bizName and bizVersion. // do nothing. } else { // if bizName or bizVersion is blank, it is invalid, throw exception. throw new CommandValidationException( - "bizName and bizVersion should be both blank or both not blank."); + "bizName and bizVersion should be both blank or both not blank."); } } diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java index 0a4b550c8..b4d2cfe50 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java @@ -41,7 +41,7 @@ * @version 1.0.0 */ public class UninstallBizHandler extends AbstractCommandHandler - implements ArkBizOps { + implements ArkBizOps { private static final ArkletLogger LOGGER = ArkletLoggerFactory.getDefaultLogger(); @@ -49,7 +49,8 @@ public class UninstallBizHandler extends AbstractCommandHandler handle(Input input) { try { - String bizIdentity = BizIdentityUtils.generateBizIdentity(input.getBizName(), input.getBizVersion()); + String bizIdentity = BizIdentityUtils.generateBizIdentity(input.getBizName(), + input.getBizVersion()); String bizModelVersion = input.getBizModelVersion(); if (!BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)) { LOGGER.error( @@ -57,7 +58,8 @@ public Output handle(Input input) { bizIdentity, bizModelVersion); return Output.ofFailed("can not access biz because the command is expired"); } - ClientResponse res = getOperationService().uninstall(input.getBizName(), input.getBizVersion()); + ClientResponse res = getOperationService().uninstall(input.getBizName(), + input.getBizVersion()); if (ResponseCode.SUCCESS.equals(res.getCode())) { BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion); return Output.ofSuccess(res); @@ -81,7 +83,7 @@ public void validate(Input input) throws CommandValidationException { notBlank(input.getBizName(), "bizName should not be blank"); notBlank(input.getBizVersion(), "bizVersion should not be blank"); isTrue(!input.isAsync() || !StringUtils.isEmpty(input.getRequestId()), - "requestId should not be blank when async is true"); + "requestId should not be blank when async is true"); } public static class Input extends ArkBizMeta { diff --git a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java index 511e4b1f0..aedd25539 100644 --- a/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java +++ b/arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java @@ -96,7 +96,7 @@ public static boolean canAccess(String bizIdentity, String bizModelVersion) { // Case 3: bizIdentityLockMap 中的版本与当前请求的版本匹配 - 版本一致,确认卸载的是该 Biz,允许访问 // 只有当 bizModelVersion 不为空且存在 bizModelVersion 且不匹配时,才拒绝访问(防止旧的卸载命令执行) return StringUtils.isEmpty(bizModelVersion) - || StringUtils.isEmpty(bizIdentityLockMap.get(bizIdentity)) - || bizIdentityLockMap.get(bizIdentity).equals(bizModelVersion); + || StringUtils.isEmpty(bizIdentityLockMap.get(bizIdentity)) + || bizIdentityLockMap.get(bizIdentity).equals(bizModelVersion); } }