From 36b6db112de4458096b27da0858239441b3b8061 Mon Sep 17 00:00:00 2001 From: xiaozhao Date: Sat, 16 Nov 2024 16:46:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(sermant-springboot-registry):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20Nacos=20=E6=B3=A8=E5=86=8C=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 NacosRegisterConfig 类用于配置 Nacos 相关参数 - 实现 NacosDiscoveryClient 类以支持 Nacos服务发现和注册 - 添加 NacosServiceManager 类用于管理 Nacos 服务 - 实现 NacosInstanceListenable 类以监听 Nacos 实例变化 - 新增 NacosWeightRandomRule 类用于 Nacos 权重随机负载均衡 - 更新配置文件和资源文件以支持 Nacos Signed-off-by: xiaozhao --- .../config/config.yaml | 11 + .../discovery/config/NacosRegisterConfig.java | 349 ++++++++++++++++++ .../discovery/config/PropertyKeyConst.java | 75 ++++ .../discovery/entity/RegisterContext.java | 120 ++++++ ...io.sermant.core.plugin.config.PluginConfig | 1 + .../springboot-registry-service/pom.xml | 7 + .../lb/cache/InstanceCacheManager.java | 8 + .../lb/discovery/InstanceChangeListener.java | 10 + .../discovery/nacos/NacosDiscoveryClient.java | 136 +++++++ .../discovery/nacos/NacosServiceManager.java | 157 ++++++++ .../nacos/listen/NacosInstanceListenable.java | 99 +++++ .../service/lb/discovery/zk/ZkClient.java | 6 +- .../lb/rule/NacosWeightRandomRule.java | 71 ++++ ...ry.service.lb.discovery.InstanceListenable | 1 + ...ervice.lb.discovery.ServiceDiscoveryClient | 1 + ...overy.service.lb.rule.AbstractLoadbalancer | 1 + .../nacos/NacosDiscoveryClientTest.java | 91 +++++ .../nacos/NacosServiceManagerTest.java | 66 ++++ .../lb/rule/NacosWeightRandomRuleTest.java | 80 ++++ 19 files changed, 1289 insertions(+), 1 deletion(-) create mode 100644 sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/config/NacosRegisterConfig.java create mode 100644 sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/config/PropertyKeyConst.java create mode 100644 sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/NacosDiscoveryClient.java create mode 100644 sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/NacosServiceManager.java create mode 100644 sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/listen/NacosInstanceListenable.java create mode 100644 sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/rule/NacosWeightRandomRule.java create mode 100644 sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/discovery/nacos/NacosDiscoveryClientTest.java create mode 100644 sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/discovery/nacos/NacosServiceManagerTest.java create mode 100644 sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/rule/NacosWeightRandomRuleTest.java diff --git a/sermant-plugins/sermant-springboot-registry/config/config.yaml b/sermant-plugins/sermant-springboot-registry/config/config.yaml index a19a3b0d30..dd4497d2e6 100644 --- a/sermant-plugins/sermant-springboot-registry/config/config.yaml +++ b/sermant-plugins/sermant-springboot-registry/config/config.yaml @@ -9,6 +9,8 @@ sermant.springboot.registry: enableRequestCount: false sermant.springboot.registry.lb: + # The type of the registry center, currently supports Nacos and Zookeeper + registryCenterType: # The load balancer type currently supports RoundRobin、Random、WeightedResponseTime、 # BestAvailable(Minimum number of concurrent transactions) lbType: RoundRobin @@ -28,3 +30,12 @@ sermant.springboot.registry.lb: # For retry scenarios, for {@link java.util.concurrent.TimeoutException}, whether you need to retry, which is enabled # by default, and this timeout is mostly used in asynchronous scenarios, such as Future, MinimalHttpAsyncClient enableTimeoutExRetry: true + + +nacos.service: + username: "nacos" # nacos验证账户 + password: "nacos" # nacos的验证密码 + namespace: "sermant" # 命名空间,nacos配置创建命名空间的id值 + weight: 1 # 服务实例权重值 + clusterName: DEFAULT # 集群名称 + ephemeral: true # 是否是临时节点,true为是,false为否 \ No newline at end of file diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/config/NacosRegisterConfig.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/config/NacosRegisterConfig.java new file mode 100644 index 0000000000..e26e66a45a --- /dev/null +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/config/NacosRegisterConfig.java @@ -0,0 +1,349 @@ +package io.sermant.discovery.config; + +import io.sermant.core.config.ConfigManager; +import io.sermant.core.config.common.ConfigTypeKey; +import io.sermant.core.plugin.config.PluginConfig; +import io.sermant.core.plugin.config.PluginConfigManager; +import io.sermant.core.plugin.config.ServiceMeta; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; + +/** + * nacos注册插件配置 + * + * @author xiaozhao + * @since 2024-11-16 + */ +@ConfigTypeKey(value = "nacos.service") +public class NacosRegisterConfig implements PluginConfig { + /** + * 默认拉取间隔时间 + */ + private static final long DEFAULT_NOTIFY_DELAY = 5000L; + + /** + * 默认监控时间 + */ + private static final long DEFAULT_LOOKUP_INTERVAL = 30L; + + /** + * 默认数据页大小 + */ + private static final int DEFAULT_PAGINATION_SIZE = 100; + + /** + * spring cloud zone + * 若未配置默认使用系统环境变量的zone, 即spring.cloud.loadbalancer.zone + */ + private String zone; + + /** + * 是否加密 + */ + private boolean secure = false; + + /** + * nacos认证账户 + */ + private String username; + + /** + * nacos认证密码 + */ + private String password; + + /** + * 节点地址 + */ + private String endpoint = ""; + + /** + * 命名空间 + */ + private String namespace; + + /** + * nacos日志文件名 + */ + private String logName; + + /** + * 服务实例权重 + */ + private float weight = 1f; + + /** + * 集群名称 + */ + private String clusterName = "DEFAULT"; + + /** + * 组 + */ + private String group = "DEFAULT_GROUP"; + + /** + * 启动时是否加载缓存 + */ + private String namingLoadCacheAtStart = "false"; + + /** + * 命名空间AK + */ + private String accessKey; + + /** + * 命名空间SK + */ + private String secretKey; + + /** + * 实例是否可用 + */ + private boolean instanceEnabled = true; + + /** + * 是否临时节点 + */ + private boolean ephemeral = true; + + /** + * 实例元数据 + */ + private Map metadata = new HashMap<>(); + + /** + * 是否快速失败取缓存数据,false为不取,直接失败 + */ + private boolean failureToleranceEnabled = false; + + /** + * 服务名分隔符 + */ + private String serviceNameSeparator = ":"; + + /** + * 数据页大小 + */ + private int paginationSize = DEFAULT_PAGINATION_SIZE; + + /** + * 监控时间 + */ + private long lookupInterval = DEFAULT_LOOKUP_INTERVAL; + + /** + * 唤醒延时时间 + */ + private long notifyDelay = DEFAULT_NOTIFY_DELAY; + + /** + * 构造方法 + */ + public NacosRegisterConfig() { + final ServiceMeta serviceMeta = ConfigManager.getConfig(ServiceMeta.class); + if (serviceMeta == null) { + return; + } + zone = serviceMeta.getZone(); + group = serviceMeta.getApplication(); + } + + public String getZone() { + return zone; + } + + public void setZone(String zone) { + this.zone = zone; + } + + public boolean isSecure() { + return secure; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getLogName() { + return logName; + } + + public void setLogName(String logName) { + this.logName = logName; + } + + public float getWeight() { + return weight; + } + + public void setWeight(float weight) { + this.weight = weight; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getNamingLoadCacheAtStart() { + return namingLoadCacheAtStart; + } + + public void setNamingLoadCacheAtStart(String namingLoadCacheAtStart) { + this.namingLoadCacheAtStart = namingLoadCacheAtStart; + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public boolean isInstanceEnabled() { + return instanceEnabled; + } + + public void setInstanceEnabled(boolean instanceEnabled) { + this.instanceEnabled = instanceEnabled; + } + + public boolean isEphemeral() { + return ephemeral; + } + + public void setEphemeral(boolean ephemeral) { + this.ephemeral = ephemeral; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public boolean isFailureToleranceEnabled() { + return failureToleranceEnabled; + } + + public void setFailureToleranceEnabled(boolean failureToleranceEnabled) { + this.failureToleranceEnabled = failureToleranceEnabled; + } + + public String getServiceNameSeparator() { + return serviceNameSeparator; + } + + public void setServiceNameSeparator(String serviceNameSeparator) { + this.serviceNameSeparator = serviceNameSeparator; + } + + public int getPaginationSize() { + return paginationSize; + } + + public void setPaginationSize(int paginationSize) { + this.paginationSize = paginationSize; + } + + public long getLookupInterval() { + return lookupInterval; + } + + public void setLookupInterval(long lookupInterval) { + this.lookupInterval = lookupInterval; + } + + public long getNotifyDelay() { + return notifyDelay; + } + + public void setNotifyDelay(long notifyDelay) { + this.notifyDelay = notifyDelay; + } + + /** + * 获取配置参数 + * + * @return 配置 + */ + public Properties getNacosProperties() { + LbConfig lbConfig = PluginConfigManager.getPluginConfig(LbConfig.class); + Properties properties = new Properties(); + properties.put(PropertyKeyConst.SERVER_ADDR, lbConfig.getRegistryAddress()); + properties.put(PropertyKeyConst.USERNAME, Objects.toString(username, "")); + properties.put(PropertyKeyConst.PASSWORD, Objects.toString(password, "")); + properties.put(PropertyKeyConst.NAMESPACE, Objects.toString(namespace, "")); + properties.put(PropertyKeyConst.NACOS_NAMING_LOG_NAME, Objects.toString(logName, "")); + if (endpoint.contains(PropertyKeyConst.HTTP_URL_COLON)) { + int index = endpoint.indexOf(PropertyKeyConst.HTTP_URL_COLON); + properties.put(PropertyKeyConst.ENDPOINT, endpoint.substring(0, index)); + properties.put(PropertyKeyConst.ENDPOINT_PORT, endpoint.substring(index + 1)); + } else { + properties.put(PropertyKeyConst.ENDPOINT, endpoint); + } + properties.put(PropertyKeyConst.ACCESS_KEY, Objects.toString(accessKey, "")); + properties.put(PropertyKeyConst.SECRET_KEY, Objects.toString(secretKey, "")); + properties.put(PropertyKeyConst.CLUSTER_NAME, clusterName); + properties.put(PropertyKeyConst.NAMING_LOAD_CACHE_AT_START, namingLoadCacheAtStart); + return properties; + } +} diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/config/PropertyKeyConst.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/config/PropertyKeyConst.java new file mode 100644 index 0000000000..fa1f3854da --- /dev/null +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/config/PropertyKeyConst.java @@ -0,0 +1,75 @@ +package io.sermant.discovery.config; + +/** + * nacos注册插件配置 + * + * @author xiaozhao + * @since 2024-11-16 + */ +public class PropertyKeyConst { + /** + * 冒号 + */ + public static final String HTTP_URL_COLON = ":"; + + /** + * 节点 + */ + public static final String ENDPOINT = "endpoint"; + + /** + * 节点端口 + */ + public static final String ENDPOINT_PORT = "endpointPort"; + + /** + * 命名空间 + */ + public static final String NAMESPACE = "namespace"; + + /** + * 用户名 + */ + public static final String USERNAME = "username"; + + /** + * 用户密码 + */ + public static final String PASSWORD = "password"; + + /** + * ak值 + */ + public static final String ACCESS_KEY = "accessKey"; + + /** + * sk值 + */ + public static final String SECRET_KEY = "secretKey"; + + /** + * 服务地址 + */ + public static final String SERVER_ADDR = "serverAddr"; + + /** + * 集群名称 + */ + public static final String CLUSTER_NAME = "clusterName"; + + /** + * 开始是否naming加载缓存 + */ + public static final String NAMING_LOAD_CACHE_AT_START = "namingLoadCacheAtStart"; + + /** + * nacos日志文件名 + */ + public static final String NACOS_NAMING_LOG_NAME = "com.alibaba.nacos.naming.log.filename"; + + /** + * 构造方法 + */ + private PropertyKeyConst() { + } +} diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/entity/RegisterContext.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/entity/RegisterContext.java index 505be6e7be..96d10c0398 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/entity/RegisterContext.java +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/java/io/sermant/discovery/entity/RegisterContext.java @@ -16,6 +16,8 @@ package io.sermant.discovery.entity; +import java.util.Map; + /** * Registration Information Class * @@ -40,4 +42,122 @@ public DefaultServiceInstance getServiceInstance() { return this.serviceInstance; } + private final ClientInfo clientInfo = new ClientInfo(); + + public ClientInfo getClientInfo() { + return clientInfo; + } + + + /** + * 客户端信息 + * + * @since 2022-03-01 + */ + public static class ClientInfo { + /** + * 服务名 通过拦截获取 + */ + private String serviceName; + + /** + * 域名 + */ + private String host; + + /** + * ip + */ + private String ip; + + /** + * 端口 + */ + private int port; + + /** + * 服务id + */ + private String serviceId; + + /** + * 服务元信息 + */ + private Map meta; + + /** + * 区域 + */ + private String zone; + + /** + * 实例状态 UP DOWN + */ + private String status = "UN_KNOWN"; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getZone() { + return zone; + } + + public void setZone(String zone) { + this.zone = zone; + } + + public Map getMeta() { + return meta; + } + + public void setMeta(Map meta) { + this.meta = meta; + } + } + } diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/resources/META-INF/services/io.sermant.core.plugin.config.PluginConfig b/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/resources/META-INF/services/io.sermant.core.plugin.config.PluginConfig index f8585113e6..942ee0b062 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/resources/META-INF/services/io.sermant.core.plugin.config.PluginConfig +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-plugin/src/main/resources/META-INF/services/io.sermant.core.plugin.config.PluginConfig @@ -16,3 +16,4 @@ io.sermant.discovery.config.LbConfig io.sermant.discovery.config.DiscoveryPluginConfig +io.sermant.discovery.config.NacosRegisterConfig diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/pom.xml b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/pom.xml index eff5848fdf..30737fec63 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/pom.xml +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/pom.xml @@ -21,6 +21,7 @@ 3.1.0 2.13.4.2 31.1-jre + 2.0.4 @@ -42,6 +43,12 @@ provided + + com.alibaba.nacos + nacos-client + ${nacos.version} + + org.springframework.cloud diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/cache/InstanceCacheManager.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/cache/InstanceCacheManager.java index a7d102da18..473c2997c6 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/cache/InstanceCacheManager.java +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/cache/InstanceCacheManager.java @@ -263,6 +263,14 @@ public void notify(EventType eventType, ServiceInstance serviceInstance) { instanceCaches.put(serviceName, instanceCache); } + @Override + public void notify(List serviceInstances) { + String serviceName = serviceInstances.get(0).getServiceName(); + InstanceCache instanceCache = new InstanceCache(serviceName, serviceInstances); + instanceCache.setUpdateTimestamp(System.currentTimeMillis()); + instanceCaches.put(serviceName, instanceCache); + } + private void printLog(EventType eventType, ServiceInstance serviceInstance) { final Map metadata = serviceInstance.getMetadata(); if (metadata == null) { diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/InstanceChangeListener.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/InstanceChangeListener.java index 0749792711..c4a8b18814 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/InstanceChangeListener.java +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/InstanceChangeListener.java @@ -18,6 +18,8 @@ import io.sermant.discovery.entity.ServiceInstance; +import java.util.List; + /** * Listeners for instance changes, and notifications are sent when the instance changes * @@ -33,6 +35,14 @@ public interface InstanceChangeListener { */ void notify(EventType eventType, ServiceInstance serviceInstance); + + /** + * Notice + * + * @param serviceInstances All instances + */ + void notify(List serviceInstances); + /** * The type of event * diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/NacosDiscoveryClient.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/NacosDiscoveryClient.java new file mode 100644 index 0000000000..52f99849ac --- /dev/null +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/NacosDiscoveryClient.java @@ -0,0 +1,136 @@ +package io.sermant.discovery.service.lb.discovery.nacos; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.alibaba.nacos.api.naming.pojo.ListView; +import io.sermant.core.common.LoggerFactory; +import io.sermant.core.plugin.config.PluginConfigManager; +import io.sermant.core.utils.StringUtils; +import io.sermant.discovery.config.NacosRegisterConfig; +import io.sermant.discovery.entity.RegisterContext; +import io.sermant.discovery.entity.ServiceInstance; +import io.sermant.discovery.service.lb.discovery.ServiceDiscoveryClient; + +import java.io.IOException; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * nacos注册插件配置 + * + * @author xiaozhao + * @since 2024-11-16 + */ +public class NacosDiscoveryClient implements ServiceDiscoveryClient { + + + private static final Logger LOGGER = LoggerFactory.getLogger(); + + private final NacosServiceManager nacosServiceManager = NacosServiceManager.getInstance(); + + private Instance instance; + + private final NacosRegisterConfig nacosRegisterConfig; + + public NacosDiscoveryClient() { + nacosRegisterConfig = PluginConfigManager.getPluginConfig(NacosRegisterConfig.class); + } + + @Override + public void init() { + + } + + @Override + public boolean registry(ServiceInstance serviceInstance) { + String serviceId = serviceInstance.getServiceName(); + String group = nacosRegisterConfig.getGroup(); + instance = nacosServiceManager.buildNacosInstanceFromRegistration(serviceInstance); + try { + NamingService namingService = nacosServiceManager.getNamingService(); + namingService.registerInstance(serviceId, group, instance); + return true; + } catch (NacosException e) { + LOGGER.log(Level.SEVERE, String.format(Locale.ENGLISH, "failed when registry service,serviceId={%s}", + serviceId), e); + } + return false; + } + + + @Override + public Collection getServices() { + try { + String group = nacosRegisterConfig.getGroup(); + NamingService namingService = nacosServiceManager.getNamingService(); + ListView services = namingService.getServicesOfServer(1, Integer.MAX_VALUE, group); + return services.getData(); + } catch (NacosException e) { + LOGGER.log(Level.SEVERE, String.format(Locale.ENGLISH, "getServices failed," + + "isFailureToleranceEnabled={%s}", nacosRegisterConfig.isFailureToleranceEnabled()), e); + } + return Collections.emptyList(); + } + + @Override + public boolean unRegistry() { + if (StringUtils.isEmpty(RegisterContext.INSTANCE.getClientInfo().getServiceId())) { + LOGGER.warning("No service to de-register for nacos client..."); + return false; + } + String serviceId = RegisterContext.INSTANCE.getClientInfo().getServiceId(); + String group = nacosRegisterConfig.getGroup(); + try { + NamingService namingService = nacosServiceManager.getNamingService(); + namingService.deregisterInstance(serviceId, group, instance); + return true; + } catch (NacosException e) { + LOGGER.log(Level.SEVERE, String.format(Locale.ENGLISH, "failed when deRegister service," + + "serviceId={%s}", serviceId), e); + } + return false; + } + + @Override + public String name() { + return "Nacos"; + } + + @Override + public void close() throws IOException { + } + + + /** + * 获取对应服务名微服务实例信息 + * + * @param serviceId 服务id + * @return 服务信息 + */ + public List getInstances(String serviceId) { + String group = nacosRegisterConfig.getGroup(); + try { + NamingService namingService = nacosServiceManager.getNamingService(); + List instances = namingService.selectInstances(serviceId, group, true); + return convertServiceInstanceList(instances, serviceId); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, String.format(Locale.ENGLISH, "failed get Instances," + + "serviceId={%s}", serviceId), e); + } + return Collections.emptyList(); + } + + public List convertServiceInstanceList(List instances, String serviceId) { + List result = new ArrayList<>(instances.size()); + for (Instance instance : instances) { + Optional optional = nacosServiceManager.convertServiceInstance(instance, serviceId); + optional.ifPresent(result::add); + } + return result; + } + + + +} diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/NacosServiceManager.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/NacosServiceManager.java new file mode 100644 index 0000000000..97eabfa9d9 --- /dev/null +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/NacosServiceManager.java @@ -0,0 +1,157 @@ +package io.sermant.discovery.service.lb.discovery.nacos; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.NamingMaintainService; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.alibaba.nacos.client.naming.NacosNamingMaintainService; +import com.alibaba.nacos.client.naming.NacosNamingService; +import io.sermant.core.plugin.config.PluginConfigManager; +import io.sermant.discovery.config.NacosRegisterConfig; +import io.sermant.discovery.entity.DefaultServiceInstance; +import io.sermant.discovery.entity.ServiceInstance; + +import java.util.*; + +/** + * nacos注册插件配置 + * + * @author xiaozhao + * @since 2024-11-16 + */ +public class NacosServiceManager { + + private static final int DEFAULT_CAPACITY = 16; + + private volatile NamingService namingService; + + private volatile NamingMaintainService namingMaintainService; + + private final NacosRegisterConfig nacosRegisterConfig; + + + private static NacosServiceManager nacosServiceManager; + + /** + * 构造方法 + */ + public NacosServiceManager() { + nacosRegisterConfig = PluginConfigManager.getPluginConfig(NacosRegisterConfig.class); + } + + public static NacosServiceManager getInstance() { + if (nacosServiceManager == null) { + synchronized (NacosServiceManager.class) { + if (nacosServiceManager == null) { + nacosServiceManager = new NacosServiceManager(); + } + } + } + return nacosServiceManager; + } + + + /** + * 获取注册服务 + * + * @return NamingService服务 + * @throws NacosException nacos异常 + */ + public NamingService getNamingService() throws NacosException { + if (Objects.isNull(this.namingService)) { + buildNamingService(nacosRegisterConfig.getNacosProperties()); + } + return namingService; + } + + /** + * 获取namingMaintain服务 + * + * @return namingMaintain服务 + * @throws NacosException nacos异常 + */ + public NamingMaintainService getNamingMaintainService() throws NacosException { + if (Objects.isNull(namingMaintainService)) { + buildNamingMaintainService(nacosRegisterConfig.getNacosProperties()); + } + return namingMaintainService; + } + + private void buildNamingMaintainService(Properties properties) throws NacosException { + if (Objects.isNull(namingMaintainService)) { + synchronized (NacosServiceManager.class) { + if (Objects.isNull(namingMaintainService)) { + namingMaintainService = createNamingMaintainService(properties); + } + } + } + } + + private void buildNamingService(Properties properties) throws NacosException { + if (Objects.isNull(namingService)) { + synchronized (NacosServiceManager.class) { + if (Objects.isNull(namingService)) { + namingService = createNewNamingService(properties); + } + } + } + } + + private NamingService createNewNamingService(Properties properties) throws NacosException { + return new NacosNamingService(properties); + } + + private NamingMaintainService createNamingMaintainService(Properties properties) throws NacosException { + return new NacosNamingMaintainService(properties); + } + + /** + * 构建nacos注册实例 + * + * @return 实例 + */ + public Instance buildNacosInstanceFromRegistration(ServiceInstance serviceInstance) { + Instance instance = new Instance(); + instance.setIp(serviceInstance.getIp()); + instance.setPort(serviceInstance.getPort()); + instance.setWeight(nacosRegisterConfig.getWeight()); + instance.setClusterName(nacosRegisterConfig.getClusterName()); + instance.setEnabled(nacosRegisterConfig.isInstanceEnabled()); + final HashMap metadata = new HashMap<>(serviceInstance.getMetadata()); + instance.setMetadata(metadata); + instance.setEphemeral(nacosRegisterConfig.isEphemeral()); + return instance; + } + + /** + * 实例信息转换 + * + * @param instance 服务实例 + * @param serviceId 服务id + * @return 转换后实例信息 + */ + public Optional convertServiceInstance(Instance instance, String serviceId) { + if (instance == null || !instance.isEnabled() || !instance.isHealthy()) { + return Optional.empty(); + } + DefaultServiceInstance nacosServiceInstance = new DefaultServiceInstance(); + nacosServiceInstance.setHost(instance.getIp()); + nacosServiceInstance.setIp(instance.getIp()); + nacosServiceInstance.setPort(instance.getPort()); + nacosServiceInstance.setServiceName(serviceId); + nacosServiceInstance.setId(instance.getIp() + ":" + instance.getPort()); + + Map metadata = new HashMap<>(DEFAULT_CAPACITY); + metadata.put("nacos.instanceId", instance.getInstanceId()); + metadata.put("nacos.weight", instance.getWeight() + ""); + metadata.put("nacos.healthy", instance.isHealthy() + ""); + metadata.put("nacos.cluster", instance.getClusterName() + ""); + if (instance.getMetadata() != null) { + metadata.putAll(instance.getMetadata()); + } + metadata.put("nacos.ephemeral", String.valueOf(instance.isEphemeral())); + nacosServiceInstance.setMetadata(metadata); + + return Optional.of(nacosServiceInstance); + } +} diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/listen/NacosInstanceListenable.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/listen/NacosInstanceListenable.java new file mode 100644 index 0000000000..206f47b2af --- /dev/null +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/nacos/listen/NacosInstanceListenable.java @@ -0,0 +1,99 @@ +package io.sermant.discovery.service.lb.discovery.nacos.listen; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.listener.EventListener; +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.alibaba.nacos.client.naming.event.InstancesChangeEvent; +import com.alibaba.nacos.common.notify.Event; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.notify.listener.Subscriber; +import io.sermant.core.common.LoggerFactory; +import io.sermant.core.utils.CollectionUtils; +import io.sermant.discovery.entity.ServiceInstance; +import io.sermant.discovery.service.lb.discovery.InstanceChangeListener; +import io.sermant.discovery.service.lb.discovery.InstanceListenable; +import io.sermant.discovery.service.lb.discovery.nacos.NacosServiceManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +/** + * nacos监听实现 + * + * @author zhouss + * @since 2024-11-12 + */ +public class NacosInstanceListenable extends Subscriber implements InstanceListenable { + + private static final Logger LOGGER = LoggerFactory.getLogger(); + + + private final Map listenerCache = new ConcurrentHashMap<>(); + + private final NacosServiceManager nacosServiceManager = NacosServiceManager.getInstance(); + + + public NacosInstanceListenable() { + } + + @Override + public void onEvent(InstancesChangeEvent instancesChangeEvent) { + String serviceName = instancesChangeEvent.getServiceName(); + List instances = instancesChangeEvent.getHosts(); + if (CollectionUtils.isEmpty(instances)) { + return; + } + List serviceInstances = new ArrayList<>(instances.size()); + for (Instance instance : instances) { + if (instance.isEnabled() && instance.isHealthy()) { + Optional optional = nacosServiceManager.convertServiceInstance(instance, serviceName); + optional.ifPresent(serviceInstances::add); + } + } + listenerCache.get(serviceName).notify(serviceInstances); + } + + @Override + public Class subscribeType() { + return InstancesChangeEvent.class; + } + + @Override + public void init() { + } + + @Override + public void tryAdd(String serviceName, InstanceChangeListener listener) { + NotifyCenter.registerSubscriber(this); + try { + NamingService namingService = nacosServiceManager.getNamingService(); + namingService.subscribe(serviceName, new EventListener() { + @Override + public void onEvent(com.alibaba.nacos.api.naming.listener.Event event) { + LOGGER.info("Receive nacos instance change event: " + event); + } + }); + listenerCache.put(serviceName, listener); + } catch (NacosException e) { + System.out.println(e.getErrMsg()); + throw new RuntimeException(e); + } + + } + + @Override + public void close() { + listenerCache.clear(); + } + + @Override + public String name() { + return "Nacos"; + } + +} diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/zk/ZkClient.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/zk/ZkClient.java index 3b243dfd8a..20eaf4f65f 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/zk/ZkClient.java +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/discovery/zk/ZkClient.java @@ -36,6 +36,9 @@ * @since 2022-10-25 */ public class ZkClient implements PluginService { + + private final String REGISTRY_CENTER_TYPE = "Zookeeper"; + private final AtomicReference zkState = new AtomicReference<>(); private final LbConfig lbConfig; @@ -51,7 +54,8 @@ public ZkClient() { @Override public void start() { - if (!PluginConfigManager.getPluginConfig(DiscoveryPluginConfig.class).isEnableRegistry()) { + if (!PluginConfigManager.getPluginConfig(DiscoveryPluginConfig.class).isEnableRegistry() + || !REGISTRY_CENTER_TYPE.equals(lbConfig.getRegistryCenterType())) { return; } this.client = this.buildClient(); diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/rule/NacosWeightRandomRule.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/rule/NacosWeightRandomRule.java new file mode 100644 index 0000000000..905114fc0c --- /dev/null +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/java/io/sermant/discovery/service/lb/rule/NacosWeightRandomRule.java @@ -0,0 +1,71 @@ +package io.sermant.discovery.service.lb.rule; + +import com.alibaba.nacos.shaded.com.google.common.collect.Lists; +import io.sermant.discovery.entity.ServiceInstance; + +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +/** + * nacos权重 + * + * @author xiaozhao + * @since 2024-11-16 + */ +public class NacosWeightRandomRule extends AbstractLoadbalancer { + + @Override + protected ServiceInstance doChoose(String serviceName, List instances) { + List withWeights = instances.stream() + .map(instance -> { + int weight = Integer.parseInt(instance.getMetadata().get("nacos.weight")); + return new InstanceWithWeight(instance, weight); + }).collect(Collectors.toList()); + return this.weightRandom(withWeights); + } + + @Override + public String lbType() { + return "NacosWeight"; + } + + private class InstanceWithWeight { + private ServiceInstance server; + private Integer weight; + + public InstanceWithWeight(ServiceInstance instance, int weight) { + this.server = instance; + this.weight = weight; + } + + + public ServiceInstance getServer() { + return server; + } + + public Integer getWeight() { + return weight; + } + + } + + /** + * 根据权重随机 + * 算法参考 https://blog.csdn.net/u011627980/article/details/79401026 + * + * @param list 实例列表 + * @return 随机出来的结果 + */ + private ServiceInstance weightRandom(List list) { + List instances = Lists.newArrayList(); + for (InstanceWithWeight instanceWithWeight : list) { + int weight = instanceWithWeight.getWeight(); + for (int i = 0; i <= weight; i++) { + instances.add(instanceWithWeight.getServer()); + } + } + int i = new Random().nextInt(instances.size()); + return instances.get(i); + } + +} diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.discovery.InstanceListenable b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.discovery.InstanceListenable index e5086a73ea..5384daa277 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.discovery.InstanceListenable +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.discovery.InstanceListenable @@ -15,3 +15,4 @@ # io.sermant.discovery.service.lb.discovery.zk.listen.ZkInstanceListenable +io.sermant.discovery.service.lb.discovery.nacos.listen.NacosInstanceListenable diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.discovery.ServiceDiscoveryClient b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.discovery.ServiceDiscoveryClient index 48e1c03d16..4f8b254477 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.discovery.ServiceDiscoveryClient +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.discovery.ServiceDiscoveryClient @@ -15,3 +15,4 @@ # io.sermant.discovery.service.lb.discovery.zk.ZkDiscoveryClientProxy +io.sermant.discovery.service.lb.discovery.nacos.NacosDiscoveryClient diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.rule.AbstractLoadbalancer b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.rule.AbstractLoadbalancer index e576606039..5dbcb0bb79 100644 --- a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.rule.AbstractLoadbalancer +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/main/resources/META-INF/services/io.sermant.discovery.service.lb.rule.AbstractLoadbalancer @@ -18,3 +18,4 @@ io.sermant.discovery.service.lb.rule.RoundRobinLoadbalancer io.sermant.discovery.service.lb.rule.RandomLoadbalancer io.sermant.discovery.service.lb.rule.BestAvailableLoadbalancer io.sermant.discovery.service.lb.rule.WeightedResponseTimeLoadbalancer +io.sermant.discovery.service.lb.rule.NacosWeightRandomRule diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/discovery/nacos/NacosDiscoveryClientTest.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/discovery/nacos/NacosDiscoveryClientTest.java new file mode 100644 index 0000000000..2d1015df71 --- /dev/null +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/discovery/nacos/NacosDiscoveryClientTest.java @@ -0,0 +1,91 @@ +package io.sermant.discovery.service.lb.discovery.nacos; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; +import io.sermant.discovery.entity.DefaultServiceInstance; +import io.sermant.discovery.entity.ServiceInstance; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class NacosDiscoveryClientTest { + + @Mock + private NamingService namingService; + + + @InjectMocks + private NacosDiscoveryClient nacosDiscoveryClient; + + private AutoCloseable closeable; + + @Before + public void setUp() throws Exception { + closeable = MockitoAnnotations.openMocks(this); + nacosDiscoveryClient = new NacosDiscoveryClient(); + } + + @After + public void tearDown() throws Exception { + closeable.close(); + } + + @Test + public void testRegister() throws NacosException { + Instance instance = new Instance(); + instance.setIp("1.1.1.1"); + instance.setPort(8080); + instance.setServiceName("test"); + + when(namingService.selectInstances(anyString(), any(Boolean.class), any(Boolean.class))) + .thenReturn(Arrays.asList(instance)); + + ServiceInstance serviceInstance = new DefaultServiceInstance(); + + instance.setInstanceId("127.0.0.1:8080"); + instance.setPort(8080); + instance.setWeight(1.0); + instance.setHealthy(true); + Map metadata = new HashMap<>(); + metadata.put("version", "1.0.0"); + metadata.put("environment", "dev"); + instance.setMetadata(metadata); + + nacosDiscoveryClient.registry(serviceInstance); + + verify(namingService, times(1)).registerInstance(anyString(), any(Instance.class)); + } + + @Test + public void testDeregister() throws NacosException { + Instance instance = new Instance(); + instance.setIp("1.1.1.1"); + instance.setPort(8080); + instance.setServiceName("test"); + + Map instanceMap = new HashMap<>(); + instanceMap.put("1.1.1.1:8080", instance); + + when(namingService.selectInstances(anyString(), any(Boolean.class), any(Boolean.class))) + .thenReturn(Arrays.asList(instance)); + + nacosDiscoveryClient.unRegistry(); + + verify(namingService, times(1)).deregisterInstance(anyString(), any(Instance.class)); + } +} diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/discovery/nacos/NacosServiceManagerTest.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/discovery/nacos/NacosServiceManagerTest.java new file mode 100644 index 0000000000..21ebcd4f6c --- /dev/null +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/discovery/nacos/NacosServiceManagerTest.java @@ -0,0 +1,66 @@ +package io.sermant.discovery.service.lb.discovery.nacos; + +import java.util.*; +import java.math.*; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class NacosServiceManagerTest { + + private NacosServiceManager nacosServiceManagerInstance; + + @Before + public void setUp() { + // Obtain an instance of NacosServiceManager before running tests + nacosServiceManagerInstance = NacosServiceManager.getInstance(); + } + + @Test + public void testGetInstance_ShouldReturnSameInstance() { + // Act + NacosServiceManager anotherInstance = NacosServiceManager.getInstance(); + + // Assert + assertNotNull("Instance should not be null", nacosServiceManagerInstance); + assertNotNull("Another instance should not be null", anotherInstance); + assertEquals("Both instances should be the same", nacosServiceManagerInstance, anotherInstance); + } + + @Test + public void testGetInstance_ShouldBeThreadSafe() { + // Arrange + final int threadCount = 100; + Thread[] threads = new Thread[threadCount]; + NacosServiceManager[] instances = new NacosServiceManager[threadCount]; + + for (int i = 0; i < threadCount; i++) { + int finalI = i; + threads[i] = new Thread(() -> { + instances[finalI] = NacosServiceManager.getInstance(); + }); + } + + // Act + for (Thread thread : threads) { + thread.start(); + } + + try { + for (Thread thread : threads) { + thread.join(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Assert + for (int i = 0; i < threadCount; i++) { + assertNotNull("Instance should not be null", instances[i]); + assertEquals("All instances should be the same", instances[0], instances[i]); + } + } +} diff --git a/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/rule/NacosWeightRandomRuleTest.java b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/rule/NacosWeightRandomRuleTest.java new file mode 100644 index 0000000000..cf6c975191 --- /dev/null +++ b/sermant-plugins/sermant-springboot-registry/springboot-registry-service/src/test/java/io/sermant/discovery/service/lb/rule/NacosWeightRandomRuleTest.java @@ -0,0 +1,80 @@ +package io.sermant.discovery.service.lb.rule; + +import java.util.*; +import java.math.*; + +import com.alibaba.nacos.api.naming.pojo.Instance; +import io.sermant.discovery.entity.ServiceInstance; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class NacosWeightRandomRuleTest { + + @InjectMocks + private NacosWeightRandomRule nacosWeightRandomRule; + + private List instances; + + @Before + public void setUp() { + instances = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + ServiceInstance instance = mock(ServiceInstance.class); + Map metadata = new ConcurrentHashMap<>(); + metadata.put("nacos.weight", String.valueOf(1)); + when(instance.getMetadata()).thenReturn(metadata); + instances.add(instance); + } + instances.get(0).getMetadata().put("nacos.weight", "1"); + instances.get(1).getMetadata().put("nacos.weight", "2"); + instances.get(2).getMetadata().put("nacos.weight", "3"); + instances.get(3).getMetadata().put("nacos.weight", "4"); + instances.get(4).getMetadata().put("nacos.weight", "5"); + } + + @Test + public void testDoChoose_WithWeights_ShouldSelectInstancesAccordingToWeights() throws InterruptedException { + int count = 10000; + CountDownLatch countDownLatch = new CountDownLatch(count); + ExecutorService executorService = Executors.newFixedThreadPool(10); + Map instanceCount = new ConcurrentHashMap<>(); + + for (int i = 0; i < count; i++) { + executorService.submit(() -> { + try { + ServiceInstance instance = nacosWeightRandomRule.doChoose("testService", instances); + instanceCount.put(instance, instanceCount.getOrDefault(instance, 0) + 1); + } finally { + countDownLatch.countDown(); + } + }); + } + countDownLatch.await(); + executorService.shutdown(); + + assertEquals(10000, countDownLatch.getCount()); + assertEquals(5, instanceCount.size()); + + // Validate the selection ratio according to weights + assertEquals(1000, instanceCount.get(instances.get(0)), 100); + assertEquals(2000, instanceCount.get(instances.get(1)), 100); + assertEquals(3000, instanceCount.get(instances.get(2)), 100); + assertEquals(4000, instanceCount.get(instances.get(3)), 100); + assertEquals(5000, instanceCount.get(instances.get(4)), 100); + } +}