diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/utils/NamingUtils.java b/api/src/main/java/com/alibaba/nacos/api/naming/utils/NamingUtils.java index 9faa8b40bf9..94708e250d3 100644 --- a/api/src/main/java/com/alibaba/nacos/api/naming/utils/NamingUtils.java +++ b/api/src/main/java/com/alibaba/nacos/api/naming/utils/NamingUtils.java @@ -54,4 +54,25 @@ public static String getGroupName(final String serviceNameWithGroup) { } return serviceNameWithGroup.split(Constants.SERVICE_INFO_SPLITER)[0]; } + + /** + * check combineServiceName format. the serviceName can't be blank. some relational logic in {@link + * com.alibaba.nacos.naming.web.DistroFilter#doFilter}, it will handle combineServiceName in some case, you should + * know it. + *
+     * serviceName = "@@"; the length = 0; illegal
+     * serviceName = "group@@"; the length = 1; illegal
+     * serviceName = "@@serviceName"; the length = 2; legal
+     * serviceName = "group@@serviceName"; the length = 2; legal
+     * 
+ * + * @param combineServiceName such as: groupName@@serviceName + */ + public static void checkServiceNameFormat(String combineServiceName) { + String[] split = combineServiceName.split(Constants.SERVICE_INFO_SPLITER); + if (split.length <= 1) { + throw new IllegalArgumentException( + "Param 'serviceName' is illegal, it should be format as 'groupName@@serviceName'"); + } + } } diff --git a/common/src/main/java/com/alibaba/nacos/common/utils/MapUtils.java b/common/src/main/java/com/alibaba/nacos/common/utils/MapUtils.java index 9161bc10717..7febf7eda05 100644 --- a/common/src/main/java/com/alibaba/nacos/common/utils/MapUtils.java +++ b/common/src/main/java/com/alibaba/nacos/common/utils/MapUtils.java @@ -129,11 +129,11 @@ public static void putIfValNoEmpty(Map target, Object key, Object value) { /** * ComputeIfAbsent lazy load. * - * @param target target Map data. - * @param key map key. + * @param target target Map data. + * @param key map key. * @param mappingFunction funtion which is need to be executed. - * @param param1 function's parameter value1. - * @param param2 function's parameter value1. + * @param param1 function's parameter value1. + * @param param2 function's parameter value1. * @return */ @NotThreadSafe @@ -153,5 +153,4 @@ public static Object computeIfAbsent(Map target, Object key, BiFunction mappingF } return val; } - } diff --git a/naming/src/main/java/com/alibaba/nacos/naming/controllers/InstanceController.java b/naming/src/main/java/com/alibaba/nacos/naming/controllers/InstanceController.java index 5f1ae15a362..5746b2f7df3 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/controllers/InstanceController.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/controllers/InstanceController.java @@ -34,15 +34,16 @@ import com.alibaba.nacos.naming.misc.SwitchDomain; import com.alibaba.nacos.naming.misc.SwitchEntry; import com.alibaba.nacos.naming.misc.UtilsAndCommons; +import com.alibaba.nacos.naming.pojo.InstanceOperationContext; +import com.alibaba.nacos.naming.pojo.InstanceOperationInfo; import com.alibaba.nacos.naming.push.ClientInfo; import com.alibaba.nacos.naming.push.DataSource; import com.alibaba.nacos.naming.push.PushService; import com.alibaba.nacos.naming.web.CanDistro; -import com.alibaba.nacos.naming.web.DistroFilter; import com.alibaba.nacos.naming.web.NamingResourceParser; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; - import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; @@ -64,6 +65,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; + +import static com.alibaba.nacos.naming.misc.UtilsAndCommons.DEFAULT_CLUSTER_NAME; +import static com.alibaba.nacos.naming.misc.UtilsAndCommons.EPHEMERAL; +import static com.alibaba.nacos.naming.misc.UtilsAndCommons.PERSIST; +import static com.alibaba.nacos.naming.misc.UtilsAndCommons.UPDATE_INSTANCE_METADATA_ACTION_REMOVE; +import static com.alibaba.nacos.naming.misc.UtilsAndCommons.UPDATE_INSTANCE_METADATA_ACTION_UPDATE; /** * Instance operation controller. @@ -119,7 +127,7 @@ public String register(HttpServletRequest request) throws Exception { final String namespaceId = WebUtils .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); - checkServiceNameFormat(serviceName); + NamingUtils.checkServiceNameFormat(serviceName); final Instance instance = parseInstance(request); @@ -141,7 +149,7 @@ public String deregister(HttpServletRequest request) throws Exception { Instance instance = getIpAddress(request); String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); - checkServiceNameFormat(serviceName); + NamingUtils.checkServiceNameFormat(serviceName); Service service = serviceManager.getService(namespaceId, serviceName); if (service == null) { @@ -167,7 +175,7 @@ public String update(HttpServletRequest request) throws Exception { final String namespaceId = WebUtils .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); - checkServiceNameFormat(serviceName); + NamingUtils.checkServiceNameFormat(serviceName); final Instance instance = parseInstance(request); String agent = WebUtils.getUserAgent(request); @@ -183,6 +191,125 @@ public String update(HttpServletRequest request) throws Exception { return "ok"; } + /** + * Batch update instance's metadata. old key exist = update, old key not exist = add. + * + * @param request http request + * @return success updated instances. such as '{"updated":["2.2.2.2:8080:unknown:xxxx-cluster:ephemeral"}'. + * @throws Exception any error during update + * @since 1.4.0 + */ + @CanDistro + @PutMapping(value = "/metadata/batch") + @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE) + public ObjectNode batchUpdateInstanceMatadata(HttpServletRequest request) throws Exception { + final String namespaceId = WebUtils + .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); + + String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); + + String consistencyType = WebUtils.optional(request, "consistencyType", StringUtils.EMPTY); + + String instances = WebUtils.optional(request, "instances", StringUtils.EMPTY); + + List targetInstances = parseBatchInstances(instances); + + String metadata = WebUtils.required(request, "metadata"); + Map targetMetadata = UtilsAndCommons.parseMetadata(metadata); + + List operatedInstances = batchOperateMetadata(namespaceId, + buildOperationInfo(serviceName, consistencyType, targetInstances), targetMetadata, + UPDATE_INSTANCE_METADATA_ACTION_UPDATE); + + ObjectNode result = JacksonUtils.createEmptyJsonNode(); + ArrayNode ipArray = JacksonUtils.createEmptyArrayNode(); + + for (Instance ip : operatedInstances) { + ipArray.add(ip.getDatumKey() + ":" + (ip.isEphemeral() ? EPHEMERAL : PERSIST)); + } + + result.replace("updated", ipArray); + return result; + } + + /** + * Batch delete instance's metadata. old key exist = delete, old key not exist = not operate + * + * @param request http request + * @return success updated instances. such as '{"updated":["2.2.2.2:8080:unknown:xxxx-cluster:ephemeral"}'. + * @throws Exception any error during update + * @since 1.4.0 + */ + @CanDistro + @DeleteMapping("/metadata/batch") + @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE) + public ObjectNode batchDeleteInstanceMatadata(HttpServletRequest request) throws Exception { + final String namespaceId = WebUtils + .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); + + String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); + + String consistencyType = WebUtils.optional(request, "consistencyType", StringUtils.EMPTY); + + String instances = WebUtils.optional(request, "instances", StringUtils.EMPTY); + + List targetInstances = parseBatchInstances(instances); + + String metadata = WebUtils.required(request, "metadata"); + Map targetMetadata = UtilsAndCommons.parseMetadata(metadata); + + List operatedInstances = batchOperateMetadata(namespaceId, + buildOperationInfo(serviceName, consistencyType, targetInstances), targetMetadata, + UPDATE_INSTANCE_METADATA_ACTION_REMOVE); + + ObjectNode result = JacksonUtils.createEmptyJsonNode(); + ArrayNode ipArray = JacksonUtils.createEmptyArrayNode(); + + for (Instance ip : operatedInstances) { + ipArray.add(ip.getDatumKey() + ":" + (ip.isEphemeral() ? EPHEMERAL : PERSIST)); + } + + result.replace("updated", ipArray); + return result; + } + + private InstanceOperationInfo buildOperationInfo(String serviceName, String consistencyType, + List instances) { + if (!CollectionUtils.isEmpty(instances)) { + for (Instance instance : instances) { + if (StringUtils.isBlank(instance.getClusterName())) { + instance.setClusterName(DEFAULT_CLUSTER_NAME); + } + } + } + return new InstanceOperationInfo(serviceName, consistencyType, instances); + } + + private List parseBatchInstances(String instances) { + try { + return JacksonUtils.toObj(instances, new TypeReference>() { + }); + } catch (Exception e) { + Loggers.SRV_LOG.warn("UPDATE-METADATA: Param 'instances' is illegal, ignore this operation", e); + } + return null; + } + + private List batchOperateMetadata(String namespace, InstanceOperationInfo instanceOperationInfo, + Map metadata, String action) { + Function> operateFunction = instanceOperationContext -> { + try { + return serviceManager.updateMetadata(instanceOperationContext.getNamespace(), + instanceOperationContext.getServiceName(), instanceOperationContext.getEphemeral(), action, + instanceOperationContext.getAll(), instanceOperationContext.getInstances(), metadata); + } catch (NacosException e) { + Loggers.SRV_LOG.warn("UPDATE-METADATA: updateMetadata failed", e); + } + return new ArrayList<>(); + }; + return serviceManager.batchOperate(namespace, instanceOperationInfo, operateFunction); + } + /** * Patch instance. * @@ -196,7 +323,7 @@ public String update(HttpServletRequest request) throws Exception { public String patch(HttpServletRequest request) throws Exception { String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); - checkServiceNameFormat(serviceName); + NamingUtils.checkServiceNameFormat(serviceName); String ip = WebUtils.required(request, "ip"); String port = WebUtils.required(request, "port"); String cluster = WebUtils.optional(request, CommonParams.CLUSTER_NAME, StringUtils.EMPTY); @@ -248,7 +375,7 @@ public ObjectNode list(HttpServletRequest request) throws Exception { String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); - checkServiceNameFormat(serviceName); + NamingUtils.checkServiceNameFormat(serviceName); String agent = WebUtils.getUserAgent(request); String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY); @@ -280,7 +407,7 @@ public ObjectNode detail(HttpServletRequest request) throws Exception { String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); - checkServiceNameFormat(serviceName); + NamingUtils.checkServiceNameFormat(serviceName); String cluster = WebUtils.optional(request, CommonParams.CLUSTER_NAME, UtilsAndCommons.DEFAULT_CLUSTER_NAME); String ip = WebUtils.required(request, "ip"); int port = Integer.parseInt(WebUtils.required(request, "port")); @@ -353,7 +480,7 @@ public ObjectNode beat(HttpServletRequest request) throws Exception { } String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); - checkServiceNameFormat(serviceName); + NamingUtils.checkServiceNameFormat(serviceName); Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}", clientBeat, serviceName); Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port); @@ -421,7 +548,7 @@ public ObjectNode listWithHealthStatus(@RequestParam String key) throws NacosExc namespaceId = Constants.DEFAULT_NAMESPACE_ID; serviceName = key; } - checkServiceNameFormat(serviceName); + NamingUtils.checkServiceNameFormat(serviceName); Service service = serviceManager.getService(namespaceId, serviceName); if (service == null) { @@ -441,26 +568,6 @@ public ObjectNode listWithHealthStatus(@RequestParam String key) throws NacosExc return result; } - /** - * check combineServiceName format. the serviceName can't be blank. some relational logic in {@link - * DistroFilter#doFilter}, it will handle combineServiceName in some case, you should know it. - *
-     * serviceName = "@@"; the length = 0; illegal
-     * serviceName = "group@@"; the length = 1; illegal
-     * serviceName = "@@serviceName"; the length = 2; legal
-     * serviceName = "group@@serviceName"; the length = 2; legal
-     * 
- * - * @param combineServiceName such as: groupName@@serviceName - */ - private void checkServiceNameFormat(String combineServiceName) { - String[] split = combineServiceName.split(Constants.SERVICE_INFO_SPLITER); - if (split.length <= 1) { - throw new IllegalArgumentException( - "Param 'serviceName' is illegal, it should be format as 'groupName@@serviceName"); - } - } - private Instance parseInstance(HttpServletRequest request) throws Exception { String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); @@ -483,12 +590,7 @@ private Instance parseInstance(HttpServletRequest request) throws Exception { } private Instance getIpAddress(HttpServletRequest request) { - final String ip = WebUtils.required(request, "ip"); - final String port = WebUtils.required(request, "port"); - String cluster = WebUtils.optional(request, CommonParams.CLUSTER_NAME, StringUtils.EMPTY); - if (StringUtils.isBlank(cluster)) { - cluster = WebUtils.optional(request, "cluster", UtilsAndCommons.DEFAULT_CLUSTER_NAME); - } + String enabledString = WebUtils.optional(request, "enabled", StringUtils.EMPTY); boolean enabled; if (StringUtils.isBlank(enabledString)) { @@ -497,20 +599,33 @@ private Instance getIpAddress(HttpServletRequest request) { enabled = BooleanUtils.toBoolean(enabledString); } - boolean ephemeral = BooleanUtils.toBoolean( - WebUtils.optional(request, "ephemeral", String.valueOf(switchDomain.isDefaultInstanceEphemeral()))); - String weight = WebUtils.optional(request, "weight", "1"); boolean healthy = BooleanUtils.toBoolean(WebUtils.optional(request, "healthy", "true")); - Instance instance = new Instance(); - instance.setPort(Integer.parseInt(port)); - instance.setIp(ip); + Instance instance = getBasicIpAddress(request); instance.setWeight(Double.parseDouble(weight)); - instance.setClusterName(cluster); instance.setHealthy(healthy); instance.setEnabled(enabled); + + return instance; + } + + private Instance getBasicIpAddress(HttpServletRequest request) { + + final String ip = WebUtils.required(request, "ip"); + final String port = WebUtils.required(request, "port"); + String cluster = WebUtils.optional(request, CommonParams.CLUSTER_NAME, StringUtils.EMPTY); + if (StringUtils.isBlank(cluster)) { + cluster = WebUtils.optional(request, "cluster", UtilsAndCommons.DEFAULT_CLUSTER_NAME); + } + boolean ephemeral = BooleanUtils.toBoolean( + WebUtils.optional(request, "ephemeral", String.valueOf(switchDomain.isDefaultInstanceEphemeral()))); + + Instance instance = new Instance(); + instance.setPort(Integer.parseInt(port)); + instance.setIp(ip); instance.setEphemeral(ephemeral); + instance.setClusterName(cluster); return instance; } diff --git a/naming/src/main/java/com/alibaba/nacos/naming/controllers/OperatorController.java b/naming/src/main/java/com/alibaba/nacos/naming/controllers/OperatorController.java index 180c931415f..d67a46c7f4c 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/controllers/OperatorController.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/controllers/OperatorController.java @@ -23,7 +23,6 @@ import com.alibaba.nacos.core.cluster.Member; import com.alibaba.nacos.core.cluster.NodeState; import com.alibaba.nacos.core.cluster.ServerMemberManager; -import com.alibaba.nacos.sys.utils.ApplicationUtils; import com.alibaba.nacos.naming.cluster.ServerListManager; import com.alibaba.nacos.naming.cluster.ServerStatusManager; import com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore; @@ -36,10 +35,10 @@ import com.alibaba.nacos.naming.misc.SwitchManager; import com.alibaba.nacos.naming.misc.UtilsAndCommons; import com.alibaba.nacos.naming.push.PushService; +import com.alibaba.nacos.sys.utils.ApplicationUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; - import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; diff --git a/naming/src/main/java/com/alibaba/nacos/naming/core/ServiceManager.java b/naming/src/main/java/com/alibaba/nacos/naming/core/ServiceManager.java index e5d82c89ab5..859c8fb3ea4 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/core/ServiceManager.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/core/ServiceManager.java @@ -36,6 +36,8 @@ import com.alibaba.nacos.naming.misc.SwitchDomain; import com.alibaba.nacos.naming.misc.Synchronizer; import com.alibaba.nacos.naming.misc.UtilsAndCommons; +import com.alibaba.nacos.naming.pojo.InstanceOperationContext; +import com.alibaba.nacos.naming.pojo.InstanceOperationInfo; import com.alibaba.nacos.naming.push.PushService; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -61,9 +63,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.alibaba.nacos.naming.misc.UtilsAndCommons.UPDATE_INSTANCE_METADATA_ACTION_REMOVE; +import static com.alibaba.nacos.naming.misc.UtilsAndCommons.UPDATE_INSTANCE_METADATA_ACTION_UPDATE; + /** * Core manager storing all services in Nacos. * @@ -524,6 +530,103 @@ public void updateInstance(String namespaceId, String serviceName, Instance inst addInstance(namespaceId, serviceName, instance.isEphemeral(), instance); } + /** + * Update instance's metadata. + * + * @param namespaceId namespace + * @param serviceName service name + * @param action update or remove + * @param ips need update instances + * @param metadata target metadata + * @return update succeed instances + * @throws NacosException nacos exception + */ + public List updateMetadata(String namespaceId, String serviceName, boolean isEphemeral, String action, + boolean all, List ips, Map metadata) throws NacosException { + + Service service = getService(namespaceId, serviceName); + + if (service == null) { + throw new NacosException(NacosException.INVALID_PARAM, + "service not found, namespace: " + namespaceId + ", service: " + serviceName); + } + + List locatedInstance = getLocatedInstance(namespaceId, serviceName, isEphemeral, all, ips); + + if (CollectionUtils.isEmpty(locatedInstance)) { + throw new NacosException(NacosException.INVALID_PARAM, "not locate instances, input instances: " + ips); + } + + if (UPDATE_INSTANCE_METADATA_ACTION_UPDATE.equals(action)) { + locatedInstance.forEach(ele -> ele.getMetadata().putAll(metadata)); + } else if (UPDATE_INSTANCE_METADATA_ACTION_REMOVE.equals(action)) { + Set removeKeys = metadata.keySet(); + for (String removeKey : removeKeys) { + locatedInstance.forEach(ele -> ele.getMetadata().remove(removeKey)); + } + } + Instance[] instances = new Instance[locatedInstance.size()]; + locatedInstance.toArray(instances); + + addInstance(namespaceId, serviceName, isEphemeral, instances); + + return locatedInstance; + } + + /** + * locate consistency's datum by all or instances provided. + * + * @param namespaceId namespace + * @param serviceName serviceName + * @param isEphemeral isEphemeral + * @param all get from consistencyService directly + * @param waitLocateInstance instances provided + * @return located instances + * @throws NacosException nacos exception + */ + public List getLocatedInstance(String namespaceId, String serviceName, boolean isEphemeral, boolean all, + List waitLocateInstance) throws NacosException { + List locatedInstance; + + //need the newest data from consistencyService + Datum datum = consistencyService.get(KeyBuilder.buildInstanceListKey(namespaceId, serviceName, isEphemeral)); + if (datum == null) { + throw new NacosException(NacosException.NOT_FOUND, + "instances from consistencyService not exist, namespace: " + namespaceId + ", service: " + + serviceName + ", ephemeral: " + isEphemeral); + } + + if (all) { + locatedInstance = ((Instances) datum.value).getInstanceList(); + } else { + locatedInstance = new ArrayList<>(); + for (Instance instance : waitLocateInstance) { + Instance located = locateInstance(((Instances) datum.value).getInstanceList(), instance); + if (located == null) { + continue; + } + locatedInstance.add(located); + } + } + + return locatedInstance; + } + + private Instance locateInstance(List instances, Instance instance) { + int target = 0; + while (target >= 0) { + target = instances.indexOf(instance); + if (target >= 0) { + Instance result = instances.get(target); + if (result.getClusterName().equals(instance.getClusterName())) { + return result; + } + instances.remove(target); + } + } + return null; + } + /** * Add instance to service. * @@ -604,6 +707,59 @@ public Instance getInstance(String namespaceId, String serviceName, String clust return null; } + /** + * batch operate kinds of resources. + * + * @param namespace namespace. + * @param operationInfo operation resources description. + * @param operateFunction some operation defined by kinds of situation. + */ + public List batchOperate(String namespace, InstanceOperationInfo operationInfo, + Function> operateFunction) { + List operatedInstances = new ArrayList<>(); + try { + String serviceName = operationInfo.getServiceName(); + NamingUtils.checkServiceNameFormat(serviceName); + // type: ephemeral/persist + InstanceOperationContext operationContext; + String type = operationInfo.getConsistencyType(); + if (!StringUtils.isEmpty(type)) { + switch (type) { + case UtilsAndCommons.EPHEMERAL: + operationContext = new InstanceOperationContext(namespace, serviceName, true, true); + operatedInstances.addAll(operateFunction.apply(operationContext)); + break; + case UtilsAndCommons.PERSIST: + operationContext = new InstanceOperationContext(namespace, serviceName, false, true); + operatedInstances.addAll(operateFunction.apply(operationContext)); + break; + default: + Loggers.SRV_LOG + .warn("UPDATE-METADATA: services.all value is illegal, it should be ephemeral/persist. ignore the service '" + + serviceName + "'"); + break; + } + } else { + List instances = operationInfo.getInstances(); + if (!CollectionUtils.isEmpty(instances)) { + //ephemeral:instances or persist:instances + Map> instanceMap = instances.stream() + .collect(Collectors.groupingBy(ele -> ele.isEphemeral())); + + for (Map.Entry> entry : instanceMap.entrySet()) { + operationContext = new InstanceOperationContext(namespace, serviceName, entry.getKey(), false, + entry.getValue()); + operatedInstances.addAll(operateFunction.apply(operationContext)); + } + } + } + } catch (Exception e) { + Loggers.SRV_LOG.warn("UPDATE-METADATA: update metadata failed, ignore the service '" + operationInfo + .getServiceName() + "'", e); + } + return operatedInstances; + } + /** * Compare and get new instance list. * diff --git a/naming/src/main/java/com/alibaba/nacos/naming/misc/UtilsAndCommons.java b/naming/src/main/java/com/alibaba/nacos/naming/misc/UtilsAndCommons.java index bc973da4c8d..c3399b93ce8 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/misc/UtilsAndCommons.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/misc/UtilsAndCommons.java @@ -20,9 +20,9 @@ import com.alibaba.nacos.api.selector.SelectorType; import com.alibaba.nacos.common.utils.JacksonUtils; import com.alibaba.nacos.common.utils.VersionUtils; -import com.alibaba.nacos.sys.utils.ApplicationUtils; import com.alibaba.nacos.naming.selector.LabelSelector; import com.alibaba.nacos.naming.selector.NoneSelector; +import com.alibaba.nacos.sys.utils.ApplicationUtils; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.StringUtils; @@ -115,6 +115,14 @@ public class UtilsAndCommons { public static final String UPDATE_INSTANCE_ACTION_REMOVE = "remove"; + public static final String UPDATE_INSTANCE_METADATA_ACTION_UPDATE = "update"; + + public static final String UPDATE_INSTANCE_METADATA_ACTION_REMOVE = "remove"; + + public static final String EPHEMERAL = "ephemeral"; + + public static final String PERSIST = "persist"; + public static final String DATA_BASE_DIR = ApplicationUtils.getNacosHome() + File.separator + "data" + File.separator + "naming"; diff --git a/naming/src/main/java/com/alibaba/nacos/naming/pojo/InstanceOperationContext.java b/naming/src/main/java/com/alibaba/nacos/naming/pojo/InstanceOperationContext.java new file mode 100644 index 00000000000..7d1ad3b4508 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/pojo/InstanceOperationContext.java @@ -0,0 +1,79 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.nacos.naming.pojo; + +import com.alibaba.nacos.naming.core.Instance; + +import java.util.List; + +/** + * InstanceOperationContext. used in instance batch operation's consumer. + * + * @author horizonzy + * @since 1.4.0 + */ +public class InstanceOperationContext { + + public InstanceOperationContext() { + } + + public InstanceOperationContext(String namespace, String serviceName, Boolean ephemeral, Boolean all) { + this.namespace = namespace; + this.serviceName = serviceName; + this.ephemeral = ephemeral; + this.all = all; + } + + public InstanceOperationContext(String namespace, String serviceName, Boolean ephemeral, Boolean all, + List instances) { + this.namespace = namespace; + this.serviceName = serviceName; + this.ephemeral = ephemeral; + this.all = all; + this.instances = instances; + } + + private String namespace; + + private String serviceName; + + private Boolean ephemeral; + + private Boolean all; + + private List instances; + + public String getNamespace() { + return namespace; + } + + public String getServiceName() { + return serviceName; + } + + public Boolean getEphemeral() { + return ephemeral; + } + + public Boolean getAll() { + return all; + } + + public List getInstances() { + return instances; + } +} diff --git a/naming/src/main/java/com/alibaba/nacos/naming/pojo/InstanceOperationInfo.java b/naming/src/main/java/com/alibaba/nacos/naming/pojo/InstanceOperationInfo.java new file mode 100644 index 00000000000..56e726194a9 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/pojo/InstanceOperationInfo.java @@ -0,0 +1,71 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.nacos.naming.pojo; + +import com.alibaba.nacos.naming.core.Instance; + +import java.util.List; + +/** + * InstanceOperationInfo. operation resources's description + * + * @author horizonzy + * @since 1.4.0 + */ +public class InstanceOperationInfo { + + public InstanceOperationInfo() { + } + + public InstanceOperationInfo(String serviceName, String consistencyType, List instances) { + this.serviceName = serviceName; + this.consistencyType = consistencyType; + this.instances = instances; + } + + /** + * serverName. + */ + private String serviceName; + + /** + * consistencyType. it helps to operate all instances from consistencyService, value = ephemeral or persist. + *

+ * ephemeral = all ephemeral instances in {@link com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl} + * persist = all persist instances in {@link com.alibaba.nacos.naming.consistency.persistent.raft.RaftConsistencyServiceImpl} + *

+ */ + private String consistencyType; + + /** + * instances which need operate. + */ + private List instances; + + public String getServiceName() { + return serviceName; + } + + public String getConsistencyType() { + return consistencyType; + } + + public List getInstances() { + return instances; + } + +} diff --git a/naming/src/test/java/com/alibaba/nacos/naming/BaseTest.java b/naming/src/test/java/com/alibaba/nacos/naming/BaseTest.java index a81502b5b06..8b32f7c2eaa 100644 --- a/naming/src/test/java/com/alibaba/nacos/naming/BaseTest.java +++ b/naming/src/test/java/com/alibaba/nacos/naming/BaseTest.java @@ -16,7 +16,6 @@ package com.alibaba.nacos.naming; -import com.alibaba.nacos.sys.utils.ApplicationUtils; import com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore; import com.alibaba.nacos.naming.consistency.persistent.raft.RaftPeer; import com.alibaba.nacos.naming.consistency.persistent.raft.RaftPeerSet; @@ -26,6 +25,7 @@ import com.alibaba.nacos.naming.misc.NetUtils; import com.alibaba.nacos.naming.misc.SwitchDomain; import com.alibaba.nacos.naming.push.PushService; +import com.alibaba.nacos.sys.utils.ApplicationUtils; import org.junit.Before; import org.junit.Rule; import org.junit.rules.ExpectedException; @@ -44,7 +44,7 @@ public class BaseTest { protected static final String TEST_CLUSTER_NAME = "test-cluster"; - protected static final String TEST_SERVICE_NAME = "test-service"; + protected static final String TEST_SERVICE_NAME = "DEFAULT_GROUP@@test-service"; protected static final String TEST_GROUP_NAME = "test-group-name"; diff --git a/naming/src/test/java/com/alibaba/nacos/naming/controllers/InstanceControllerTest.java b/naming/src/test/java/com/alibaba/nacos/naming/controllers/InstanceControllerTest.java index d8b859c0b54..173cc82698f 100644 --- a/naming/src/test/java/com/alibaba/nacos/naming/controllers/InstanceControllerTest.java +++ b/naming/src/test/java/com/alibaba/nacos/naming/controllers/InstanceControllerTest.java @@ -24,12 +24,13 @@ import com.alibaba.nacos.naming.core.Instance; import com.alibaba.nacos.naming.core.Service; import com.alibaba.nacos.naming.misc.UtilsAndCommons; +import com.alibaba.nacos.naming.pojo.InstanceOperationInfo; import com.fasterxml.jackson.databind.JsonNode; - import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -44,7 +45,11 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.function.Function; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = MockServletContext.class) @@ -62,6 +67,7 @@ public class InstanceControllerTest extends BaseTest { @Before public void before() { super.before(); + mockInjectPushServer(); mockmvc = MockMvcBuilders.standaloneSetup(instanceController).build(); } @@ -84,7 +90,7 @@ public void registerInstance() throws Exception { Mockito.when(serviceManager.getService(Constants.DEFAULT_NAMESPACE_ID, TEST_SERVICE_NAME)).thenReturn(service); MockHttpServletRequestBuilder builder = MockMvcRequestBuilders - .put(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance").param("serviceName", TEST_SERVICE_NAME) + .post(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance").param("serviceName", TEST_SERVICE_NAME) .param("ip", "1.1.1.1").param("port", "9999"); String actualValue = mockmvc.perform(builder).andReturn().getResponse().getContentAsString(); @@ -156,4 +162,94 @@ public void getNullServiceInstances() throws Exception { JsonNode hosts = result.get("hosts"); Assert.assertEquals(hosts.size(), 0); } + + @Test + public void batchUpdateMetadata() throws Exception { + Instance instance = new Instance("1.1.1.1", 8080, TEST_CLUSTER_NAME); + instance.setServiceName(TEST_SERVICE_NAME); + Map metadata = new HashMap<>(); + metadata.put("key1", "value1"); + instance.setMetadata(metadata); + + Instance instance2 = new Instance("2.2.2.2", 8080, TEST_CLUSTER_NAME); + instance2.setServiceName(TEST_SERVICE_NAME); + + List instanceList = new LinkedList<>(); + instanceList.add(instance); + instanceList.add(instance2); + + Mockito.when(serviceManager + .batchOperate(ArgumentMatchers.anyString(), ArgumentMatchers.any(InstanceOperationInfo.class), + ArgumentMatchers.any(Function.class))).thenReturn(instanceList); + + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders + .put(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance/metadata/batch").param("namespace", "public") + .param("serviceName", TEST_SERVICE_NAME).param("instances", + "[{\"ip\":\"1.1.1.1\",\"port\": \"8080\",\"ephemeral\":\"true\",\"clusterName\":\"test-cluster\"}," + + "{\"ip\":\"2.2.2.2\",\"port\":\"8080\",\"ephemeral\":\"true\",\"clusterName\":\"test-cluster\"}]") + .param("metadata", "{\"age\":\"20\",\"name\":\"horizon\"}"); + + String actualValue = mockmvc.perform(builder).andReturn().getResponse().getContentAsString(); + + JsonNode result = JacksonUtils.toObj(actualValue); + + JsonNode updated = result.get("updated"); + + Assert.assertEquals(updated.size(), 2); + + Assert.assertTrue(updated.get(0).asText().contains("1.1.1.1")); + Assert.assertTrue(updated.get(0).asText().contains("8080")); + Assert.assertTrue(updated.get(0).asText().contains(TEST_CLUSTER_NAME)); + Assert.assertTrue(updated.get(0).asText().contains("ephemeral")); + + Assert.assertTrue(updated.get(1).asText().contains("2.2.2.2")); + Assert.assertTrue(updated.get(1).asText().contains("8080")); + Assert.assertTrue(updated.get(1).asText().contains(TEST_CLUSTER_NAME)); + Assert.assertTrue(updated.get(1).asText().contains("ephemeral")); + } + + @Test + public void batchDeleteMetadata() throws Exception { + Instance instance = new Instance("1.1.1.1", 8080, TEST_CLUSTER_NAME); + instance.setServiceName(TEST_SERVICE_NAME); + Map metadata = new HashMap<>(); + metadata.put("key1", "value1"); + instance.setMetadata(metadata); + + Instance instance2 = new Instance("2.2.2.2", 8080, TEST_CLUSTER_NAME); + instance2.setServiceName(TEST_SERVICE_NAME); + + List instanceList = new LinkedList<>(); + instanceList.add(instance); + instanceList.add(instance2); + + Mockito.when(serviceManager + .batchOperate(ArgumentMatchers.anyString(), ArgumentMatchers.any(InstanceOperationInfo.class), + ArgumentMatchers.any(Function.class))).thenReturn(instanceList); + + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders + .delete(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance/metadata/batch").param("namespace", "public") + .param("serviceName", TEST_SERVICE_NAME).param("instances", + "[{\"ip\":\"1.1.1.1\",\"port\": \"8080\",\"ephemeral\":\"true\",\"clusterName\":\"test-cluster\"}," + + "{\"ip\":\"2.2.2.2\",\"port\":\"8080\",\"ephemeral\":\"true\",\"clusterName\":\"test-cluster\"}]") + .param("metadata", "{\"age\":\"20\",\"name\":\"horizon\"}"); + + String actualValue = mockmvc.perform(builder).andReturn().getResponse().getContentAsString(); + + JsonNode result = JacksonUtils.toObj(actualValue); + + JsonNode updated = result.get("updated"); + + Assert.assertEquals(updated.size(), 2); + + Assert.assertTrue(updated.get(0).asText().contains("1.1.1.1")); + Assert.assertTrue(updated.get(0).asText().contains("8080")); + Assert.assertTrue(updated.get(0).asText().contains(TEST_CLUSTER_NAME)); + Assert.assertTrue(updated.get(0).asText().contains("ephemeral")); + + Assert.assertTrue(updated.get(1).asText().contains("2.2.2.2")); + Assert.assertTrue(updated.get(1).asText().contains("8080")); + Assert.assertTrue(updated.get(1).asText().contains(TEST_CLUSTER_NAME)); + Assert.assertTrue(updated.get(1).asText().contains("ephemeral")); + } } diff --git a/naming/src/test/java/com/alibaba/nacos/naming/core/ServiceManagerTest.java b/naming/src/test/java/com/alibaba/nacos/naming/core/ServiceManagerTest.java index d51bb4bffda..ff02d014829 100644 --- a/naming/src/test/java/com/alibaba/nacos/naming/core/ServiceManagerTest.java +++ b/naming/src/test/java/com/alibaba/nacos/naming/core/ServiceManagerTest.java @@ -30,6 +30,7 @@ import com.alibaba.nacos.naming.misc.Message; import com.alibaba.nacos.naming.misc.Synchronizer; import com.alibaba.nacos.naming.misc.UtilsAndCommons; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.junit.Assert; import org.junit.Before; @@ -38,10 +39,14 @@ import org.springframework.test.util.ReflectionTestUtils; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import static com.alibaba.nacos.naming.misc.UtilsAndCommons.UPDATE_INSTANCE_METADATA_ACTION_REMOVE; +import static com.alibaba.nacos.naming.misc.UtilsAndCommons.UPDATE_INSTANCE_METADATA_ACTION_UPDATE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -97,6 +102,9 @@ private void mockCluster() { private void mockInstance() { instance = new Instance("1.1.1.1", 1, TEST_CLUSTER_NAME); + Map metadata = new HashMap<>(); + metadata.put("key1", "value1"); + instance.setMetadata(metadata); instance2 = new Instance("2.2.2.2", 2); } @@ -214,6 +222,76 @@ public void testUpdateInstance() throws NacosException { verify(consistencyService).put(eq(instanceListKey), any(Instances.class)); } + @Test + public void testUpdateMetadata() throws NacosException { + + serviceManager.createEmptyService(TEST_NAMESPACE, TEST_SERVICE_NAME, true); + + List instanceList = new LinkedList<>(); + Datum datam = new Datum(); + datam.key = KeyBuilder.buildInstanceListKey(TEST_NAMESPACE, TEST_SERVICE_NAME, true); + Instances instances = new Instances(); + instanceList.add(instance); + instanceList.add(instance2); + instances.setInstanceList(instanceList); + datam.value = instances; + when(consistencyService.get(KeyBuilder.buildInstanceListKey(TEST_NAMESPACE, TEST_SERVICE_NAME, true))) + .thenReturn(datam); + + Instance updateMetadataInstance = new Instance(); + updateMetadataInstance.setIp(instance.getIp()); + updateMetadataInstance.setPort(instance.getPort()); + updateMetadataInstance.setClusterName(cluster.getName()); + updateMetadataInstance.setEphemeral(instance.isEphemeral()); + + Map updateMetadata = new HashMap<>(16); + updateMetadata.put("key1", "new-value1"); + updateMetadata.put("key2", "value2"); + updateMetadataInstance.setMetadata(updateMetadata); + + //all=false, update input instances + serviceManager + .updateMetadata(TEST_NAMESPACE, TEST_SERVICE_NAME, true, UPDATE_INSTANCE_METADATA_ACTION_UPDATE, false, + Lists.newArrayList(updateMetadataInstance), updateMetadata); + + assertEquals(instance.getMetadata().get("key1"), "new-value1"); + assertEquals(instance.getMetadata().get("key2"), "value2"); + + //all=true, update all instances + serviceManager + .updateMetadata(TEST_NAMESPACE, TEST_SERVICE_NAME, true, UPDATE_INSTANCE_METADATA_ACTION_UPDATE, true, + null, updateMetadata); + + assertEquals(instance2.getMetadata().get("key1"), "new-value1"); + assertEquals(instance2.getMetadata().get("key2"), "value2"); + + Instance deleteMetadataInstance = new Instance(); + deleteMetadataInstance.setIp(instance.getIp()); + deleteMetadataInstance.setPort(instance.getPort()); + deleteMetadataInstance.setClusterName(cluster.getName()); + deleteMetadataInstance.setEphemeral(instance.isEphemeral()); + Map deleteMetadata = new HashMap<>(16); + deleteMetadata.put("key2", null); + deleteMetadata.put("key3", null); + updateMetadataInstance.setMetadata(deleteMetadata); + + serviceManager + .updateMetadata(TEST_NAMESPACE, TEST_SERVICE_NAME, true, UPDATE_INSTANCE_METADATA_ACTION_REMOVE, false, + Lists.newArrayList(deleteMetadataInstance), deleteMetadata); + + assertEquals(instance.getMetadata().get("key1"), "new-value1"); + assertNull(instance.getMetadata().get("key2")); + assertNull(instance.getMetadata().get("key3")); + + serviceManager + .updateMetadata(TEST_NAMESPACE, TEST_SERVICE_NAME, true, UPDATE_INSTANCE_METADATA_ACTION_REMOVE, true, + null, deleteMetadata); + + assertEquals(instance2.getMetadata().get("key1"), "new-value1"); + assertNull(instance2.getMetadata().get("key2")); + assertNull(instance2.getMetadata().get("key3")); + } + @Test public void testRemoveInstance() throws NacosException { serviceManager.createEmptyService(TEST_NAMESPACE, TEST_SERVICE_NAME, true);