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);