diff --git a/api/src/main/java/com/alibaba/nacos/api/common/Constants.java b/api/src/main/java/com/alibaba/nacos/api/common/Constants.java index 8cd9f940c6..509972f07f 100644 --- a/api/src/main/java/com/alibaba/nacos/api/common/Constants.java +++ b/api/src/main/java/com/alibaba/nacos/api/common/Constants.java @@ -99,6 +99,10 @@ public class Constants { public static final Integer CLUSTER_GRPC_PORT_DEFAULT_OFFSET = 1001; + public static final String NAMESPACE_ID_SPLITTER = ">>"; + + public static final String DATA_ID_SPLITTER = "@@"; + /** * second. */ @@ -208,6 +212,8 @@ public class Constants { public static final String ALL_PATTERN = "*"; + public static final String FUZZY_LISTEN_PATTERN_WILDCARD = "*"; + public static final String COLON = ":"; public static final String LINE_BREAK = "\n"; @@ -231,6 +237,16 @@ public class Constants { public static final int DEFAULT_REDO_THREAD_COUNT = 1; + public static class ConfigChangeType { + + public static final String ADD_CONFIG = "ADD_CONFIG"; + + public static final String DELETE_CONFIG = "DELETE_CONFIG"; + + public static final String FINISH_LISTEN_INIT = "FINISH_LISTEN_INIT"; + + public static final String LISTEN_INIT = "LISTEN_INIT"; + } public static final String APP_CONN_LABELS_KEY = "nacos.app.conn.labels"; public static final String DOT = "."; diff --git a/api/src/main/java/com/alibaba/nacos/api/config/ConfigService.java b/api/src/main/java/com/alibaba/nacos/api/config/ConfigService.java index 3adab5aa4a..a1017860f6 100644 --- a/api/src/main/java/com/alibaba/nacos/api/config/ConfigService.java +++ b/api/src/main/java/com/alibaba/nacos/api/config/ConfigService.java @@ -17,9 +17,13 @@ package com.alibaba.nacos.api.config; import com.alibaba.nacos.api.config.filter.IConfigFilter; +import com.alibaba.nacos.api.config.listener.AbstractFuzzyListenListener; import com.alibaba.nacos.api.config.listener.Listener; import com.alibaba.nacos.api.exception.NacosException; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; + /** * Config Service Interface. * @@ -59,8 +63,8 @@ String getConfigAndSignListener(String dataId, String group, long timeoutMs, Lis /** * Add a listener to the configuration, after the server modified the configuration, the client will use the * incoming listener callback. Recommended asynchronous processing, the application can implement the getExecutor - * method in the ManagerListener, provide a thread pool of execution. If not provided, use the main thread callback, May - * block other configurations or be blocked by other configurations. + * method in the ManagerListener, provide a thread pool of execution. If not provided, use the main thread callback, + * May block other configurations or be blocked by other configurations. * * @param dataId dataId * @param group group @@ -69,6 +73,78 @@ String getConfigAndSignListener(String dataId, String group, long timeoutMs, Lis */ void addListener(String dataId, String group, Listener listener) throws NacosException; + /** + * Add a fuzzy listener to the configuration. After the server modifies the configuration matching the specified + * fixed group name, the client will utilize the incoming fuzzy listener callback. Fuzzy listeners allow for + * pattern-based subscription to configurations, where the fixed group name represents the group and dataId patterns + * specified for subscription. + * + * @param fixedGroupName The fixed group name representing the group and dataId patterns to subscribe to. + * @param listener The fuzzy listener to be added. + * @throws NacosException NacosException + */ + void addFuzzyListener(String fixedGroupName, AbstractFuzzyListenListener listener) throws NacosException; + + /** + * Add a fuzzy listener to the configuration. After the server modifies the configuration matching the specified + * dataId pattern and fixed group name, the client will utilize the incoming fuzzy listener callback. Fuzzy + * listeners allow for pattern-based subscription to configurations. + * + * @param dataIdPattern The pattern to match dataIds for subscription. + * @param fixedGroupName The fixed group name representing the group and dataId patterns to subscribe to. + * @param listener The fuzzy listener to be added. + * @throws NacosException NacosException + */ + void addFuzzyListener(String dataIdPattern, String fixedGroupName, AbstractFuzzyListenListener listener) + throws NacosException; + + /** + * Add a fuzzy listener to the configuration and retrieve all configs that match the specified fixed group name. + * Fuzzy listeners allow for pattern-based subscription to configs, where the fixed group name represents the group + * and dataId patterns specified for subscription. + * + * @param fixedGroupName The fixed group name representing the group and dataId patterns to subscribe to. + * @param listener The fuzzy listener to be added. + * @return CompletableFuture containing collection of configs that match the specified fixed group name. + * @throws NacosException NacosException + */ + CompletableFuture> addFuzzyListenerAndGetConfigs(String fixedGroupName, + AbstractFuzzyListenListener listener) throws NacosException; + + /** + * Add a fuzzy listener to the configuration and retrieve all configs that match the specified dataId pattern and + * fixed group name. Fuzzy listeners allow for pattern-based subscription to configs. + * + * @param dataIdPattern The pattern to match dataIds for subscription. + * @param fixedGroupName The fixed group name representing the group and dataId patterns to subscribe to. + * @param listener The fuzzy listener to be added. + * @return CompletableFuture containing collection of configs that match the specified dataId pattern and fixed + * group name. + * @throws NacosException NacosException + */ + CompletableFuture> addFuzzyListenerAndGetConfigs(String dataIdPattern, String fixedGroupName, + AbstractFuzzyListenListener listener) throws NacosException; + + /** + * Cancel fuzzy listen and remove the event listener for a specified fixed group name. + * + * @param fixedGroupName The fixed group name for fuzzy watch. + * @param listener The event listener to be removed. + * @throws NacosException If an error occurs during the cancellation process. + */ + void cancelFuzzyListen(String fixedGroupName, AbstractFuzzyListenListener listener) throws NacosException; + + /** + * Cancel fuzzy listen and remove the event listener for a specified service name pattern and fixed group name. + * + * @param dataIdPatter The pattern to match dataId for fuzzy watch. + * @param fixedGroupName The fixed group name for fuzzy watch. + * @param listener The event listener to be removed. + * @throws NacosException If an error occurs during the cancellation process. + */ + void cancelFuzzyListen(String dataIdPatter, String fixedGroupName, AbstractFuzzyListenListener listener) + throws NacosException; + /** * Publish config. * @@ -144,10 +220,10 @@ boolean publishConfigCas(String dataId, String group, String content, String cas * @return whether health */ String getServerStatus(); - + /** - * add config filter. - * It is recommended to use {@link com.alibaba.nacos.api.config.filter.AbstractConfigFilter} to expand the filter. + * add config filter. It is recommended to use {@link com.alibaba.nacos.api.config.filter.AbstractConfigFilter} to + * expand the filter. * * @param configFilter filter * @since 2.3.0 diff --git a/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractFuzzyListenListener.java b/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractFuzzyListenListener.java new file mode 100644 index 0000000000..bd317884a4 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractFuzzyListenListener.java @@ -0,0 +1,98 @@ +/* + * Copyright 1999-2023 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.api.config.listener; + +import java.util.Objects; + +/** + * AbstractFuzzyListenListener is an abstract class that provides basic functionality for listening to fuzzy + * configuration changes in Nacos. + * + * @author stone-98 + * @date 2024/3/4 + */ +public abstract class AbstractFuzzyListenListener extends AbstractListener { + + /** + * Unique identifier for the listener. + */ + private String uuid; + + /** + * Get the UUID (Unique Identifier) of the listener. + * + * @return The UUID of the listener + */ + public String getUuid() { + return uuid; + } + + /** + * Set the UUID (Unique Identifier) of the listener. + * + * @param uuid The UUID to be set + */ + public void setUuid(String uuid) { + this.uuid = uuid; + } + + /** + * Callback method invoked when a fuzzy configuration change event occurs. + * + * @param event The fuzzy configuration change event + */ + public abstract void onEvent(FuzzyListenConfigChangeEvent event); + + /** + * Receive the configuration information. This method is overridden but does nothing in this abstract class. + * + * @param configInfo The configuration information + */ + @Override + public void receiveConfigInfo(String configInfo) { + // Do nothing by default + } + + /** + * Compute the hash code for this listener based on its UUID. + * + * @return The hash code value for this listener + */ + @Override + public int hashCode() { + return Objects.hashCode(uuid); + } + + /** + * Compare this listener to the specified object for equality. Two listeners are considered equal if they have the + * same UUID. + * + * @param o The object to compare to + * @return true if the specified object is equal to this listener, false otherwise + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractFuzzyListenListener that = (AbstractFuzzyListenListener) o; + return Objects.equals(uuid, that.uuid); + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/listener/FuzzyListenConfigChangeEvent.java b/api/src/main/java/com/alibaba/nacos/api/config/listener/FuzzyListenConfigChangeEvent.java new file mode 100644 index 0000000000..6e2c3e319c --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/listener/FuzzyListenConfigChangeEvent.java @@ -0,0 +1,109 @@ +/* + * Copyright 1999-2023 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.api.config.listener; + +/** + * Represents a fuzzy listening configuration change event. + * + *

This event indicates that a change has occurred in a configuration that matches a fuzzy listening pattern. + * + * @author stone-98 + * @date 2024/3/12 + */ +public class FuzzyListenConfigChangeEvent { + + /** + * The group of the configuration that has changed. + */ + private String group; + + /** + * The data ID of the configuration that has changed. + */ + private String dataId; + + /** + * The type of change that has occurred (e.g., "ADD_CONFIG", "DELETE_CONFIG"). + */ + private String type; + + /** + * Constructs an empty FuzzyListenConfigChangeEvent. + */ + public FuzzyListenConfigChangeEvent() { + } + + /** + * Constructs a FuzzyListenConfigChangeEvent with the specified parameters. + * + * @param group The group of the configuration that has changed + * @param dataId The data ID of the configuration that has changed + * @param type The type of change that has occurred + */ + public FuzzyListenConfigChangeEvent(String group, String dataId, String type) { + this.group = group; + this.dataId = dataId; + this.type = type; + } + + /** + * Constructs and returns a new FuzzyListenConfigChangeEvent with the specified parameters. + * + * @param group The group of the configuration that has changed + * @param dataId The data ID of the configuration that has changed + * @param type The type of change that has occurred + * @return A new FuzzyListenConfigChangeEvent instance + */ + public static FuzzyListenConfigChangeEvent build(String group, String dataId, String type) { + return new FuzzyListenConfigChangeEvent(group, dataId, type); + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + /** + * Returns a string representation of the FuzzyListenConfigChangeEvent. + * + * @return A string representation of the event + */ + @Override + public String toString() { + return "FuzzyListenConfigChangeEvent{" + "group='" + group + '\'' + ", dataId='" + dataId + '\'' + ", type='" + + type + '\'' + '}'; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/request/AbstractFuzzyListenNotifyRequest.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/AbstractFuzzyListenNotifyRequest.java new file mode 100644 index 0000000000..1ba2b7bda1 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/AbstractFuzzyListenNotifyRequest.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2023 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.api.config.remote.request; + +import com.alibaba.nacos.api.remote.request.ServerRequest; + +import static com.alibaba.nacos.api.common.Constants.Config.CONFIG_MODULE; + +/** + * AbstractFuzzyListenNotifyRequest. + * + * @author stone-98 + * @date 2024/3/14 + */ +public abstract class AbstractFuzzyListenNotifyRequest extends ServerRequest { + + private String serviceChangedType; + + public AbstractFuzzyListenNotifyRequest() { + } + + public AbstractFuzzyListenNotifyRequest(String serviceChangedType) { + this.serviceChangedType = serviceChangedType; + } + + public String getServiceChangedType() { + return serviceChangedType; + } + + public void setServiceChangedType(String serviceChangedType) { + this.serviceChangedType = serviceChangedType; + } + + @Override + public String getModule() { + return CONFIG_MODULE; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigBatchFuzzyListenRequest.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigBatchFuzzyListenRequest.java new file mode 100644 index 0000000000..fd5ce81e1c --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigBatchFuzzyListenRequest.java @@ -0,0 +1,233 @@ +/* + * Copyright 1999-2023 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.api.config.remote.request; + +import com.alibaba.nacos.api.common.Constants; +import com.alibaba.nacos.api.remote.request.Request; +import com.alibaba.nacos.api.utils.StringUtils; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents a request for batch fuzzy listening configurations. + * + *

This request is used to request batch fuzzy listening configurations from the server. It contains a set of + * contexts, each representing a fuzzy listening context. + * + * @author stone-98 + * @date 2024/3/4 + */ +public class ConfigBatchFuzzyListenRequest extends Request { + + /** + * Set of fuzzy listening contexts. + */ + private Set contexts = new HashSet<>(); + + /** + * Constructs an empty ConfigBatchFuzzyListenRequest. + */ + public ConfigBatchFuzzyListenRequest() { + } + + /** + * Adds a new context to the request. + * + * @param tenant The namespace or tenant associated with the configurations + * @param group The group associated with the configurations + * @param dataIdPattern The pattern for matching data IDs + * @param dataIds Set of data IDs + * @param listen Flag indicating whether to listen for changes + * @param isInitializing Flag indicating whether the client is initializing + */ + public void addContext(String tenant, String group, String dataIdPattern, Set dataIds, boolean listen, + boolean isInitializing) { + contexts.add( + new Context(StringUtils.isEmpty(tenant) ? Constants.DEFAULT_NAMESPACE_ID : tenant, group, dataIdPattern, + dataIds, listen, isInitializing)); + } + + /** + * Get the set of fuzzy listening contexts. + * + * @return The set of contexts + */ + public Set getContexts() { + return contexts; + } + + /** + * Set the set of fuzzy listening contexts. + * + * @param contexts The set of contexts to be set + */ + public void setContexts(Set contexts) { + this.contexts = contexts; + } + + /** + * Get the module name for this request. + * + * @return The module name + */ + @Override + public String getModule() { + return Constants.Config.CONFIG_MODULE; + } + + /** + * Represents a fuzzy listening context. + */ + public static class Context { + + /** + * The namespace or tenant associated with the configurations. + */ + private String tenant; + + /** + * The group associated with the configurations. + */ + private String group; + + /** + * The pattern for matching data IDs. + */ + private String dataIdPattern; + + /** + * Set of data IDs. + */ + private Set dataIds; + + /** + * Flag indicating whether to listen for changes. + */ + private boolean listen; + + /** + * Flag indicating whether the client is initializing. + */ + private boolean isInitializing; + + /** + * Constructs an empty Context. + */ + public Context() { + } + + /** + * Constructs a Context with the specified parameters. + * + * @param tenant The namespace or tenant associated with the configurations + * @param group The group associated with the configurations + * @param dataIdPattern The pattern for matching data IDs + * @param dataIds Set of data IDs + * @param listen Flag indicating whether to listen for changes + * @param isInitializing Flag indicating whether the client is initializing + */ + public Context(String tenant, String group, String dataIdPattern, Set dataIds, boolean listen, + boolean isInitializing) { + this.tenant = tenant; + this.group = group; + this.dataIdPattern = dataIdPattern; + this.dataIds = dataIds; + this.listen = listen; + this.isInitializing = isInitializing; + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getDataIdPattern() { + return dataIdPattern; + } + + public void setDataIdPattern(String dataIdPattern) { + this.dataIdPattern = dataIdPattern; + } + + public Set getDataIds() { + return dataIds; + } + + public void setDataIds(Set dataIds) { + this.dataIds = dataIds; + } + + public boolean isListen() { + return listen; + } + + public void setListen(boolean listen) { + this.listen = listen; + } + + public boolean isInitializing() { + return isInitializing; + } + + public void setInitializing(boolean initializing) { + isInitializing = initializing; + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param o The reference object with which to compare + * @return True if this object is the same as the obj argument, false otherwise + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Context that = (Context) o; + return Objects.equals(tenant, that.tenant) && Objects.equals(group, that.group) && Objects.equals( + dataIdPattern, that.dataIdPattern) && Objects.equals(dataIds, that.dataIds) && Objects.equals( + listen, that.listen) && Objects.equals(isInitializing, that.isInitializing); + } + + /** + * Returns a hash code value for the object. + * + * @return A hash code value for this object + */ + @Override + public int hashCode() { + return Objects.hash(tenant, group, dataIdPattern, dataIds, listen, isInitializing); + } + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/request/FuzzyListenNotifyChangeRequest.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/FuzzyListenNotifyChangeRequest.java new file mode 100644 index 0000000000..b149d70689 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/FuzzyListenNotifyChangeRequest.java @@ -0,0 +1,113 @@ +/* + * Copyright 1999-2023 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.api.config.remote.request; + +/** + * Represents a request to notify changes in a fuzzy listening configuration. + * + *

This request is used to notify clients about changes in configurations that match fuzzy listening patterns. + * + * @author stone-98 + * @date 2024/3/13 + */ +public class FuzzyListenNotifyChangeRequest extends AbstractFuzzyListenNotifyRequest { + + /** + * The tenant of the configuration that has changed. + */ + private String tenant; + + /** + * The group of the configuration that has changed. + */ + private String group; + + /** + * The data ID of the configuration that has changed. + */ + private String dataId; + + /** + * Indicates whether the configuration exists or not. + */ + private boolean isExist; + + /** + * Constructs an empty FuzzyListenNotifyChangeRequest. + */ + public FuzzyListenNotifyChangeRequest() { + } + + /** + * Constructs a FuzzyListenNotifyChangeRequest with the specified parameters. + * + * @param tenant The tenant of the configuration that has changed + * @param group The group of the configuration that has changed + * @param dataId The data ID of the configuration that has changed + * @param isExist Indicates whether the configuration exists or not + */ + public FuzzyListenNotifyChangeRequest(String tenant, String group, String dataId, boolean isExist) { + this.tenant = tenant; + this.group = group; + this.dataId = dataId; + this.isExist = isExist; + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } + + public boolean isExist() { + return isExist; + } + + public void setExist(boolean exist) { + isExist = exist; + } + + /** + * Returns a string representation of the FuzzyListenNotifyChangeRequest. + * + * @return A string representation of the request + */ + @Override + public String toString() { + return "FuzzyListenNotifyChangeRequest{" + "tenant='" + tenant + '\'' + ", group='" + group + '\'' + + ", dataId='" + dataId + '\'' + ", isExist=" + isExist + '}'; + } + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/request/FuzzyListenNotifyDiffRequest.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/FuzzyListenNotifyDiffRequest.java new file mode 100644 index 0000000000..0c925a3dad --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/FuzzyListenNotifyDiffRequest.java @@ -0,0 +1,191 @@ +/* + * Copyright 1999-2023 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.api.config.remote.request; + +import com.alibaba.nacos.api.common.Constants; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents a request to notify the difference in configurations for fuzzy listening. + * + *

This request is used to notify clients about the difference in configurations that match fuzzy listening + * patterns. + * + * @author stone-98 + * @date 2024/3/6 + */ +public class FuzzyListenNotifyDiffRequest extends AbstractFuzzyListenNotifyRequest { + + /** + * The pattern used to match group keys for the configurations. + */ + private String groupKeyPattern; + + /** + * The set of contexts containing information about the configurations. + */ + private Set contexts; + + /** + * Constructs an empty FuzzyListenNotifyDiffRequest. + */ + public FuzzyListenNotifyDiffRequest() { + } + + /** + * Constructs a FuzzyListenNotifyDiffRequest with the specified parameters. + * + * @param serviceChangedType The type of service change + * @param groupKeyPattern The pattern used to match group keys for the configurations + * @param contexts The set of contexts containing information about the configurations + */ + public FuzzyListenNotifyDiffRequest(String serviceChangedType, String groupKeyPattern, Set contexts) { + super(serviceChangedType); + this.groupKeyPattern = groupKeyPattern; + this.contexts = contexts; + } + + /** + * Builds an initial FuzzyListenNotifyDiffRequest with the specified set of contexts and group key pattern. + * + * @param contexts The set of contexts containing information about the configurations + * @param groupKeyPattern The pattern used to match group keys for the configurations + * @return An initial FuzzyListenNotifyDiffRequest + */ + public static FuzzyListenNotifyDiffRequest buildInitRequest(Set contexts, String groupKeyPattern) { + return new FuzzyListenNotifyDiffRequest(Constants.ConfigChangeType.LISTEN_INIT, groupKeyPattern, contexts); + } + + /** + * Builds a final FuzzyListenNotifyDiffRequest with the specified group key pattern. + * + * @param groupKeyPattern The pattern used to match group keys for the configurations + * @return A final FuzzyListenNotifyDiffRequest + */ + public static FuzzyListenNotifyDiffRequest buildInitFinishRequest(String groupKeyPattern) { + return new FuzzyListenNotifyDiffRequest(Constants.ConfigChangeType.FINISH_LISTEN_INIT, groupKeyPattern, + new HashSet<>()); + } + + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + public void setGroupKeyPattern(String groupKeyPattern) { + this.groupKeyPattern = groupKeyPattern; + } + + public Set getContexts() { + return contexts; + } + + public void setContexts(Set contexts) { + this.contexts = contexts; + } + + /** + * Represents context information about a configuration. + */ + public static class Context { + + private String tenant; + + private String group; + + private String dataId; + + private String type; + + /** + * Constructs an empty Context object. + */ + public Context() { + } + + /** + * Builds a new context object with the provided parameters. + * + * @param tenant The tenant associated with the configuration. + * @param group The group associated with the configuration. + * @param dataId The data ID of the configuration. + * @param type The type of the configuration change event. + * @return A new context object initialized with the provided parameters. + */ + public static Context build(String tenant, String group, String dataId, String type) { + Context context = new Context(); + context.setTenant(tenant); + context.setGroup(group); + context.setDataId(dataId); + context.setType(type); + return context; + } + + @Override + public int hashCode() { + return Objects.hash(tenant, group, dataId, tenant); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Context that = (Context) o; + return Objects.equals(tenant, that.tenant) && Objects.equals(group, that.group) && Objects.equals(dataId, + that.dataId) && Objects.equals(type, that.type); + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigBatchFuzzyListenResponse.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigBatchFuzzyListenResponse.java new file mode 100644 index 0000000000..f1eca4df7c --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigBatchFuzzyListenResponse.java @@ -0,0 +1,29 @@ +/* + * Copyright 1999-2023 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.api.config.remote.response; + +import com.alibaba.nacos.api.remote.response.Response; + +/** + * ConfigBatchFuzzyListenResponse. + * + * @author stone-98 + * @date 2024/3/4 + */ +public class ConfigBatchFuzzyListenResponse extends Response { + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/response/FuzzyListenNotifyChangeResponse.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/FuzzyListenNotifyChangeResponse.java new file mode 100644 index 0000000000..1e58ec3f7d --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/FuzzyListenNotifyChangeResponse.java @@ -0,0 +1,29 @@ +/* + * Copyright 1999-2023 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.api.config.remote.response; + +import com.alibaba.nacos.api.remote.response.Response; + +/** + * FuzzyListenNotifyChangeResponse. + * + * @author stone-98 + * @date 2024/3/18 + */ +public class FuzzyListenNotifyChangeResponse extends Response { + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/response/FuzzyListenNotifyDiffResponse.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/FuzzyListenNotifyDiffResponse.java new file mode 100644 index 0000000000..672d8dfc80 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/FuzzyListenNotifyDiffResponse.java @@ -0,0 +1,29 @@ +/* + * Copyright 1999-2023 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.api.config.remote.response; + +import com.alibaba.nacos.api.remote.response.Response; + +/** + * FuzzyListenNotifyDiffResponse. + * + * @author stone-98 + * @date 2024/3/18 + */ +public class FuzzyListenNotifyDiffResponse extends Response { + +} diff --git a/api/src/main/resources/META-INF/services/com.alibaba.nacos.api.remote.Payload b/api/src/main/resources/META-INF/services/com.alibaba.nacos.api.remote.Payload index cbd1e87502..eba07c73d2 100644 --- a/api/src/main/resources/META-INF/services/com.alibaba.nacos.api.remote.Payload +++ b/api/src/main/resources/META-INF/services/com.alibaba.nacos.api.remote.Payload @@ -57,4 +57,10 @@ com.alibaba.nacos.api.naming.remote.response.InstanceResponse com.alibaba.nacos.api.naming.remote.response.NotifySubscriberResponse com.alibaba.nacos.api.naming.remote.response.QueryServiceResponse com.alibaba.nacos.api.naming.remote.response.ServiceListResponse -com.alibaba.nacos.api.naming.remote.response.SubscribeServiceResponse \ No newline at end of file +com.alibaba.nacos.api.naming.remote.response.SubscribeServiceResponse +com.alibaba.nacos.api.config.remote.request.ConfigBatchFuzzyListenRequest +com.alibaba.nacos.api.config.remote.response.ConfigBatchFuzzyListenResponse +com.alibaba.nacos.api.config.remote.request.FuzzyListenNotifyChangeRequest +com.alibaba.nacos.api.config.remote.response.FuzzyListenNotifyChangeResponse +com.alibaba.nacos.api.config.remote.request.FuzzyListenNotifyDiffRequest +com.alibaba.nacos.api.config.remote.response.FuzzyListenNotifyDiffResponse diff --git a/client/src/main/java/com/alibaba/nacos/client/config/NacosConfigService.java b/client/src/main/java/com/alibaba/nacos/client/config/NacosConfigService.java index ba17c03bb1..7e921e45ed 100644 --- a/client/src/main/java/com/alibaba/nacos/client/config/NacosConfigService.java +++ b/client/src/main/java/com/alibaba/nacos/client/config/NacosConfigService.java @@ -21,6 +21,7 @@ import com.alibaba.nacos.api.config.ConfigService; import com.alibaba.nacos.api.config.ConfigType; import com.alibaba.nacos.api.config.filter.IConfigFilter; +import com.alibaba.nacos.api.config.listener.AbstractFuzzyListenListener; import com.alibaba.nacos.api.config.listener.Listener; import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.client.config.filter.impl.ConfigFilterChainManager; @@ -28,6 +29,7 @@ import com.alibaba.nacos.client.config.filter.impl.ConfigResponse; import com.alibaba.nacos.client.config.http.ServerHttpAgent; import com.alibaba.nacos.client.config.impl.ClientWorker; +import com.alibaba.nacos.client.config.impl.FuzzyListenContext; import com.alibaba.nacos.client.config.impl.ConfigServerListManager; import com.alibaba.nacos.client.config.impl.LocalConfigInfoProcessor; import com.alibaba.nacos.client.config.impl.LocalEncryptedDataKeyProcessor; @@ -40,8 +42,13 @@ import com.alibaba.nacos.common.utils.StringUtils; import org.slf4j.Logger; +import java.util.Collection; import java.util.Collections; import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static com.alibaba.nacos.api.common.Constants.FUZZY_LISTEN_PATTERN_WILDCARD; /** * Config Impl. @@ -125,6 +132,87 @@ public void addListener(String dataId, String group, Listener listener) throws N worker.addTenantListeners(dataId, group, Collections.singletonList(listener)); } + @Override + public void addFuzzyListener(String fixedGroupName, AbstractFuzzyListenListener listener) throws NacosException { + doFuzzyListen(FUZZY_LISTEN_PATTERN_WILDCARD, fixedGroupName, listener); + } + + @Override + public void addFuzzyListener(String dataIdPattern, String fixedGroupName, AbstractFuzzyListenListener listener) + throws NacosException { + // only support prefix match right now + if (!dataIdPattern.endsWith(FUZZY_LISTEN_PATTERN_WILDCARD)) { + if (dataIdPattern.startsWith(FUZZY_LISTEN_PATTERN_WILDCARD)) { + throw new UnsupportedOperationException("Suffix matching for dataId is not supported yet." + + " It will be supported in future updates if needed."); + } else { + throw new UnsupportedOperationException( + "Illegal dataId pattern, please read the documentation and pass a valid pattern."); + } + } + doFuzzyListen(dataIdPattern, fixedGroupName, listener); + } + + @Override + public CompletableFuture> addFuzzyListenerAndGetConfigs(String fixedGroupName, + AbstractFuzzyListenListener listener) throws NacosException { + return doAddFuzzyListenerAndGetConfigs(FUZZY_LISTEN_PATTERN_WILDCARD, fixedGroupName, listener); + } + + @Override + public CompletableFuture> addFuzzyListenerAndGetConfigs(String dataIdPattern, + String fixedGroupName, AbstractFuzzyListenListener listener) throws NacosException { + return doAddFuzzyListenerAndGetConfigs(dataIdPattern, fixedGroupName, listener); + } + + private CompletableFuture> doAddFuzzyListenerAndGetConfigs(String dataIdPattern, + String fixedGroupName, AbstractFuzzyListenListener listener) throws NacosException { + CompletableFuture> future = new CompletableFuture<>(); + if (listener == null) { + future.completeExceptionally(new IllegalArgumentException("Listener cannot be null")); + return future; + } + addFuzzyListener(dataIdPattern, fixedGroupName, listener); + FuzzyListenContext context = worker.getFuzzyListenContext(dataIdPattern, fixedGroupName); + if (context == null) { + future.complete(Collections.emptyList()); + return future; + } + return context.waitForInitializationComplete(future); + } + + private void doFuzzyListen(String dataIdPattern, String fixedGroupName, AbstractFuzzyListenListener listener) + throws NacosException { + if (listener == null) { + return; + } + listener.setUuid(UUID.randomUUID().toString()); + if (!worker.containsPatternMatchCache(dataIdPattern, fixedGroupName)) { + worker.addTenantFuzzyListenListens(dataIdPattern, fixedGroupName, Collections.singletonList(listener)); + } else { + worker.duplicateFuzzyListenInit(dataIdPattern, fixedGroupName, listener); + } + } + + @Override + public void cancelFuzzyListen(String fixedGroupName, AbstractFuzzyListenListener listener) throws NacosException { + cancelFuzzyListen(FUZZY_LISTEN_PATTERN_WILDCARD, fixedGroupName, listener); + } + + @Override + public void cancelFuzzyListen(String dataIdPattern, String fixedGroupName, AbstractFuzzyListenListener listener) + throws NacosException { + doCancelFuzzyListen(dataIdPattern, fixedGroupName, listener); + } + + private void doCancelFuzzyListen(String dataIdPattern, String groupNamePattern, + AbstractFuzzyListenListener listener) throws NacosException { + if (null == listener) { + return; + } + worker.removeFuzzyListenListener(dataIdPattern, groupNamePattern, listener); + } + @Override public boolean publishConfig(String dataId, String group, String content) throws NacosException { return publishConfig(dataId, group, content, ConfigType.getDefaultType().getType()); @@ -176,8 +264,8 @@ private String getConfigInner(String tenant, String dataId, String group, long t LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}", worker.getAgentName(), dataId, group, tenant); cr.setContent(content); - String encryptedDataKey = LocalEncryptedDataKeyProcessor - .getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant); + String encryptedDataKey = LocalEncryptedDataKeyProcessor.getEncryptDataKeyFailover(agent.getName(), dataId, + group, tenant); cr.setEncryptedDataKey(encryptedDataKey); configFilterChainManager.doFilter(null, cr); content = cr.getContent(); @@ -199,15 +287,15 @@ private String getConfigInner(String tenant, String dataId, String group, long t LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}", worker.getAgentName(), dataId, group, tenant, ioe.toString()); } - + content = LocalConfigInfoProcessor.getSnapshot(worker.getAgentName(), dataId, group, tenant); if (content != null) { LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}", worker.getAgentName(), dataId, group, tenant); } cr.setContent(content); - String encryptedDataKey = LocalEncryptedDataKeyProcessor - .getEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant); + String encryptedDataKey = LocalEncryptedDataKeyProcessor.getEncryptDataKeySnapshot(agent.getName(), dataId, + group, tenant); cr.setEncryptedDataKey(encryptedDataKey); configFilterChainManager.doFilter(null, cr); content = cr.getContent(); @@ -239,8 +327,8 @@ private boolean publishConfigInner(String tenant, String dataId, String group, S content = cr.getContent(); String encryptedDataKey = cr.getEncryptedDataKey(); - return worker - .publishConfig(dataId, group, tenant, appName, tag, betaIps, content, encryptedDataKey, casMd5, type); + return worker.publishConfig(dataId, group, tenant, appName, tag, betaIps, content, encryptedDataKey, casMd5, + type); } @Override @@ -251,12 +339,12 @@ public String getServerStatus() { return DOWN; } } - + @Override public void addConfigFilter(IConfigFilter configFilter) { configFilterChainManager.addFilter(configFilter); } - + @Override public void shutDown() throws NacosException { worker.shutdown(); diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientWorker.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientWorker.java index a8629ecb0d..b2ab88f777 100644 --- a/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientWorker.java +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientWorker.java @@ -19,19 +19,26 @@ import com.alibaba.nacos.api.PropertyKeyConst; import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.api.config.ConfigType; +import com.alibaba.nacos.api.config.listener.AbstractFuzzyListenListener; import com.alibaba.nacos.api.config.listener.Listener; import com.alibaba.nacos.api.config.remote.request.ClientConfigMetricRequest; +import com.alibaba.nacos.api.config.remote.request.ConfigBatchFuzzyListenRequest; import com.alibaba.nacos.api.config.remote.request.ConfigBatchListenRequest; import com.alibaba.nacos.api.config.remote.request.ConfigChangeNotifyRequest; import com.alibaba.nacos.api.config.remote.request.ConfigPublishRequest; import com.alibaba.nacos.api.config.remote.request.ConfigQueryRequest; import com.alibaba.nacos.api.config.remote.request.ConfigRemoveRequest; +import com.alibaba.nacos.api.config.remote.request.FuzzyListenNotifyChangeRequest; +import com.alibaba.nacos.api.config.remote.request.FuzzyListenNotifyDiffRequest; import com.alibaba.nacos.api.config.remote.response.ClientConfigMetricResponse; +import com.alibaba.nacos.api.config.remote.response.ConfigBatchFuzzyListenResponse; import com.alibaba.nacos.api.config.remote.response.ConfigChangeBatchListenResponse; import com.alibaba.nacos.api.config.remote.response.ConfigChangeNotifyResponse; import com.alibaba.nacos.api.config.remote.response.ConfigPublishResponse; import com.alibaba.nacos.api.config.remote.response.ConfigQueryResponse; import com.alibaba.nacos.api.config.remote.response.ConfigRemoveResponse; +import com.alibaba.nacos.api.config.remote.response.FuzzyListenNotifyChangeResponse; +import com.alibaba.nacos.api.config.remote.response.FuzzyListenNotifyDiffResponse; import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.remote.RemoteConstants; import com.alibaba.nacos.api.remote.request.Request; @@ -65,6 +72,7 @@ import com.alibaba.nacos.common.remote.client.ServerListFactory; import com.alibaba.nacos.common.utils.ConnLabelsUtils; import com.alibaba.nacos.common.utils.ConvertUtils; +import com.alibaba.nacos.common.utils.GroupKeyPattern; import com.alibaba.nacos.common.utils.JacksonUtils; import com.alibaba.nacos.common.utils.MD5Utils; import com.alibaba.nacos.common.utils.StringUtils; @@ -84,6 +92,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.UUID; @@ -129,6 +138,11 @@ public class ClientWorker implements Closeable { */ private final AtomicReference> cacheMap = new AtomicReference<>(new HashMap<>()); + /** + * fuzzyListenGroupKey -> fuzzyListenContext. + */ + private final AtomicReference> fuzzyListenContextMap = new AtomicReference<>( + new HashMap<>()); private final DefaultLabelsCollectorManager defaultLabelsCollectorManager = new DefaultLabelsCollectorManager(); private Map appLables = new HashMap<>(); @@ -156,6 +170,49 @@ public class ClientWorker implements Closeable { */ private final List taskIdCacheCountList = new ArrayList<>(); + /** + * index(taskId)-> total context count for this taskId. + */ + private final List taskIdContextCountList = new ArrayList<>(); + + @SuppressWarnings("PMD.ThreadPoolCreationRule") + public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager, + final NacosClientProperties properties) throws NacosException { + this.configFilterChainManager = configFilterChainManager; + + init(properties); + + agent = new ConfigRpcTransportClient(properties, serverListManager); + ScheduledExecutorService executorService = Executors.newScheduledThreadPool(initWorkerThreadCount(properties), + new NameThreadFactory("com.alibaba.nacos.client.Worker")); + agent.setExecutor(executorService); + agent.start(); + + } + + /** + * Adds a list of fuzzy listen listeners for the specified data ID pattern and group. + * + * @param dataIdPattern The pattern of the data ID to listen for. + * @param group The group of the configuration. + * @param listeners The list of listeners to add. + * @throws NacosException If an error occurs while adding the listeners. + */ + public void addTenantFuzzyListenListens(String dataIdPattern, String group, + List listeners) throws NacosException { + group = blank2defaultGroup(group); + FuzzyListenContext context = addFuzzyListenContextIfAbsent(dataIdPattern, group); + synchronized (context) { + for (AbstractFuzzyListenListener listener : listeners) { + context.addListener(listener); + } + context.setInitializing(true); + context.setDiscard(false); + context.getIsConsistentWithServer().set(false); + agent.notifyFuzzyListenConfig(); + } + } + /** * Add listeners for data. * @@ -286,19 +343,72 @@ public void removeTenantListener(String dataId, String group, Listener listener) } } - void removeCache(String dataId, String group, String tenant) { - String groupKey = GroupKey.getKeyTenant(dataId, group, tenant); - synchronized (cacheMap) { - Map copy = new HashMap<>(cacheMap.get()); - CacheData remove = copy.remove(groupKey); - if (remove != null) { - decreaseTaskIdCount(remove.getTaskId()); + /** + * Initializes a duplicate fuzzy listen for the specified data ID pattern, group, and listener. + * + * @param dataIdPattern The pattern of the data ID to listen for. + * @param group The group of the configuration. + * @param listener The listener to add. + */ + public void duplicateFuzzyListenInit(String dataIdPattern, String group, AbstractFuzzyListenListener listener) { + String groupKeyPattern = GroupKeyPattern.generateFuzzyListenGroupKeyPattern(dataIdPattern, group); + Map contextMap = fuzzyListenContextMap.get(); + FuzzyListenContext context = contextMap.get(groupKeyPattern); + if (Objects.isNull(context)) { + return; + } + synchronized (context) { + context.addListener(listener); + + for (String dataId : context.getDataIds()) { + NotifyCenter.publishEvent(FuzzyListenNotifyEvent.buildNotifyPatternSpecificListenerEvent(group, dataId, + Constants.ConfigChangeType.ADD_CONFIG, groupKeyPattern, listener.getUuid())); + } + } + } + + /** + * Removes a fuzzy listen listener for the specified data ID pattern, group, and listener. + * + * @param dataIdPattern The pattern of the data ID. + * @param group The group of the configuration. + * @param listener The listener to remove. + * @throws NacosException If an error occurs while removing the listener. + */ + public void removeFuzzyListenListener(String dataIdPattern, String group, AbstractFuzzyListenListener listener) + throws NacosException { + group = blank2defaultGroup(group); + FuzzyListenContext fuzzyListenContext = getFuzzyListenContext(dataIdPattern, group); + if (fuzzyListenContext != null) { + synchronized (fuzzyListenContext) { + fuzzyListenContext.removeListener(listener); + if (fuzzyListenContext.getListeners().isEmpty()) { + fuzzyListenContext.setDiscard(true); + fuzzyListenContext.getIsConsistentWithServer().set(false); + agent.removeFuzzyListenContext(dataIdPattern, group); + } } - cacheMap.set(copy); } - LOGGER.info("[{}] [unsubscribe] {}", agent.getName(), groupKey); - - MetricsMonitor.getListenConfigCountMonitor().set(cacheMap.get().size()); + } + + /** + * Removes the fuzzy listen context for the specified data ID pattern and group. + * + * @param dataIdPattern The pattern of the data ID. + * @param group The group of the configuration. + */ + public void removeFuzzyListenContext(String dataIdPattern, String group) { + String groupKeyPattern = GroupKeyPattern.generateFuzzyListenGroupKeyPattern(dataIdPattern, group); + synchronized (fuzzyListenContextMap) { + Map copy = new HashMap<>(fuzzyListenContextMap.get()); + FuzzyListenContext removedContext = copy.remove(groupKeyPattern); + if (removedContext != null) { + decreaseContextTaskIdCount(removedContext.getTaskId()); + } + fuzzyListenContextMap.set(copy); + } + LOGGER.info("[{}] [fuzzy-listen-unsubscribe] {}", agent.getName(), groupKeyPattern); + // TODO: Record metric for fuzzy listen unsubscribe. } /** @@ -425,6 +535,28 @@ public CacheData addCacheDataIfAbsent(String dataId, String group, String tenant return cache; } + /** + * Removes the cache entry associated with the given data ID, group, and tenant. + * + * @param dataId The data ID. + * @param group The group name. + * @param tenant The tenant. + */ + public void removeCache(String dataId, String group, String tenant) { + String groupKey = GroupKey.getKeyTenant(dataId, group, tenant); + synchronized (cacheMap) { + Map copy = new HashMap<>(cacheMap.get()); + CacheData remove = copy.remove(groupKey); + if (remove != null) { + decreaseTaskIdCount(remove.getTaskId()); + } + cacheMap.set(copy); + } + LOGGER.info("[{}] [unsubscribe] {}", agent.getName(), groupKey); + + MetricsMonitor.getListenConfigCountMonitor().set(cacheMap.get().size()); + } + /** * Put cache. * @@ -439,23 +571,114 @@ private void putCache(String key, CacheData cache) { } } + /** + * Adds a fuzzy listen context if it doesn't already exist for the specified data ID pattern and group. If the + * context already exists, returns the existing context. + * + * @param dataIdPattern The pattern of the data ID. + * @param group The group of the configuration. + * @return The fuzzy listen context for the specified data ID pattern and group. + */ + public FuzzyListenContext addFuzzyListenContextIfAbsent(String dataIdPattern, String group) { + FuzzyListenContext context = getFuzzyListenContext(dataIdPattern, group); + if (context != null) { + return context; + } + synchronized (fuzzyListenContextMap) { + FuzzyListenContext contextFromMap = getFuzzyListenContext(dataIdPattern, group); + if (contextFromMap != null) { + context = contextFromMap; + context.getIsConsistentWithServer().set(false); + } else { + context = new FuzzyListenContext(agent.getName(), dataIdPattern, group); + int taskId = calculateContextTaskId(); + increaseContextTaskIdCount(taskId); + context.setTaskId(taskId); + } + } + + Map copy = new HashMap<>(fuzzyListenContextMap.get()); + String groupKeyPattern = GroupKeyPattern.generateFuzzyListenGroupKeyPattern(dataIdPattern, group); + copy.put(groupKeyPattern, context); + fuzzyListenContextMap.set(copy); + + // TODO: Record metrics + + return context; + } + + /** + * Increases the count for the specified task ID in the given count list. + * + * @param taskId The ID of the task for which the count needs to be increased. + */ private void increaseTaskIdCount(int taskId) { - taskIdCacheCountList.get(taskId).incrementAndGet(); + increaseCount(taskId, taskIdCacheCountList); } + /** + * Decreases the count for the specified task ID in the given count list. + * + * @param taskId The ID of the task for which the count needs to be decreased. + */ private void decreaseTaskIdCount(int taskId) { - taskIdCacheCountList.get(taskId).decrementAndGet(); + decreaseCount(taskId, taskIdCacheCountList); } + /** + * Increases the context task ID count in the corresponding list. + * + * @param taskId The ID of the context task for which the count needs to be increased. + */ + private void increaseContextTaskIdCount(int taskId) { + increaseCount(taskId, taskIdContextCountList); + } + + /** + * Decreases the context task ID count in the corresponding list. + * + * @param taskId The ID of the context task for which the count needs to be decreased. + */ + private void decreaseContextTaskIdCount(int taskId) { + decreaseCount(taskId, taskIdContextCountList); + } + + /** + * Calculates the task ID based on the configuration size. + * + * @return The calculated task ID. + */ private int calculateTaskId() { - int perTaskSize = (int) ParamUtil.getPerTaskConfigSize(); - for (int index = 0; index < taskIdCacheCountList.size(); index++) { - if (taskIdCacheCountList.get(index).get() < perTaskSize) { - return index; - } - } - taskIdCacheCountList.add(new AtomicInteger(0)); - return taskIdCacheCountList.size() - 1; + return calculateId(taskIdCacheCountList, (long) ParamUtil.getPerTaskConfigSize()); + } + + /** + * Calculates the context task ID based on the configuration size. + * + * @return The calculated context task ID. + */ + private int calculateContextTaskId() { + return calculateId(taskIdContextCountList, (long) ParamUtil.getPerTaskContextSize()); + } + + /** + * Increases the count for the specified task ID in the given count list. + * + * @param taskId The ID of the task for which the count needs to be increased. + * @param countList The list containing the counts for different task IDs. + */ + private void increaseCount(int taskId, List countList) { + countList.get(taskId).incrementAndGet(); + } + + /** + * Decreases the count for the specified task ID in the given count list. + * + * @param taskId The ID of the task for which the count needs to be decreased. + * @param countList The list containing the counts for different task IDs. + */ + private void decreaseCount(int taskId, List countList) { + countList.get(taskId).decrementAndGet(); } public CacheData getCache(String dataId, String group) { @@ -469,6 +692,35 @@ public CacheData getCache(String dataId, String group, String tenant) { return cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant)); } + /** + * Calculates the task ID based on the provided count list and per-task size. + * + * @param countList The list containing the counts for different task IDs. + * @param perTaskSize The size of each task. + * @return The calculated task ID. + */ + private int calculateId(List countList, long perTaskSize) { + for (int index = 0; index < countList.size(); index++) { + if (countList.get(index).get() < perTaskSize) { + return index; + } + } + countList.add(new AtomicInteger(0)); + return countList.size() - 1; + } + + /** + * Retrieves the FuzzyListenContext for the given data ID pattern and group. + * + * @param dataIdPattern The data ID pattern. + * @param group The group name. + * @return The corresponding FuzzyListenContext, or null if not found. + */ + public FuzzyListenContext getFuzzyListenContext(String dataIdPattern, String group) { + return fuzzyListenContextMap.get() + .get(GroupKeyPattern.generateFuzzyListenGroupKeyPattern(dataIdPattern, group)); + } + public ConfigResponse getServerConfig(String dataId, String group, String tenant, long readTimeout, boolean notify) throws NacosException { if (StringUtils.isBlank(group)) { @@ -481,6 +733,19 @@ private String blank2defaultGroup(String group) { return StringUtils.isBlank(group) ? Constants.DEFAULT_GROUP : group.trim(); } + /** + * Checks if the pattern match cache contains an entry for the specified data ID pattern and group. + * + * @param dataIdPattern The data ID pattern. + * @param group The group name. + * @return True if the cache contains an entry, false otherwise. + */ + public boolean containsPatternMatchCache(String dataIdPattern, String group) { + Map contextMap = fuzzyListenContextMap.get(); + String groupKeyPattern = GroupKeyPattern.generateFuzzyListenGroupKeyPattern(dataIdPattern, group); + return contextMap.containsKey(groupKeyPattern); + } + @SuppressWarnings("PMD.ThreadPoolCreationRule") public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ConfigServerListManager serverListManager, final NacosClientProperties properties) throws NacosException { @@ -587,14 +852,28 @@ public boolean isHealthServer() { public class ConfigRpcTransportClient extends ConfigTransportClient { - Map multiTaskExecutor = new HashMap<>(); + /** + * 5 minutes to check all fuzzy listen context. + */ + private static final long FUZZY_LISTEN_ALL_SYNC_INTERNAL = 5 * 60 * 1000L; + + private final String configListenerTaskPrefix = "nacos.client.config.listener.task"; + + private final String fuzzyListenerTaskPrefix = "nacos.client.config.fuzzyListener.task"; private final BlockingQueue listenExecutebell = new ArrayBlockingQueue<>(1); + private final Map multiTaskExecutor = new HashMap<>(); + private final Object bellItem = new Object(); private long lastAllSyncTime = System.currentTimeMillis(); + /** + * fuzzyListenExecuteBell. + */ + private final BlockingQueue fuzzyListenExecuteBell = new ArrayBlockingQueue<>(1); + Subscriber subscriber = null; /** @@ -602,6 +881,11 @@ public class ConfigRpcTransportClient extends ConfigTransportClient { */ private static final long ALL_SYNC_INTERNAL = 3 * 60 * 1000L; + /** + * fuzzyListenLastAllSyncTime. + */ + private long fuzzyListenLastAllSyncTime = System.currentTimeMillis(); + public ConfigRpcTransportClient(NacosClientProperties properties, ConfigServerListManager serverListManager) { super(properties, serverListManager); } @@ -665,6 +949,85 @@ private Map getLabels() { return labels; } + /** + * Handles a fuzzy listen init notify request. + * + *

This method processes the incoming fuzzy listen init notify request from a client. It updates the fuzzy + * listen context based on the request's information, and publishes events if necessary. + * + * @param request The fuzzy listen init notify request to handle. + * @param clientName The name of the client sending the request. + * @return A {@link FuzzyListenNotifyDiffResponse} indicating the result of handling the request. + */ + private FuzzyListenNotifyDiffResponse handleFuzzyListenNotifyDiffRequest(FuzzyListenNotifyDiffRequest request, + String clientName) { + LOGGER.info("[{}] [fuzzy-listen-config-push] config init.", clientName); + String groupKeyPattern = request.getGroupKeyPattern(); + FuzzyListenContext context = fuzzyListenContextMap.get().get(groupKeyPattern); + if (Constants.ConfigChangeType.FINISH_LISTEN_INIT.equals(request.getServiceChangedType())) { + context.markInitializationComplete(); + return new FuzzyListenNotifyDiffResponse(); + } + for (FuzzyListenNotifyDiffRequest.Context requestContext : request.getContexts()) { + Set existsDataIds = context.getDataIds(); + switch (requestContext.getType()) { + case Constants.ConfigChangeType.LISTEN_INIT: + case Constants.ConfigChangeType.ADD_CONFIG: + if (existsDataIds.add(requestContext.getDataId())) { + NotifyCenter.publishEvent(FuzzyListenNotifyEvent.buildNotifyPatternAllListenersEvent( + requestContext.getGroup(), requestContext.getDataId(), request.getGroupKeyPattern(), + Constants.ConfigChangeType.ADD_CONFIG)); + } + break; + case Constants.ConfigChangeType.DELETE_CONFIG: + if (existsDataIds.remove(requestContext.getDataId())) { + NotifyCenter.publishEvent(FuzzyListenNotifyEvent.buildNotifyPatternAllListenersEvent( + requestContext.getGroup(), requestContext.getDataId(), request.getGroupKeyPattern(), + Constants.ConfigChangeType.DELETE_CONFIG)); + } + break; + default: + LOGGER.error("Invalid config change type: {}", requestContext.getType()); + break; + } + } + return new FuzzyListenNotifyDiffResponse(); + } + + /** + * Handles a fuzzy listen notify change request. + * + *

This method processes the incoming fuzzy listen notify change request from a client. It updates the fuzzy + * listen context based on the request's information, and publishes events if necessary. + * + * @param request The fuzzy listen notify change request to handle. + * @param clientName The name of the client sending the request. + */ + private FuzzyListenNotifyChangeResponse handlerFuzzyListenNotifyChangeRequest( + FuzzyListenNotifyChangeRequest request, String clientName) { + LOGGER.info("[{}] [fuzzy-listen-config-push] config changed.", clientName); + Map listenContextMap = fuzzyListenContextMap.get(); + Set matchedPatterns = GroupKeyPattern.getConfigMatchedPatternsWithoutNamespace(request.getDataId(), + request.getGroup(), listenContextMap.keySet()); + for (String matchedPattern : matchedPatterns) { + FuzzyListenContext context = listenContextMap.get(matchedPattern); + if (request.isExist()) { + if (context.getDataIds().add(request.getDataId())) { + NotifyCenter.publishEvent( + FuzzyListenNotifyEvent.buildNotifyPatternAllListenersEvent(request.getGroup(), + request.getDataId(), matchedPattern, Constants.ConfigChangeType.ADD_CONFIG)); + } + } else { + if (context.getDataIds().remove(request.getDataId())) { + NotifyCenter.publishEvent( + FuzzyListenNotifyEvent.buildNotifyPatternAllListenersEvent(request.getGroup(), + request.getDataId(), matchedPattern, Constants.ConfigChangeType.DELETE_CONFIG)); + } + } + } + return new FuzzyListenNotifyChangeResponse(); + } + ConfigChangeNotifyResponse handleConfigChangeNotifyRequest(ConfigChangeNotifyRequest configChangeNotifyRequest, String clientName) { LOGGER.info("[{}] [server-push] config changed. dataId={}, group={},tenant={}", clientName, @@ -700,6 +1063,14 @@ private void initRpcClientHandler(final RpcClient rpcClientInner) { return handleConfigChangeNotifyRequest((ConfigChangeNotifyRequest) request, rpcClientInner.getName()); } + if (request instanceof FuzzyListenNotifyDiffRequest) { + return handleFuzzyListenNotifyDiffRequest((FuzzyListenNotifyDiffRequest) request, + rpcClientInner.getName()); + } + if (request instanceof FuzzyListenNotifyChangeRequest) { + return handlerFuzzyListenNotifyChangeRequest((FuzzyListenNotifyChangeRequest) request, + rpcClientInner.getName()); + } return null; }); @@ -716,6 +1087,9 @@ private void initRpcClientHandler(final RpcClient rpcClientInner) { public void onConnected(Connection connection) { LOGGER.info("[{}] Connected,notify listen context...", rpcClientInner.getName()); notifyListenConfig(); + + LOGGER.info("[{}] Connected,notify fuzzy listen context...", rpcClientInner.getName()); + notifyFuzzyListenConfig(); } @Override @@ -733,6 +1107,18 @@ public void onDisConnect(Connection connection) { cacheData.setConsistentWithServer(false); } } + + Collection fuzzyListenContexts = fuzzyListenContextMap.get().values(); + + for (FuzzyListenContext context : fuzzyListenContexts) { + if (StringUtils.isNotBlank(taskId)) { + if (Integer.valueOf(taskId).equals(context.getTaskId())) { + context.getIsConsistentWithServer().set(false); + } + } else { + context.getIsConsistentWithServer().set(false); + } + } } }); @@ -769,6 +1155,25 @@ public Class subscribeType() { } }; NotifyCenter.registerSubscriber(subscriber); + + NotifyCenter.registerSubscriber(new Subscriber() { + @Override + public void onEvent(Event event) { + FuzzyListenNotifyEvent fuzzyListenNotifyEvent = (FuzzyListenNotifyEvent) event; + FuzzyListenContext context = fuzzyListenContextMap.get() + .get(fuzzyListenNotifyEvent.getGroupKeyPattern()); + if (context == null) { + return; + } + context.notifyListener(fuzzyListenNotifyEvent.getDataId(), fuzzyListenNotifyEvent.getType(), + fuzzyListenNotifyEvent.getUuid()); + } + + @Override + public Class subscribeType() { + return FuzzyListenNotifyEvent.class; + } + }); } @Override @@ -793,6 +1198,26 @@ public void startInternal() { } }, 0L, TimeUnit.MILLISECONDS); + executor.schedule(() -> { + while (!executor.isShutdown() && !executor.isTerminated()) { + try { + fuzzyListenExecuteBell.poll(5L, TimeUnit.SECONDS); + if (executor.isShutdown() || executor.isTerminated()) { + continue; + } + executeConfigFuzzyListen(); + } catch (Throwable e) { + LOGGER.error("[rpc-fuzzy-listen-execute] rpc fuzzy listen exception", e); + try { + Thread.sleep(50L); + } catch (InterruptedException interruptedException) { + //ignore + } + notifyFuzzyListenConfig(); + } + } + }, 0L, TimeUnit.MILLISECONDS); + } @Override @@ -805,6 +1230,11 @@ public void notifyListenConfig() { listenExecutebell.offer(bellItem); } + @Override + public void notifyFuzzyListenConfig() { + fuzzyListenExecuteBell.offer(bellItem); + } + @Override public void executeConfigListen() throws NacosException { @@ -860,6 +1290,118 @@ public void executeConfigListen() throws NacosException { } + /** + * Execute fuzzy listen configuration changes. + * + *

This method iterates through all fuzzy listen contexts and determines whether they need to be added or + * removed based on their consistency with the server and discard status. It then calls the appropriate method + * to execute the fuzzy listen operation. + * + * @throws NacosException If an error occurs during the execution of fuzzy listen configuration changes. + */ + @Override + public void executeConfigFuzzyListen() throws NacosException { + Map> needSyncContextMap = new HashMap<>(16); + + // Obtain the current timestamp + long now = System.currentTimeMillis(); + + // Determine whether a full synchronization is needed + boolean needAllSync = now - fuzzyListenLastAllSyncTime >= FUZZY_LISTEN_ALL_SYNC_INTERNAL; + + // Iterate through all fuzzy listen contexts + for (FuzzyListenContext context : fuzzyListenContextMap.get().values()) { + // Check if the context is consistent with the server + if (context.getIsConsistentWithServer().get()) { + // Skip if a full synchronization is not needed + if (!needAllSync) { + continue; + } + } + + List needSyncContexts = needSyncContextMap.computeIfAbsent( + String.valueOf(context.getTaskId()), k -> new LinkedList<>()); + needSyncContexts.add(context); + } + + // Execute fuzzy listen operation for addition + doExecuteConfigFuzzyListen(needSyncContextMap); + + // Update last all sync time if a full synchronization was performed + if (needAllSync) { + fuzzyListenLastAllSyncTime = now; + } + } + + /** + * Execute fuzzy listen configuration changes for a specific map of contexts. + * + *

This method submits tasks to execute fuzzy listen operations asynchronously for the provided contexts. It + * waits for all tasks to complete and logs any errors that occur. + * + * @param contextMap The map of contexts to execute fuzzy listen operations for. + * @throws NacosException If an error occurs during the execution of fuzzy listen configuration changes. + */ + private void doExecuteConfigFuzzyListen(Map> contextMap) + throws NacosException { + // Return if the context map is null or empty + if (contextMap == null || contextMap.isEmpty()) { + return; + } + + // List to hold futures for asynchronous tasks + List> listenFutures = new ArrayList<>(); + + // Iterate through the context map and submit tasks for execution + for (Map.Entry> entry : contextMap.entrySet()) { + String taskId = entry.getKey(); + List contexts = entry.getValue(); + RpcClient rpcClient = ensureRpcClient(taskId); + ExecutorService executorService = ensureSyncExecutor(fuzzyListenerTaskPrefix, taskId); + // Submit task for execution + Future future = executorService.submit(() -> { + ConfigBatchFuzzyListenRequest configBatchFuzzyListenRequest = buildFuzzyListenConfigRequest( + contexts); + try { + // Execute the fuzzy listen operation + ConfigBatchFuzzyListenResponse listenResponse = (ConfigBatchFuzzyListenResponse) requestProxy( + rpcClient, configBatchFuzzyListenRequest); + if (listenResponse != null && listenResponse.isSuccess()) { + for (FuzzyListenContext context : contexts) { + if (context.isDiscard()) { + ClientWorker.this.removeFuzzyListenContext(context.getDataIdPattern(), + context.getGroup()); + } else { + context.getIsConsistentWithServer().set(true); + } + } + } + } catch (NacosException e) { + // Log error and retry after a short delay + LOGGER.error("Execute batch fuzzy listen config change error.", e); + try { + Thread.sleep(50L); + } catch (InterruptedException interruptedException) { + // Ignore interruption + } + // Retry notification + notifyFuzzyListenConfig(); + } + }); + listenFutures.add(future); + } + + // Wait for all tasks to complete + for (Future future : listenFutures) { + try { + future.get(); + } catch (Throwable throwable) { + // Log async listen error + LOGGER.error("Async fuzzy listen config change error.", throwable); + } + } + } + /** * Checks and handles local configuration for a given CacheData object. This method evaluates the use of * failover files for local configuration storage and updates the CacheData accordingly. @@ -908,16 +1450,41 @@ public void checkLocalConfig(CacheData cacheData) { } } - private ExecutorService ensureSyncExecutor(String taskId) { - if (!multiTaskExecutor.containsKey(taskId)) { - multiTaskExecutor.put(taskId, + /** + * Ensure to create a synchronous executor for the given task prefix and task ID. If an executor for the given + * task doesn't exist yet, a new executor will be created. + * + * @param taskPrefix The prefix of the task identifier + * @param taskId The ID of the task + * @return The created or existing executor + */ + private ExecutorService ensureSyncExecutor(String taskPrefix, String taskId) { + // Generate the unique task identifier + String taskIdentifier = generateTaskIdentifier(taskPrefix, taskId); + + // If the task identifier doesn't exist in the existing executors, create a new executor and add it to the multiTaskExecutor map + if (!multiTaskExecutor.containsKey(taskIdentifier)) { + multiTaskExecutor.put(taskIdentifier, new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), r -> { - Thread thread = new Thread(r, "nacos.client.config.listener.task-" + taskId); + Thread thread = new Thread(r, taskIdentifier); thread.setDaemon(true); return thread; })); } - return multiTaskExecutor.get(taskId); + + // Return the created or existing executor + return multiTaskExecutor.get(taskIdentifier); + } + + /** + * Generate a task identifier based on the task prefix and task ID. + * + * @param taskPrefix The prefix of the task identifier + * @param taskId The ID of the task + * @return The generated task identifier + */ + private String generateTaskIdentifier(String taskPrefix, String taskId) { + return taskPrefix + "-" + taskId; } private void refreshContentAndCheck(RpcClient rpcClient, String groupKey, boolean notify) { @@ -957,7 +1524,7 @@ private void checkRemoveListenCache(Map> removeListenCac String taskId = entry.getKey(); RpcClient rpcClient = ensureRpcClient(taskId); - ExecutorService executorService = ensureSyncExecutor(taskId); + ExecutorService executorService = ensureSyncExecutor(configListenerTaskPrefix, taskId); Future future = executorService.submit(() -> { List removeListenCaches = entry.getValue(); ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(removeListenCaches); @@ -1007,7 +1574,7 @@ private boolean checkListenCache(Map> listenCachesMap) t String taskId = entry.getKey(); RpcClient rpcClient = ensureRpcClient(taskId); - ExecutorService executorService = ensureSyncExecutor(taskId); + ExecutorService executorService = ensureSyncExecutor(configListenerTaskPrefix, taskId); Future future = executorService.submit(() -> { List listenCaches = entry.getValue(); //reset notify change flag. @@ -1124,12 +1691,33 @@ private ConfigBatchListenRequest buildConfigRequest(List caches) { return configChangeListenRequest; } + /** + * Builds a request for fuzzy listen configuration. + * + * @param contexts The list of fuzzy listen contexts. + * @return A {@code ConfigBatchFuzzyListenRequest} object representing the request. + */ + private ConfigBatchFuzzyListenRequest buildFuzzyListenConfigRequest(List contexts) { + ConfigBatchFuzzyListenRequest request = new ConfigBatchFuzzyListenRequest(); + for (FuzzyListenContext context : contexts) { + request.addContext(getTenant(), context.getGroup(), context.getDataIdPattern(), context.getDataIds(), + !context.isDiscard(), context.isInitializing()); + } + return request; + } + @Override public void removeCache(String dataId, String group) { // Notify to rpc un listen ,and remove cache if success. notifyListenConfig(); } + @Override + public void removeFuzzyListenContext(String dataIdPattern, String group) throws NacosException { + // Notify to rpc un fuzzy listen, and remove cache if success. + notifyFuzzyListenConfig(); + } + /** * send cancel listen config change request . * diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigTransportClient.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigTransportClient.java index 1bd5a8e10c..28d49e5905 100644 --- a/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigTransportClient.java +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigTransportClient.java @@ -19,14 +19,14 @@ import com.alibaba.nacos.api.PropertyKeyConst; import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.api.exception.NacosException; -import com.alibaba.nacos.client.env.NacosClientProperties; -import com.alibaba.nacos.plugin.auth.api.RequestResource; import com.alibaba.nacos.client.config.filter.impl.ConfigResponse; +import com.alibaba.nacos.client.env.NacosClientProperties; import com.alibaba.nacos.client.security.SecurityProxy; +import com.alibaba.nacos.client.utils.ParamUtil; import com.alibaba.nacos.common.utils.ConvertUtils; import com.alibaba.nacos.common.utils.MD5Utils; import com.alibaba.nacos.common.utils.StringUtils; -import com.alibaba.nacos.client.utils.ParamUtil; +import com.alibaba.nacos.plugin.auth.api.RequestResource; import java.util.HashMap; import java.util.Map; @@ -177,6 +177,11 @@ public String getTenant() { **/ public abstract void notifyListenConfig(); + /** + * notify fuzzy listen config. + */ + public abstract void notifyFuzzyListenConfig(); + /** * listen change . * @@ -184,6 +189,13 @@ public String getTenant() { */ public abstract void executeConfigListen() throws NacosException; + /** + * Fuzzy listen change. + * + * @throws NacosException nacos exception throws, should retry. + */ + public abstract void executeConfigFuzzyListen() throws NacosException; + /** * remove cache implements. * @@ -192,6 +204,15 @@ public String getTenant() { */ public abstract void removeCache(String dataId, String group); + /** + * Remove fuzzy listen context. + * + * @param dataIdPattern dataIdPattern + * @param group group + * @throws NacosException if an error occurs while removing the fuzzy listen context. + */ + public abstract void removeFuzzyListenContext(String dataIdPattern, String group) throws NacosException; + /** * query config. * diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/FuzzyListenContext.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/FuzzyListenContext.java new file mode 100644 index 0000000000..93ddbcfbb7 --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/FuzzyListenContext.java @@ -0,0 +1,447 @@ +/* + * Copyright 1999-2023 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.client.config.impl; + +import com.alibaba.nacos.api.config.listener.AbstractFuzzyListenListener; +import com.alibaba.nacos.api.config.listener.FuzzyListenConfigChangeEvent; +import com.alibaba.nacos.client.utils.LogUtils; +import com.alibaba.nacos.common.utils.ConcurrentHashSet; +import com.alibaba.nacos.common.utils.StringUtils; +import org.slf4j.Logger; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Context for fuzzy listening. + * + *

This class manages the context information for fuzzy listening, including environment name, task ID, data ID + * pattern, group, tenant, listener set, and other related information. + *

+ * + * @author stone-98 + * @date 2024/3/4 + */ +public class FuzzyListenContext { + + /** + * Logger for FuzzyListenContext. + */ + private static final Logger LOGGER = LogUtils.logger(FuzzyListenContext.class); + + /** + * Environment name. + */ + private String envName; + + /** + * Task ID. + */ + private int taskId; + + /** + * Data ID pattern. + */ + private String dataIdPattern; + + /** + * Group name. + */ + private String group; + + /** + * Tenant name. + */ + private String tenant; + + /** + * Flag indicating whether the context is consistent with the server. + */ + private final AtomicBoolean isConsistentWithServer = new AtomicBoolean(); + + /** + * Lock object for synchronization of initialization. + */ + private final Lock initializationLock = new ReentrantLock(); + + /** + * Condition object for waiting initialization completion. + */ + private final Condition initializationCompleted = initializationLock.newCondition(); + + /** + * Flag indicating whether the context is initializing. + */ + private boolean isInitializing = false; + + /** + * Flag indicating whether the context is discarded. + */ + private volatile boolean isDiscard = false; + + /** + * Set of data IDs associated with the context. + */ + private Set dataIds = new ConcurrentHashSet<>(); + + /** + * Set of listeners associated with the context. + */ + private Set listeners = new HashSet<>(); + + /** + * Constructor with environment name, data ID pattern, and group. + * + * @param envName Environment name + * @param dataIdPattern Data ID pattern + * @param group Group name + */ + public FuzzyListenContext(String envName, String dataIdPattern, String group) { + this.envName = envName; + this.dataIdPattern = dataIdPattern; + this.group = group; + } + + /** + * Calculate the listeners to notify based on the given UUID. + * + * @param uuid UUID to filter listeners + * @return Set of listeners to notify + */ + public Set calculateListenersToNotify(String uuid) { + Set listenersToNotify = new HashSet<>(); + if (StringUtils.isEmpty(uuid)) { + listenersToNotify = listeners; + } else { + for (AbstractFuzzyListenListener listener : listeners) { + if (uuid.equals(listener.getUuid())) { + listenersToNotify.add(listener); + } + } + } + return listenersToNotify; + } + + /** + * Notify the listener with the specified data ID, type, and UUID. + * + * @param dataId Data ID + * @param type Type of the event + * @param uuid UUID to filter listeners + */ + public void notifyListener(final String dataId, final String type, final String uuid) { + Set listenersToNotify = calculateListenersToNotify(uuid); + doNotifyListener(dataId, type, listenersToNotify); + } + + /** + * Perform the notification for the specified data ID, type, and listeners. + * + * @param dataId Data ID + * @param type Type of the event + * @param listenersToNotify Set of listeners to notify + */ + private void doNotifyListener(final String dataId, final String type, + Set listenersToNotify) { + for (AbstractFuzzyListenListener listener : listenersToNotify) { + AbstractFuzzyNotifyTask job = new AbstractFuzzyNotifyTask() { + @Override + public void run() { + long start = System.currentTimeMillis(); + FuzzyListenConfigChangeEvent event = FuzzyListenConfigChangeEvent.build(group, dataId, type); + if (listener != null) { + listener.onEvent(event); + } + LOGGER.info("[{}] [notify-ok] dataId={}, group={}, tenant={}, listener={}, job run cost={} millis.", + envName, dataId, group, tenant, listener, (System.currentTimeMillis() - start)); + } + }; + + try { + if (null != listener.getExecutor()) { + LOGGER.info( + "[{}] [notify-listener] task submitted to user executor, dataId={}, group={}, tenant={}, listener={}.", + envName, dataId, group, tenant, listener); + job.async = true; + listener.getExecutor().execute(job); + } else { + LOGGER.info( + "[{}] [notify-listener] task execute in nacos thread, dataId={}, group={}, tenant={}, listener={}.", + envName, dataId, group, tenant, listener); + job.run(); + } + } catch (Throwable t) { + LOGGER.error("[{}] [notify-listener-error] dataId={}, group={}, tenant={}, listener={}, throwable={}.", + envName, dataId, group, tenant, listener, t.getCause()); + } + } + } + + + /** + * Wait for initialization to be complete. + * + * @return CompletableFuture> Completes with the collection of data IDs if initialization is + * @return CompletableFuture> Completes with the collection of data IDs if initialization is + * complete, or completes exceptionally if an error occurs + */ + public CompletableFuture> waitForInitializationComplete( + CompletableFuture> future) { + initializationLock.lock(); + try { + while (isInitializing) { + initializationCompleted.await(); + } + future.complete(Collections.unmodifiableCollection(dataIds)); + } catch (InterruptedException e) { + future.completeExceptionally(e); + } finally { + initializationLock.unlock(); + } + return future; + } + + /** + * Mark initialization as complete and notify waiting threads. + */ + public void markInitializationComplete() { + initializationLock.lock(); + try { + isInitializing = false; + initializationCompleted.signalAll(); + } finally { + initializationLock.unlock(); + } + } + + /** + * Remove a listener from the context. + * + * @param listener Listener to be removed + */ + public void removeListener(AbstractFuzzyListenListener listener) { + listeners.remove(listener); + } + + /** + * Add a listener to the context. + * + * @param listener Listener to be added + */ + public void addListener(AbstractFuzzyListenListener listener) { + listeners.add(listener); + } + + /** + * Get the environment name. + * + * @return Environment name + */ + public String getEnvName() { + return envName; + } + + /** + * Set the environment name. + * + * @param envName Environment name to be set + */ + public void setEnvName(String envName) { + this.envName = envName; + } + + /** + * Get the task ID. + * + * @return Task ID + */ + public int getTaskId() { + return taskId; + } + + /** + * Set the task ID. + * + * @param taskId Task ID to be set + */ + public void setTaskId(int taskId) { + this.taskId = taskId; + } + + /** + * Get the data ID pattern. + * + * @return Data ID pattern + */ + public String getDataIdPattern() { + return dataIdPattern; + } + + /** + * Set the data ID pattern. + * + * @param dataIdPattern Data ID pattern to be set + */ + public void setDataIdPattern(String dataIdPattern) { + this.dataIdPattern = dataIdPattern; + } + + /** + * Get the group name. + * + * @return Group name + */ + public String getGroup() { + return group; + } + + /** + * Set the group name. + * + * @param group Group name to be set + */ + public void setGroup(String group) { + this.group = group; + } + + /** + * Get the tenant name. + * + * @return Tenant name + */ + public String getTenant() { + return tenant; + } + + /** + * Set the tenant name. + * + * @param tenant Tenant name to be set + */ + public void setTenant(String tenant) { + this.tenant = tenant; + } + + /** + * Get the flag indicating whether the context is consistent with the server. + * + * @return AtomicBoolean indicating whether the context is consistent with the server + */ + public AtomicBoolean getIsConsistentWithServer() { + return isConsistentWithServer; + } + + /** + * Check if the context is discarded. + * + * @return True if the context is discarded, otherwise false + */ + public boolean isDiscard() { + return isDiscard; + } + + /** + * Set the flag indicating whether the context is discarded. + * + * @param discard True to mark the context as discarded, otherwise false + */ + public void setDiscard(boolean discard) { + isDiscard = discard; + } + + /** + * Check if the context is initializing. + * + * @return True if the context is initializing, otherwise false + */ + public boolean isInitializing() { + return isInitializing; + } + + /** + * Set the flag indicating whether the context is initializing. + * + * @param initializing True to mark the context as initializing, otherwise false + */ + public void setInitializing(boolean initializing) { + isInitializing = initializing; + } + + /** + * Get the set of data IDs associated with the context. + * + * @return Set of data IDs + */ + public Set getDataIds() { + return Collections.unmodifiableSet(dataIds); + } + + /** + * Set the set of data IDs associated with the context. + * + * @param dataIds Set of data IDs to be set + */ + public void setDataIds(Set dataIds) { + this.dataIds = dataIds; + } + + /** + * Get the set of listeners associated with the context. + * + * @return Set of listeners + */ + public Set getListeners() { + return listeners; + } + + /** + * Set the set of listeners associated with the context. + * + * @param listeners Set of listeners to be set + */ + public void setListeners(Set listeners) { + this.listeners = listeners; + } + + /** + * Abstract task for fuzzy notification. + */ + abstract static class AbstractFuzzyNotifyTask implements Runnable { + + /** + * Flag indicating whether the task is asynchronous. + */ + boolean async = false; + + /** + * Check if the task is asynchronous. + * + * @return True if the task is asynchronous, otherwise false + */ + public boolean isAsync() { + return async; + } + } +} + diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/FuzzyListenNotifyEvent.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/FuzzyListenNotifyEvent.java new file mode 100644 index 0000000000..a710630c13 --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/FuzzyListenNotifyEvent.java @@ -0,0 +1,200 @@ +/* + * Copyright 1999-2023 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.client.config.impl; + +import com.alibaba.nacos.common.notify.Event; + +/** + * Event class for fuzzy listen notifications. + * + *

This class represents an event used for notifying fuzzy listen changes. It extends {@link Event}, indicating + * that it may be processed asynchronously. The event contains information about the group, dataId, type, and UUID of + * the notification. + * + * @author stone-98 + * @date 2024/3/4 + */ +public class FuzzyListenNotifyEvent extends Event { + + /** + * The unique identifier for the listener. + */ + private String uuid; + + /** + * The groupKeyPattern of configuration. + */ + private String groupKeyPattern; + + /** + * The group of the configuration. + */ + private String group; + + /** + * The dataId of the configuration. + */ + private String dataId; + + /** + * The type of notification (e.g., ADD_CONFIG, DELETE_CONFIG). + */ + private String type; + + /** + * Constructs a new FuzzyListenNotifyEvent. + */ + public FuzzyListenNotifyEvent() { + } + + /** + * Constructs a new FuzzyListenNotifyEvent with the specified group, dataId, type, and UUID. + * + * @param group The group of the configuration. + * @param dataId The dataId of the configuration. + * @param type The type of notification. + * @param uuid The UUID (Unique Identifier) of the listener. + */ + public FuzzyListenNotifyEvent(String group, String dataId, String type, String groupKeyPattern, String uuid) { + this.group = group; + this.dataId = dataId; + this.type = type; + this.groupKeyPattern = groupKeyPattern; + this.uuid = uuid; + } + + /** + * Constructs a new FuzzyListenNotifyEvent with the specified group, dataId, and type. + * + * @param group The group of the configuration. + * @param dataId The dataId of the configuration. + * @param type The type of notification. + */ + public FuzzyListenNotifyEvent(String group, String dataId, String type, String groupKeyPattern) { + this.group = group; + this.dataId = dataId; + this.type = type; + this.groupKeyPattern = groupKeyPattern; + } + + /** + * Builds a new FuzzyListenNotifyEvent with the specified group, dataId, type, and UUID. + * + * @param group The group of the configuration. + * @param dataId The dataId of the configuration. + * @param type The type of notification. + * @param uuid The UUID (Unique Identifier) of the listener. + * @return A new FuzzyListenNotifyEvent instance. + */ + public static FuzzyListenNotifyEvent buildNotifyPatternSpecificListenerEvent(String group, String dataId, + String type, String groupKeyPattern, String uuid) { + return new FuzzyListenNotifyEvent(group, dataId, type, groupKeyPattern, uuid); + } + + /** + * Builds a new FuzzyListenNotifyEvent with the specified group, dataId, and type. + * + * @param group The group of the configuration. + * @param dataId The dataId of the configuration. + * @param type The type of notification. + * @return A new FuzzyListenNotifyEvent instance. + */ + public static FuzzyListenNotifyEvent buildNotifyPatternAllListenersEvent(String group, String dataId, + String groupKeyPattern, String type) { + return new FuzzyListenNotifyEvent(group, dataId, type, groupKeyPattern); + } + + /** + * Gets the UUID (Unique Identifier) of the listener. + * + * @return The UUID of the listener. + */ + public String getUuid() { + return uuid; + } + + /** + * Sets the UUID (Unique Identifier) of the listener. + * + * @param uuid The UUID to set. + */ + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + public void setGroupKeyPattern(String groupKeyPattern) { + this.groupKeyPattern = groupKeyPattern; + } + + /** + * Gets the group of the configuration. + * + * @return The group of the configuration. + */ + public String getGroup() { + return group; + } + + /** + * Sets the group of the configuration. + * + * @param group The group to set. + */ + public void setGroup(String group) { + this.group = group; + } + + /** + * Gets the dataId of the configuration. + * + * @return The dataId of the configuration. + */ + public String getDataId() { + return dataId; + } + + /** + * Sets the dataId of the configuration. + * + * @param dataId The dataId to set. + */ + public void setDataId(String dataId) { + this.dataId = dataId; + } + + /** + * Gets the type of notification. + * + * @return The type of notification. + */ + public String getType() { + return type; + } + + /** + * Sets the type of notification. + * + * @param type The type to set. + */ + public void setType(String type) { + this.type = type; + } +} diff --git a/client/src/main/java/com/alibaba/nacos/client/utils/ParamUtil.java b/client/src/main/java/com/alibaba/nacos/client/utils/ParamUtil.java index 4325d60adc..f6f8be2b4c 100644 --- a/client/src/main/java/com/alibaba/nacos/client/utils/ParamUtil.java +++ b/client/src/main/java/com/alibaba/nacos/client/utils/ParamUtil.java @@ -62,6 +62,8 @@ public class ParamUtil { private static double perTaskConfigSize = 3000; + private static final String PER_TASK_CONTEXT_SIZE_KEY = "PER_TASK_CONTEXT_SIZE_KEY"; + private static final String NACOS_CLIENT_APP_KEY = "nacos.client.appKey"; private static final String BLANK_STR = ""; @@ -84,6 +86,9 @@ public class ParamUtil { private static final String DEFAULT_PER_TASK_CONFIG_SIZE_KEY = "3000"; + private static final String DEFAULT_PER_TASK_CONTEXT_SIZE_KEY = "3000"; + + private static double perTaskContextSize = 3000; private static final int DESENSITISE_PARAMETER_MIN_LENGTH = 2; private static final int DESENSITISE_PARAMETER_KEEP_ONE_CHAR_LENGTH = 8; @@ -110,6 +115,9 @@ public class ParamUtil { perTaskConfigSize = initPerTaskConfigSize(); LOGGER.info("PER_TASK_CONFIG_SIZE: {}", perTaskConfigSize); + + perTaskContextSize = initPerTaskContextSize(); + LOGGER.info("PER_TASK_CONTEXT_SIZE: {}", perTaskContextSize); } private static int initConnectionTimeout() { @@ -146,6 +154,16 @@ private static double initPerTaskConfigSize() { } } + private static double initPerTaskContextSize() { + try { + return Double.parseDouble(NacosClientProperties.PROTOTYPE.getProperty(PER_TASK_CONTEXT_SIZE_KEY, + DEFAULT_PER_TASK_CONTEXT_SIZE_KEY)); + } catch (NumberFormatException e) { + LOGGER.error("[PER_TASK_CONTEXT_SIZE] PER_TASK_CONTEXT_SIZE invalid", e); + throw new IllegalArgumentException("invalid PER_TASK_CONTEXT_SIZE, expected value type double", e); + } + } + public static String getAppKey() { return appKey; } @@ -202,6 +220,14 @@ public static void setPerTaskConfigSize(double perTaskConfigSize) { ParamUtil.perTaskConfigSize = perTaskConfigSize; } + public static double getPerTaskContextSize() { + return perTaskContextSize; + } + + public static void setPerTaskContextSize(double perTaskContextSize) { + ParamUtil.perTaskContextSize = perTaskContextSize; + } + public static String getDefaultServerPort() { return serverPort; } @@ -257,10 +283,10 @@ public static String parsingEndpointRule(String endpointUrl) { if (StringUtils.isNotBlank(endpointUrlSource)) { endpointUrl = endpointUrlSource; } - + return StringUtils.isNotBlank(endpointUrl) ? endpointUrl : ""; } - + endpointUrl = endpointUrl.substring(endpointUrl.indexOf("${") + 2, endpointUrl.lastIndexOf("}")); int defStartOf = endpointUrl.indexOf(":"); String defaultEndpointUrl = null; @@ -268,12 +294,13 @@ public static String parsingEndpointRule(String endpointUrl) { defaultEndpointUrl = endpointUrl.substring(defStartOf + 1); endpointUrl = endpointUrl.substring(0, defStartOf); } + String endpointUrlSource = TemplateUtils.stringBlankAndThenExecute( NacosClientProperties.PROTOTYPE.getProperty(endpointUrl), () -> NacosClientProperties.PROTOTYPE.getProperty( PropertyKeyConst.SystemEnv.ALIBABA_ALIWARE_ENDPOINT_URL)); - + if (StringUtils.isBlank(endpointUrlSource)) { if (StringUtils.isNotBlank(defaultEndpointUrl)) { endpointUrl = defaultEndpointUrl; @@ -281,7 +308,7 @@ public static String parsingEndpointRule(String endpointUrl) { } else { endpointUrl = endpointUrlSource; } - + return StringUtils.isNotBlank(endpointUrl) ? endpointUrl : ""; } diff --git a/client/src/test/java/com/alibaba/nacos/client/config/NacosConfigServiceTest.java b/client/src/test/java/com/alibaba/nacos/client/config/NacosConfigServiceTest.java index 7e56dde22d..4cf6293689 100644 --- a/client/src/test/java/com/alibaba/nacos/client/config/NacosConfigServiceTest.java +++ b/client/src/test/java/com/alibaba/nacos/client/config/NacosConfigServiceTest.java @@ -38,7 +38,7 @@ import org.mockito.quality.Strictness; import java.lang.reflect.Field; -import java.util.Arrays; +import java.util.Collections; import java.util.Properties; import java.util.concurrent.Executor; @@ -157,6 +157,7 @@ void testGetConfig403() throws NacosException { .thenThrow(new NacosException(NacosException.NO_RIGHT, "no right")); try { nacosConfigService.getConfig(dataId, group, timeout); + Assert.fail(); assertTrue(false); } catch (NacosException e) { assertEquals(NacosException.NO_RIGHT, e.getErrCode()); @@ -195,27 +196,42 @@ public void receiveConfigInfo(String configInfo) { public void startInternal() throws NacosException { // NOOP } - + @Override public String getName() { return "TestConfigTransportClient"; } - + @Override public void notifyListenConfig() { // NOOP } - + + @Override + public void notifyFuzzyListenConfig() { + // NOOP + } + @Override public void executeConfigListen() { // NOOP } - + + @Override + public void executeConfigFuzzyListen() throws NacosException { + // NOOP + } + @Override public void removeCache(String dataId, String group) { // NOOP } - + + @Override + public void removeFuzzyListenContext(String dataIdPattern, String group) { + // NOOP + } + @Override public ConfigResponse queryConfig(String dataId, String group, String tenant, long readTimeous, boolean notify) throws NacosException { @@ -241,6 +257,10 @@ public boolean removeConfig(String dataId, String group, String tenant, String t Mockito.when(mockWoker.getAgent()).thenReturn(client); final String config = nacosConfigService.getConfigAndSignListener(dataId, group, timeout, listener); + Assert.assertEquals(content, config); + + Mockito.verify(mockWoker, Mockito.times(1)) + .addTenantListenersWithContent(dataId, group, content, null, Collections.singletonList(listener)); assertEquals(content, config); Mockito.verify(mockWoker, Mockito.times(1)).addTenantListenersWithContent(dataId, group, content, null, Arrays.asList(listener)); @@ -255,15 +275,16 @@ void testAddListener() throws NacosException { public Executor getExecutor() { return null; } - + @Override public void receiveConfigInfo(String configInfo) { - + } }; - + nacosConfigService.addListener(dataId, group, listener); - Mockito.verify(mockWoker, Mockito.times(1)).addTenantListeners(dataId, group, Arrays.asList(listener)); + Mockito.verify(mockWoker, Mockito.times(1)) + .addTenantListeners(dataId, group, Collections.singletonList(listener)); } @Test @@ -383,4 +404,4 @@ void testShutDown() { nacosConfigService.shutDown(); }); } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/alibaba/nacos/common/utils/GroupKey.java b/common/src/main/java/com/alibaba/nacos/common/utils/GroupKey.java new file mode 100644 index 0000000000..c821876ed5 --- /dev/null +++ b/common/src/main/java/com/alibaba/nacos/common/utils/GroupKey.java @@ -0,0 +1,140 @@ +/* + * 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.common.utils; + +/** + * Synthesize the form of dataId+groupId. Escapes reserved characters in dataId and groupId. + * + * @author Nacos + */ +public class GroupKey { + + private static final char PLUS = '+'; + + private static final char PERCENT = '%'; + + private static final char TWO = '2'; + + private static final char B = 'B'; + + private static final char FIVE = '5'; + + public static String getKey(String dataId, String group) { + return getKey(dataId, group, ""); + } + + public static String getKey(String dataId, String group, String datumStr) { + return doGetKey(dataId, group, datumStr); + } + + public static String getKeyTenant(String dataId, String group, String tenant) { + return doGetKey(dataId, group, tenant); + } + + private static String doGetKey(String dataId, String group, String datumStr) { + if (StringUtils.isBlank(dataId)) { + throw new IllegalArgumentException("invalid dataId"); + } + if (StringUtils.isBlank(group)) { + throw new IllegalArgumentException("invalid group"); + } + StringBuilder sb = new StringBuilder(); + urlEncode(dataId, sb); + sb.append(PLUS); + urlEncode(group, sb); + if (StringUtils.isNotEmpty(datumStr)) { + sb.append(PLUS); + urlEncode(datumStr, sb); + } + + return sb.toString(); + } + + /** + * Parse key. + * + * @param groupKey group key + * @return parsed key + */ + public static String[] parseKey(String groupKey) { + StringBuilder sb = new StringBuilder(); + String dataId = null; + String group = null; + String tenant = null; + + for (int i = 0; i < groupKey.length(); ++i) { + char c = groupKey.charAt(i); + if (PLUS == c) { + if (null == dataId) { + dataId = sb.toString(); + sb.setLength(0); + } else if (null == group) { + group = sb.toString(); + sb.setLength(0); + } else { + throw new IllegalArgumentException("invalid groupkey:" + groupKey); + } + } else if (PERCENT == c) { + char next = groupKey.charAt(++i); + char nextnext = groupKey.charAt(++i); + if (TWO == next && B == nextnext) { + sb.append(PLUS); + } else if (TWO == next && FIVE == nextnext) { + sb.append(PERCENT); + } else { + throw new IllegalArgumentException("invalid groupkey:" + groupKey); + } + } else { + sb.append(c); + } + } + + if (group == null) { + group = sb.toString(); + } else { + tenant = sb.toString(); + } + + if (StringUtils.isBlank(dataId)) { + throw new IllegalArgumentException("invalid dataId"); + } + if (StringUtils.isBlank(group)) { + throw new IllegalArgumentException("invalid group"); + } + if (StringUtils.isBlank(tenant)) { + return new String[] {dataId, group}; + } + return new String[] {dataId, group, tenant}; + } + + /** + * + -> %2B % -> %25. + */ + static void urlEncode(String str, StringBuilder sb) { + for (int idx = 0; idx < str.length(); ++idx) { + char c = str.charAt(idx); + if (PLUS == c) { + sb.append("%2B"); + } else if (PERCENT == c) { + sb.append("%25"); + } else { + sb.append(c); + } + } + } + +} diff --git a/common/src/main/java/com/alibaba/nacos/common/utils/GroupKeyPattern.java b/common/src/main/java/com/alibaba/nacos/common/utils/GroupKeyPattern.java new file mode 100644 index 0000000000..f00d50e7db --- /dev/null +++ b/common/src/main/java/com/alibaba/nacos/common/utils/GroupKeyPattern.java @@ -0,0 +1,259 @@ +/* + * Copyright 1999-2023 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.common.utils; + +import com.alibaba.nacos.api.common.Constants; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import static com.alibaba.nacos.api.common.Constants.DATA_ID_SPLITTER; +import static com.alibaba.nacos.api.common.Constants.FUZZY_LISTEN_PATTERN_WILDCARD; +import static com.alibaba.nacos.api.common.Constants.NAMESPACE_ID_SPLITTER; + +/** + * Utility class for matching group keys against a given pattern. + * + *

This class provides methods to match group keys based on a pattern specified. It supports matching based on + * dataId, group, and namespace components of the group key. + * + * @author stone-98 + * @date 2024/3/14 + */ +public class GroupKeyPattern { + + /** + * Generates a fuzzy listen group key pattern based on the given dataId pattern, group, and optional tenant. + * + *

This method generates a unique group key pattern for fuzzy listen based on the specified dataId pattern, + * group, and optional tenant. It concatenates the dataId pattern, group, and tenant (if provided) with a delimiter + * and returns the resulting string. The resulting string is interned to improve memory efficiency. + * + * @param dataIdPattern The pattern for matching dataIds. + * @param group The group associated with the dataIds. + * @param namespace (Optional) The tenant associated with the dataIds (can be null or empty). + * @return A unique group key pattern for fuzzy listen. + * @throws IllegalArgumentException If the dataId pattern or group is blank. + */ + public static String generateFuzzyListenGroupKeyPattern(final String dataIdPattern, final String group, + final String namespace) { + if (StringUtils.isBlank(dataIdPattern)) { + throw new IllegalArgumentException("Param 'dataIdPattern' is illegal, dataIdPattern is blank"); + } + if (StringUtils.isBlank(group)) { + throw new IllegalArgumentException("Param 'group' is illegal, group is blank"); + } + StringBuilder sb = new StringBuilder(); + if (StringUtils.isNotBlank(namespace)) { + sb.append(namespace); + } + sb.append(NAMESPACE_ID_SPLITTER); + sb.append(group); + sb.append(DATA_ID_SPLITTER); + sb.append(dataIdPattern); + return sb.toString().intern(); + } + + /** + * Generates a fuzzy listen group key pattern based on the given dataId pattern and group. + * + *

This method generates a unique group key pattern for fuzzy listen based on the specified dataId pattern and + * group. It concatenates the dataId pattern and group with a delimiter and returns the resulting string. The + * resulting string is interned to improve memory efficiency. + * + * @param dataIdPattern The pattern for matching dataIds. + * @param group The group associated with the dataIds. + * @return A unique group key pattern for fuzzy listen. + * @throws IllegalArgumentException If the dataId pattern or group is blank. + */ + public static String generateFuzzyListenGroupKeyPattern(final String dataIdPattern, final String group) { + if (StringUtils.isBlank(dataIdPattern)) { + throw new IllegalArgumentException("Param 'dataIdPattern' is illegal, dataIdPattern is blank"); + } + if (StringUtils.isBlank(group)) { + throw new IllegalArgumentException("Param 'group' is illegal, group is blank"); + } + final String fuzzyListenGroupKey = group + DATA_ID_SPLITTER + dataIdPattern; + return fuzzyListenGroupKey.intern(); + } + + /** + * Checks whether a group key matches the specified pattern. + * + * @param groupKey The group key to match. + * @param groupKeyPattern The pattern to match against. + * @return {@code true} if the group key matches the pattern, otherwise {@code false}. + */ + public static boolean isMatchPatternWithNamespace(String groupKey, String groupKeyPattern) { + String[] parseKey = GroupKey.parseKey(groupKey); + String dataId = parseKey[0]; + String group = parseKey[1]; + String namespace = parseKey.length > 2 ? parseKey[2] : Constants.DEFAULT_NAMESPACE_ID; + + String namespacePattern = getNamespace(groupKeyPattern); + String groupPattern = getGroup(groupKeyPattern); + String dataIdPattern = getDataIdPattern(groupKeyPattern); + + if (dataIdPattern.equals(FUZZY_LISTEN_PATTERN_WILDCARD)) { + return namespace.equals(namespacePattern) && group.equals(groupPattern); + } + + if (dataIdPattern.endsWith(FUZZY_LISTEN_PATTERN_WILDCARD)) { + String dataIdPrefix = dataIdPattern.substring(0, dataIdPattern.length() - 1); + return namespace.equals(namespacePattern) && groupPattern.equals(group) && dataId.startsWith(dataIdPrefix); + } + + return namespace.equals(namespacePattern) && group.equals(groupPattern) && dataId.equals(dataIdPattern); + } + + /** + * Checks whether a group key matches the specified pattern. + * + * @param groupKey The group key to match. + * @param groupKeyPattern The pattern to match against. + * @return {@code true} if the group key matches the pattern, otherwise {@code false}. + */ + public static boolean isMatchPatternWithoutNamespace(String groupKey, String groupKeyPattern) { + String[] parseKey = GroupKey.parseKey(groupKey); + String dataId = parseKey[0]; + String group = parseKey[1]; + + String groupPattern = getGroup(groupKeyPattern); + String dataIdPattern = getDataIdPattern(groupKeyPattern); + + if (dataIdPattern.equals(FUZZY_LISTEN_PATTERN_WILDCARD)) { + return group.equals(groupPattern); + } + + if (dataIdPattern.endsWith(FUZZY_LISTEN_PATTERN_WILDCARD)) { + String dataIdPrefix = dataIdPattern.substring(0, dataIdPattern.length() - 1); + return groupPattern.equals(group) && dataId.startsWith(dataIdPrefix); + } + + return group.equals(groupPattern) && dataId.equals(dataIdPattern); + } + + /** + * Given a dataId, group, dataId pattern, and group pattern, determines whether it can match. + * + * @param dataId The dataId to match. + * @param group The group to match. + * @param dataIdPattern The dataId pattern to match against. + * @param groupPattern The group pattern to match against. + * @return {@code true} if the dataId and group match the patterns, otherwise {@code false}. + */ + public static boolean isMatchPatternWithoutNamespace(String dataId, String group, String dataIdPattern, + String groupPattern) { + String groupKey = GroupKey.getKey(dataId, group); + String groupKeyPattern = generateFuzzyListenGroupKeyPattern(dataIdPattern, groupPattern); + return isMatchPatternWithoutNamespace(groupKey, groupKeyPattern); + } + + /** + * Given a dataId, group, and a collection of completed group key patterns, returns the patterns that match. + * + * @param dataId The dataId to match. + * @param group The group to match. + * @param groupKeyPatterns The collection of completed group key patterns to match against. + * @return A set of patterns that match the dataId and group. + */ + public static Set getConfigMatchedPatternsWithoutNamespace(String dataId, String group, + Collection groupKeyPatterns) { + if (CollectionUtils.isEmpty(groupKeyPatterns)) { + return new HashSet<>(1); + } + Set matchedPatternList = new HashSet<>(); + for (String keyPattern : groupKeyPatterns) { + if (isMatchPatternWithoutNamespace(dataId, group, getDataIdPattern(keyPattern), getGroup(keyPattern))) { + matchedPatternList.add(keyPattern); + } + } + return matchedPatternList; + } + + /** + * Extracts the namespace from the given group key pattern. + * + * @param groupKeyPattern The group key pattern from which to extract the namespace. + * @return The namespace extracted from the group key pattern. + */ + public static String getNamespace(final String groupKeyPattern) { + if (StringUtils.isBlank(groupKeyPattern)) { + return StringUtils.EMPTY; + } + if (!groupKeyPattern.contains(NAMESPACE_ID_SPLITTER)) { + return StringUtils.EMPTY; + } + return groupKeyPattern.split(NAMESPACE_ID_SPLITTER)[0]; + } + + /** + * Extracts the group from the given group key pattern. + * + * @param groupKeyPattern The group key pattern from which to extract the group. + * @return The group extracted from the group key pattern. + */ + public static String getGroup(final String groupKeyPattern) { + if (StringUtils.isBlank(groupKeyPattern)) { + return StringUtils.EMPTY; + } + String groupWithNamespace; + if (!groupKeyPattern.contains(DATA_ID_SPLITTER)) { + groupWithNamespace = groupKeyPattern; + } else { + groupWithNamespace = groupKeyPattern.split(DATA_ID_SPLITTER)[0]; + } + + if (!groupKeyPattern.contains(NAMESPACE_ID_SPLITTER)) { + return groupWithNamespace; + } + return groupWithNamespace.split(NAMESPACE_ID_SPLITTER)[1]; + } + + /** + * Extracts the dataId pattern from the given group key pattern. + * + * @param groupKeyPattern The group key pattern from which to extract the dataId pattern. + * @return The dataId pattern extracted from the group key pattern. + */ + public static String getDataIdPattern(final String groupKeyPattern) { + if (StringUtils.isBlank(groupKeyPattern)) { + return StringUtils.EMPTY; + } + if (!groupKeyPattern.contains(DATA_ID_SPLITTER)) { + return StringUtils.EMPTY; + } + return groupKeyPattern.split(DATA_ID_SPLITTER)[1]; + } + + /** + * Given a completed pattern, removes the namespace. + * + * @param completedPattern The completed pattern from which to remove the namespace. + * @return The pattern with the namespace removed. + */ + public static String getPatternRemovedNamespace(String completedPattern) { + if (StringUtils.isBlank(completedPattern)) { + return StringUtils.EMPTY; + } + if (!completedPattern.contains(NAMESPACE_ID_SPLITTER)) { + return completedPattern; + } + return completedPattern.split(NAMESPACE_ID_SPLITTER)[1]; + } +} diff --git a/common/src/test/java/com/alibaba/nacos/common/utils/GroupKeyPatternTest.java b/common/src/test/java/com/alibaba/nacos/common/utils/GroupKeyPatternTest.java new file mode 100644 index 0000000000..a16e222ecb --- /dev/null +++ b/common/src/test/java/com/alibaba/nacos/common/utils/GroupKeyPatternTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 1999-2023 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.common.utils; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +/** + * GroupKeyPatternUtilsTest. + * + * @author stone-98 + * @date 2024/3/19 + */ +public class GroupKeyPatternTest { + + @Test + public void testGetGroupKeyPatternWithNamespace() { + String dataIdPattern = "examplePattern*"; + String group = "exampleGroup"; + String namespace = "exampleNamespace"; + + String groupKeyPattern = GroupKeyPattern.generateFuzzyListenGroupKeyPattern(dataIdPattern, group, namespace); + + Assert.assertEquals("exampleNamespace>>exampleGroup@@examplePattern*", groupKeyPattern); + } + + @Test + public void testGetGroupKeyPatternWithoutNamespace() { + String dataIdPattern = "examplePattern*"; + String group = "exampleGroup"; + + String groupKeyPattern = GroupKeyPattern.generateFuzzyListenGroupKeyPattern(dataIdPattern, group); + + Assert.assertEquals("exampleGroup@@examplePattern*", groupKeyPattern); + } + + @Test + public void testIsMatchPatternWithNamespace() { + String groupKey = "examplePattern+exampleGroup+exampleNamespace"; + String groupKeyPattern = "exampleNamespace>>exampleGroup@@examplePattern*"; + + boolean result = GroupKeyPattern.isMatchPatternWithNamespace(groupKey, groupKeyPattern); + + Assert.assertTrue(result); + } + + @Test + public void testIsMatchPatternWithoutNamespace() { + String groupKey = "examplePattern+exampleGroup+exampleNamespace"; + String groupKeyPattern = "exampleNamespace>>exampleGroup@@*"; + + boolean result = GroupKeyPattern.isMatchPatternWithoutNamespace(groupKey, groupKeyPattern); + + Assert.assertTrue(result); + } + + @Test + public void testIsMatchPatternWithoutNamespaceWithDataIdPrefix() { + String groupKey = "examplePattern+exampleGroup+exampleNamespace"; + String groupKeyPattern = "exampleNamespace>>exampleGroup@@examplePattern*"; + + boolean result = GroupKeyPattern.isMatchPatternWithoutNamespace(groupKey, groupKeyPattern); + + Assert.assertTrue(result); + } + + @Test + public void testGetConfigMatchedPatternsWithoutNamespace() { + String dataId = "exampleDataId"; + String group = "exampleGroup"; + Set groupKeyPatterns = new HashSet<>(); + groupKeyPatterns.add("exampleGroup@@exampleDataId*"); + groupKeyPatterns.add("exampleGroup@@exampleDataI*"); + + Set matchedPatterns = GroupKeyPattern.getConfigMatchedPatternsWithoutNamespace(dataId, group, + groupKeyPatterns); + + Assert.assertEquals(2, matchedPatterns.size()); + Assert.assertTrue(matchedPatterns.contains("exampleGroup@@exampleDataId*")); + Assert.assertTrue(matchedPatterns.contains("exampleGroup@@exampleDataI*")); + } + + @Test + public void testGetNamespace() { + String groupKeyPattern = "exampleNamespace>>exampleGroup@@examplePattern"; + + String namespace = GroupKeyPattern.getNamespace(groupKeyPattern); + + Assert.assertEquals("exampleNamespace", namespace); + } + + @Test + public void testGetGroup() { + String groupKeyPattern = "exampleNamespace>>exampleGroup@@examplePattern"; + + String group = GroupKeyPattern.getGroup(groupKeyPattern); + + Assert.assertEquals("exampleGroup", group); + } + + @Test + public void testGetDataIdPattern() { + String groupKeyPattern = "exampleNamespace>>exampleGroup@@examplePattern"; + + String dataIdPattern = GroupKeyPattern.getDataIdPattern(groupKeyPattern); + + Assert.assertEquals("examplePattern", dataIdPattern); + } + + @Test + public void testGetPatternRemovedNamespace() { + String groupKeyPattern = "exampleNamespace>>exampleGroup@@examplePattern"; + + String patternRemovedNamespace = GroupKeyPattern.getPatternRemovedNamespace(groupKeyPattern); + + Assert.assertEquals("exampleGroup@@examplePattern", patternRemovedNamespace); + } +} + diff --git a/config/src/main/java/com/alibaba/nacos/config/server/configuration/ConfigCommonConfig.java b/config/src/main/java/com/alibaba/nacos/config/server/configuration/ConfigCommonConfig.java index ac6e286877..722e581849 100644 --- a/config/src/main/java/com/alibaba/nacos/config/server/configuration/ConfigCommonConfig.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/configuration/ConfigCommonConfig.java @@ -33,6 +33,9 @@ public class ConfigCommonConfig extends AbstractDynamicConfig { private int maxPushRetryTimes = 50; + private long pushTimeout = 3000L; + + private int batchSize = 10; private boolean derbyOpsEnabled = false; private ConfigCommonConfig() { @@ -52,6 +55,20 @@ public void setMaxPushRetryTimes(int maxPushRetryTimes) { this.maxPushRetryTimes = maxPushRetryTimes; } + public long getPushTimeout() { + return pushTimeout; + } + + public void setPushTimeout(long pushTimeout) { + this.pushTimeout = pushTimeout; + } + + public int getBatchSize() { + return batchSize; + } + + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; public boolean isDerbyOpsEnabled() { return derbyOpsEnabled; } @@ -63,6 +80,8 @@ public void setDerbyOpsEnabled(boolean derbyOpsEnabled) { @Override protected void getConfigFromEnv() { maxPushRetryTimes = EnvUtil.getProperty("nacos.config.push.maxRetryTime", Integer.class, 50); + pushTimeout = EnvUtil.getProperty("nacos.config.push.timeout", Long.class, 3000L); + pushTimeout = EnvUtil.getProperty("nacos.config.push.batchSize", Integer.class, 10); derbyOpsEnabled = EnvUtil.getProperty("nacos.config.derby.ops.enabled", Boolean.class, false); } diff --git a/config/src/main/java/com/alibaba/nacos/config/server/model/CacheItem.java b/config/src/main/java/com/alibaba/nacos/config/server/model/CacheItem.java index e0423f3daa..836713a3be 100644 --- a/config/src/main/java/com/alibaba/nacos/config/server/model/CacheItem.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/model/CacheItem.java @@ -16,6 +16,8 @@ package com.alibaba.nacos.config.server.model; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.StringUtils; import com.alibaba.nacos.config.server.utils.SimpleReadWriteLock; import com.alibaba.nacos.core.utils.StringPool; @@ -127,5 +129,19 @@ public void clearConfigGrays() { this.configCacheGray = null; this.sortedConfigCacheGrayList = null; } + + /** + * Checks if the configuration is effective for the specified client IP and tag. + * + * @param tag The tag associated with the configuration. + * @param clientIp The IP address of the client. + * @return true if the configuration is effective for the client, false otherwise. + */ + public boolean effectiveForClient(String tag, String clientIp) { + if (isBeta && CollectionUtils.isNotEmpty(ips4Beta) && !ips4Beta.contains(clientIp)) { + return false; + } + return StringUtils.isBlank(tag) || (getConfigCacheTags() != null && getConfigCacheTags().containsKey(tag)); + } } diff --git a/config/src/main/java/com/alibaba/nacos/config/server/model/event/ConfigBatchFuzzyListenEvent.java b/config/src/main/java/com/alibaba/nacos/config/server/model/event/ConfigBatchFuzzyListenEvent.java new file mode 100644 index 0000000000..2e44763ee9 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/model/event/ConfigBatchFuzzyListenEvent.java @@ -0,0 +1,142 @@ +/* + * Copyright 1999-2023 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.config.server.model.event; + +import com.alibaba.nacos.common.notify.Event; + +import java.util.Set; + +/** + * This event represents a batch fuzzy listening event for configurations. It is used to notify the server about a batch + * of fuzzy listening requests from clients. Each request contains a client ID, a set of existing group keys associated + * with the client, a key group pattern, and a flag indicating whether the client is initializing. + * + * @author stone-98 + * @date 2024/3/5 + */ +public class ConfigBatchFuzzyListenEvent extends Event { + + private static final long serialVersionUID = 1953965691384930209L; + + /** + * ID of the client making the request. + */ + private String clientId; + + /** + * Pattern for matching group keys. + */ + private String keyGroupPattern; + + /** + * Set of existing group keys associated with the client. + */ + private Set clientExistingGroupKeys; + + /** + * Flag indicating whether the client is initializing. + */ + private boolean isInitializing; + + /** + * Constructs a new ConfigBatchFuzzyListenEvent with the specified parameters. + * + * @param clientId ID of the client making the request + * @param clientExistingGroupKeys Set of existing group keys associated with the client + * @param keyGroupPattern Pattern for matching group keys + * @param isInitializing Flag indicating whether the client is initializing + */ + public ConfigBatchFuzzyListenEvent(String clientId, Set clientExistingGroupKeys, String keyGroupPattern, + boolean isInitializing) { + this.clientId = clientId; + this.clientExistingGroupKeys = clientExistingGroupKeys; + this.keyGroupPattern = keyGroupPattern; + this.isInitializing = isInitializing; + } + + /** + * Get the ID of the client making the request. + * + * @return The client ID + */ + public String getClientId() { + return clientId; + } + + /** + * Set the ID of the client making the request. + * + * @param clientId The client ID to be set + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Get the pattern for matching group keys. + * + * @return The key group pattern + */ + public String getKeyGroupPattern() { + return keyGroupPattern; + } + + /** + * Set the pattern for matching group keys. + * + * @param keyGroupPattern The key group pattern to be set + */ + public void setKeyGroupPattern(String keyGroupPattern) { + this.keyGroupPattern = keyGroupPattern; + } + + /** + * Get the set of existing group keys associated with the client. + * + * @return The set of existing group keys + */ + public Set getClientExistingGroupKeys() { + return clientExistingGroupKeys; + } + + /** + * Set the set of existing group keys associated with the client. + * + * @param clientExistingGroupKeys The set of existing group keys to be set + */ + public void setClientExistingGroupKeys(Set clientExistingGroupKeys) { + this.clientExistingGroupKeys = clientExistingGroupKeys; + } + + /** + * Check whether the client is initializing. + * + * @return True if the client is initializing, otherwise false + */ + public boolean isInitializing() { + return isInitializing; + } + + /** + * Set the flag indicating whether the client is initializing. + * + * @param initializing True if the client is initializing, otherwise false + */ + public void setInitializing(boolean initializing) { + isInitializing = initializing; + } +} diff --git a/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigBatchFuzzyListenRequestHandler.java b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigBatchFuzzyListenRequestHandler.java new file mode 100644 index 0000000000..0a344ba212 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigBatchFuzzyListenRequestHandler.java @@ -0,0 +1,107 @@ +/* + * Copyright 1999-2023 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.config.server.remote; + +import com.alibaba.nacos.api.config.remote.request.ConfigBatchFuzzyListenRequest; +import com.alibaba.nacos.api.config.remote.response.ConfigBatchFuzzyListenResponse; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.remote.request.RequestMeta; +import com.alibaba.nacos.auth.annotation.Secured; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.GroupKeyPattern; +import com.alibaba.nacos.config.server.model.event.ConfigBatchFuzzyListenEvent; +import com.alibaba.nacos.config.server.utils.GroupKey; +import com.alibaba.nacos.core.control.TpsControl; +import com.alibaba.nacos.core.paramcheck.ExtractorManager; +import com.alibaba.nacos.core.paramcheck.impl.ConfigBatchFuzzyListenRequestParamsExtractor; +import com.alibaba.nacos.core.remote.RequestHandler; +import com.alibaba.nacos.core.utils.StringPool; +import com.alibaba.nacos.plugin.auth.constant.ActionTypes; +import com.alibaba.nacos.plugin.auth.constant.SignType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Handler for processing batch fuzzy listen requests. + *

+ * This handler is responsible for processing batch fuzzy listen requests sent by clients. It adds or removes clients + * from the fuzzy listening context based on the request, and publishes corresponding events to notify interested + * parties. + *

+ * + * @author stone-98 + * @date 2024/3/4 + */ +@Component +public class ConfigBatchFuzzyListenRequestHandler + extends RequestHandler { + + /** + * Context for managing fuzzy listen changes. + */ + @Autowired + private ConfigChangeListenContext configChangeListenContext; + + /** + * Handles the batch fuzzy listen request. + *

+ * This method processes the batch fuzzy listen request by adding or removing clients from the fuzzy listening + * context based on the request, and publishes corresponding events to notify interested parties. + *

+ * + * @param request The batch fuzzy listen request + * @param meta Request meta information + * @return The response to the batch fuzzy listen request + * @throws NacosException If an error occurs while processing the request + */ + @Override + @TpsControl(pointName = "ConfigFuzzyListen") + @Secured(action = ActionTypes.READ, signType = SignType.CONFIG) + @ExtractorManager.Extractor(rpcExtractor = ConfigBatchFuzzyListenRequestParamsExtractor.class) + public ConfigBatchFuzzyListenResponse handle(ConfigBatchFuzzyListenRequest request, RequestMeta meta) + throws NacosException { + String connectionId = StringPool.get(meta.getConnectionId()); + for (ConfigBatchFuzzyListenRequest.Context context : request.getContexts()) { + String groupKeyPattern = GroupKeyPattern.generateFuzzyListenGroupKeyPattern(context.getDataIdPattern(), + context.getGroup(), context.getTenant()); + groupKeyPattern = StringPool.get(groupKeyPattern); + if (context.isListen()) { + // Add client to the fuzzy listening context + configChangeListenContext.addFuzzyListen(groupKeyPattern, connectionId); + // Get existing group keys for the client and publish initialization event + Set clientExistingGroupKeys = null; + if (CollectionUtils.isNotEmpty(context.getDataIds())) { + clientExistingGroupKeys = context.getDataIds().stream() + .map(dataId -> GroupKey.getKeyTenant(dataId, context.getGroup(), context.getTenant())) + .collect(Collectors.toSet()); + } + NotifyCenter.publishEvent( + new ConfigBatchFuzzyListenEvent(connectionId, clientExistingGroupKeys, groupKeyPattern, + context.isInitializing())); + } else { + // Remove client from the fuzzy listening context + configChangeListenContext.removeFuzzyListen(groupKeyPattern, connectionId); + } + } + // Return response + return new ConfigBatchFuzzyListenResponse(); + } +} diff --git a/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigChangeListenContext.java b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigChangeListenContext.java index adf7453558..c71ef288f7 100644 --- a/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigChangeListenContext.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigChangeListenContext.java @@ -17,6 +17,7 @@ package com.alibaba.nacos.config.server.remote; import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.GroupKeyPattern; import org.springframework.stereotype.Component; import java.util.Collection; @@ -36,18 +37,100 @@ @Component public class ConfigChangeListenContext { + /** + * groupKeyPattern -> connection set. + */ + private final Map> keyPatternContext = new ConcurrentHashMap<>(); + /** * groupKey-> connection set. */ - private ConcurrentHashMap> groupKeyContext = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> groupKeyContext = new ConcurrentHashMap<>(); /** * connectionId-> group key set. */ - private ConcurrentHashMap> connectionIdContext = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> connectionIdContext = new ConcurrentHashMap<>(); /** - * add listen. + * Adds a fuzzy listen connection ID associated with the specified group key pattern. If the key pattern does not + * exist in the context, a new entry will be created. If the key pattern already exists, the connection ID will be + * added to the existing set. + * + * @param groupKeyPattern The group key pattern to associate with the listen connection. + * @param connectId The connection ID to be added. + */ + public synchronized void addFuzzyListen(String groupKeyPattern, String connectId) { + // Add the connection ID to the set associated with the key pattern in keyPatternContext + keyPatternContext.computeIfAbsent(groupKeyPattern, k -> new HashSet<>()).add(connectId); + } + + /** + * Removes a fuzzy listen connection ID associated with the specified group key pattern. If the group key pattern + * exists in the context and the connection ID is found in the associated set, the connection ID will be removed + * from the set. If the set becomes empty after removal, the entry for the group key pattern will be removed from + * the context. + * + * @param groupKeyPattern The group key pattern associated with the listen connection to be removed. + * @param connectionId The connection ID to be removed. + */ + public synchronized void removeFuzzyListen(String groupKeyPattern, String connectionId) { + // Retrieve the set of connection IDs associated with the group key pattern + Set connectIds = keyPatternContext.get(groupKeyPattern); + if (CollectionUtils.isNotEmpty(connectIds)) { + // Remove the connection ID from the set if it exists + connectIds.remove(connectionId); + // Remove the entry for the group key pattern if the set becomes empty after removal + if (connectIds.isEmpty()) { + keyPatternContext.remove(groupKeyPattern); + } + } + } + + /** + * Retrieves the set of fuzzy listen connection IDs associated with the specified group key pattern. + * + * @param groupKeyPattern The group key pattern to retrieve the associated connection IDs. + * @return The set of connection IDs associated with the group key pattern, or null if no connections are found. + */ + public synchronized Set getFuzzyListeners(String groupKeyPattern) { + // Retrieve the set of connection IDs associated with the group key pattern + Set connectionIds = keyPatternContext.get(groupKeyPattern); + // If the set is not empty, create a new set and safely copy the connection IDs into it + if (CollectionUtils.isNotEmpty(connectionIds)) { + Set listenConnections = new HashSet<>(); + safeCopy(connectionIds, listenConnections); + return listenConnections; + } + // Return null if no connections are found for the specified group key pattern + return null; + } + + /** + * Retrieves the set of connection IDs matched with the specified group key. + * + * @param groupKey The group key to match with the key patterns. + * @return The set of connection IDs matched with the group key. + */ + public Set getConnectIdMatchedPatterns(String groupKey) { + // Initialize a set to store the matched connection IDs + Set connectIds = new HashSet<>(); + // Iterate over each key pattern in the context + for (String keyPattern : keyPatternContext.keySet()) { + // Check if the group key matches the current key pattern + if (GroupKeyPattern.isMatchPatternWithNamespace(groupKey, keyPattern)) { + // If matched, add the associated connection IDs to the set + Set connectIdSet = keyPatternContext.get(keyPattern); + if (CollectionUtils.isNotEmpty(connectIdSet)) { + connectIds.addAll(connectIdSet); + } + } + } + return connectIds; + } + + /** + * Add listen. * * @param groupKey groupKey. * @param connectionId connectionId. @@ -127,7 +210,7 @@ public synchronized void clearContextForConnectionId(final String connectionId) return; } for (Map.Entry groupKey : listenKeys.entrySet()) { - + Set connectionIds = groupKeyContext.get(groupKey.getKey()); if (CollectionUtils.isNotEmpty(connectionIds)) { connectionIds.remove(connectionId); @@ -137,9 +220,23 @@ public synchronized void clearContextForConnectionId(final String connectionId) } else { groupKeyContext.remove(groupKey.getKey()); } - + } connectionIdContext.remove(connectionId); + + // Remove any remaining fuzzy listen connections + for (Map.Entry> keyPatternContextEntry : keyPatternContext.entrySet()) { + String keyPattern = keyPatternContextEntry.getKey(); + Set connectionIds = keyPatternContextEntry.getValue(); + if (CollectionUtils.isEmpty(connectionIds)) { + keyPatternContext.remove(keyPattern); + } else { + connectionIds.remove(keyPattern); + if (CollectionUtils.isEmpty(connectionIds)) { + keyPatternContext.remove(keyPattern); + } + } + } } /** diff --git a/config/src/main/java/com/alibaba/nacos/config/server/remote/RpcFuzzyListenConfigChangeNotifier.java b/config/src/main/java/com/alibaba/nacos/config/server/remote/RpcFuzzyListenConfigChangeNotifier.java new file mode 100644 index 0000000000..3724a0eaf6 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/remote/RpcFuzzyListenConfigChangeNotifier.java @@ -0,0 +1,216 @@ +/* + * Copyright 1999-2023 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.config.server.remote; + +import com.alibaba.nacos.api.config.remote.request.FuzzyListenNotifyChangeRequest; +import com.alibaba.nacos.api.remote.AbstractPushCallBack; +import com.alibaba.nacos.common.notify.Event; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.notify.listener.Subscriber; +import com.alibaba.nacos.config.server.configuration.ConfigCommonConfig; +import com.alibaba.nacos.config.server.model.event.LocalDataChangeEvent; +import com.alibaba.nacos.config.server.service.ConfigCacheService; +import com.alibaba.nacos.config.server.utils.ConfigExecutor; +import com.alibaba.nacos.config.server.utils.GroupKey; +import com.alibaba.nacos.core.remote.Connection; +import com.alibaba.nacos.core.remote.ConnectionManager; +import com.alibaba.nacos.core.remote.ConnectionMeta; +import com.alibaba.nacos.core.remote.RpcPushService; +import com.alibaba.nacos.core.utils.Loggers; +import com.alibaba.nacos.plugin.control.ControlManagerCenter; +import com.alibaba.nacos.plugin.control.tps.TpsControlManager; +import com.alibaba.nacos.plugin.control.tps.request.TpsCheckRequest; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * Notify remote clients about fuzzy listen configuration changes. Use subscriber mode to monitor local data changes, + * and push notifications to remote clients accordingly. + * + * @author stone-98 + * @date 2024/3/18 + */ +@Component(value = "rpcFuzzyListenConfigChangeNotifier") +public class RpcFuzzyListenConfigChangeNotifier extends Subscriber { + + private static final String POINT_FUZZY_LISTEN_CONFIG_PUSH = "POINT_FUZZY_LISTEN_CONFIG_PUSH"; + + private static final String POINT_FUZZY_LISTEN_CONFIG_PUSH_SUCCESS = "POINT_FUZZY_LISTEN_CONFIG_PUSH_SUCCESS"; + + private static final String POINT_FUZZY_LISTEN_CONFIG_PUSH_FAIL = "POINT_FUZZY_LISTEN_CONFIG_PUSH_FAIL"; + + private final ConfigChangeListenContext configChangeListenContext; + + private final ConnectionManager connectionManager; + + private final RpcPushService rpcPushService; + + private final TpsControlManager tpsControlManager; + + /** + * Constructs RpcFuzzyListenConfigChangeNotifier with the specified dependencies. + * + * @param configChangeListenContext The context for config change listening. + * @param connectionManager The manager for connections. + * @param rpcPushService The service for RPC push. + */ + public RpcFuzzyListenConfigChangeNotifier(ConfigChangeListenContext configChangeListenContext, + ConnectionManager connectionManager, RpcPushService rpcPushService) { + this.configChangeListenContext = configChangeListenContext; + this.connectionManager = connectionManager; + this.rpcPushService = rpcPushService; + this.tpsControlManager = ControlManagerCenter.getInstance().getTpsControlManager(); + NotifyCenter.registerSubscriber(this); + } + + @Override + public void onEvent(LocalDataChangeEvent event) { + String groupKey = event.groupKey; + String[] parseKey = GroupKey.parseKey(groupKey); + String dataId = parseKey[0]; + String group = parseKey[1]; + String tenant = parseKey.length > 2 ? parseKey[2] : ""; + + for (String clientId : configChangeListenContext.getConnectIdMatchedPatterns(groupKey)) { + Connection connection = connectionManager.getConnection(clientId); + if (null == connection) { + Loggers.REMOTE_PUSH.warn( + "clientId not found, Config change notification not sent. clientId={},keyGroupPattern={}", + clientId, event.groupKey); + continue; + } + ConnectionMeta metaInfo = connection.getMetaInfo(); + String clientIp = metaInfo.getClientIp(); + String clientTag = metaInfo.getTag(); + String appName = metaInfo.getAppName(); + boolean exists = ConfigCacheService.containsAndEffectiveForClient(groupKey, clientIp, clientTag); + FuzzyListenNotifyChangeRequest request = new FuzzyListenNotifyChangeRequest(tenant, group, dataId, exists); + int maxPushRetryTimes = ConfigCommonConfig.getInstance().getMaxPushRetryTimes(); + RpcPushTask rpcPushTask = new RpcPushTask(request, maxPushRetryTimes, clientId, clientIp, appName); + push(rpcPushTask); + } + } + + @Override + public Class subscribeType() { + return LocalDataChangeEvent.class; + } + + /** + * Pushes the notification to remote clients. + * + * @param retryTask The task for retrying to push notification. + */ + private void push(RpcPushTask retryTask) { + FuzzyListenNotifyChangeRequest notifyRequest = retryTask.notifyRequest; + if (retryTask.isOverTimes()) { + Loggers.REMOTE_PUSH.warn( + "push callback retry fail over times. dataId={},group={},tenant={},clientId={}, will unregister client.", + notifyRequest.getDataId(), notifyRequest.getGroup(), notifyRequest.getTenant(), + retryTask.connectionId); + connectionManager.unregister(retryTask.connectionId); + } else if (connectionManager.getConnection(retryTask.connectionId) != null) { + // First time: delay 0s; Second time: delay 2s; Third time: delay 4s + ConfigExecutor.getClientConfigNotifierServiceExecutor() + .schedule(retryTask, retryTask.tryTimes * 2L, TimeUnit.SECONDS); + } else { + // Client is already offline, ignore the task. + Loggers.REMOTE_PUSH.warn( + "Client is already offline, ignore the task. dataId={},group={},tenant={},clientId={}", + notifyRequest.getDataId(), notifyRequest.getGroup(), notifyRequest.getTenant(), + retryTask.connectionId); + } + } + + /** + * Represents a task for pushing notification to remote clients. + */ + class RpcPushTask implements Runnable { + + FuzzyListenNotifyChangeRequest notifyRequest; + + int maxRetryTimes; + + int tryTimes = 0; + + String connectionId; + + String clientIp; + + String appName; + + /** + * Constructs a RpcPushTask with the specified parameters. + * + * @param notifyRequest The notification request to be sent. + * @param maxRetryTimes The maximum number of retry times. + * @param connectionId The ID of the connection. + * @param clientIp The IP address of the client. + * @param appName The name of the application. + */ + public RpcPushTask(FuzzyListenNotifyChangeRequest notifyRequest, int maxRetryTimes, String connectionId, + String clientIp, String appName) { + this.notifyRequest = notifyRequest; + this.maxRetryTimes = maxRetryTimes; + this.connectionId = connectionId; + this.clientIp = clientIp; + this.appName = appName; + } + + /** + * Checks if the number of retry times exceeds the maximum limit. + * + * @return {@code true} if the number of retry times exceeds the maximum limit; otherwise, {@code false}. + */ + public boolean isOverTimes() { + return maxRetryTimes > 0 && this.tryTimes >= maxRetryTimes; + } + + @Override + public void run() { + tryTimes++; + TpsCheckRequest tpsCheckRequest = new TpsCheckRequest(); + tpsCheckRequest.setPointName(POINT_FUZZY_LISTEN_CONFIG_PUSH); + if (!tpsControlManager.check(tpsCheckRequest).isSuccess()) { + push(this); + } else { + long timeout = ConfigCommonConfig.getInstance().getPushTimeout(); + rpcPushService.pushWithCallback(connectionId, notifyRequest, new AbstractPushCallBack(timeout) { + @Override + public void onSuccess() { + TpsCheckRequest tpsCheckRequest = new TpsCheckRequest(); + tpsCheckRequest.setPointName(POINT_FUZZY_LISTEN_CONFIG_PUSH_SUCCESS); + tpsControlManager.check(tpsCheckRequest); + } + + @Override + public void onFail(Throwable e) { + TpsCheckRequest tpsCheckRequest = new TpsCheckRequest(); + tpsCheckRequest.setPointName(POINT_FUZZY_LISTEN_CONFIG_PUSH_FAIL); + tpsControlManager.check(tpsCheckRequest); + Loggers.REMOTE_PUSH.warn("Push fail, dataId={}, group={}, tenant={}, clientId={}", + notifyRequest.getDataId(), notifyRequest.getGroup(), notifyRequest.getTenant(), + connectionId, e); + push(RpcPushTask.this); + } + + }, ConfigExecutor.getClientConfigNotifierServiceExecutor()); + } + } + } +} diff --git a/config/src/main/java/com/alibaba/nacos/config/server/remote/RpcFuzzyListenConfigDiffNotifier.java b/config/src/main/java/com/alibaba/nacos/config/server/remote/RpcFuzzyListenConfigDiffNotifier.java new file mode 100644 index 0000000000..67e7a46de4 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/remote/RpcFuzzyListenConfigDiffNotifier.java @@ -0,0 +1,488 @@ +/* + * Copyright 1999-2023 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.config.server.remote; + +import com.alibaba.nacos.api.common.Constants; +import com.alibaba.nacos.api.config.remote.request.FuzzyListenNotifyDiffRequest; +import com.alibaba.nacos.api.remote.AbstractPushCallBack; +import com.alibaba.nacos.common.notify.Event; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.notify.listener.Subscriber; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.GroupKeyPattern; +import com.alibaba.nacos.config.server.configuration.ConfigCommonConfig; +import com.alibaba.nacos.config.server.model.event.ConfigBatchFuzzyListenEvent; +import com.alibaba.nacos.config.server.service.ConfigCacheService; +import com.alibaba.nacos.config.server.utils.ConfigExecutor; +import com.alibaba.nacos.config.server.utils.GroupKey; +import com.alibaba.nacos.core.remote.Connection; +import com.alibaba.nacos.core.remote.ConnectionManager; +import com.alibaba.nacos.core.remote.ConnectionMeta; +import com.alibaba.nacos.core.remote.RpcPushService; +import com.alibaba.nacos.core.utils.Loggers; +import com.alibaba.nacos.plugin.control.ControlManagerCenter; +import com.alibaba.nacos.plugin.control.tps.TpsControlManager; +import com.alibaba.nacos.plugin.control.tps.request.TpsCheckRequest; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Handles batch fuzzy listen events and pushes corresponding notifications to clients. + * + * @author stone-98 + * @date 2024/3/18 + */ +@Component(value = "rpcFuzzyListenConfigDiffNotifier") +public class RpcFuzzyListenConfigDiffNotifier extends Subscriber { + + private static final String FUZZY_LISTEN_CONFIG_DIFF_PUSH = "FUZZY_LISTEN_CONFIG_DIFF_PUSH_COUNT"; + + private static final String FUZZY_LISTEN_CONFIG_DIFF_PUSH_SUCCESS = "FUZZY_LISTEN_CONFIG_DIFF_PUSH_SUCCESS"; + + private static final String FUZZY_LISTEN_CONFIG_DIFF_PUSH_FAIL = "FUZZY_LISTEN_CONFIG_DIFF_PUSH_FAIL"; + + private final ConnectionManager connectionManager; + + private final TpsControlManager tpsControlManager; + + private final RpcPushService rpcPushService; + + public RpcFuzzyListenConfigDiffNotifier(ConnectionManager connectionManager, RpcPushService rpcPushService) { + this.connectionManager = connectionManager; + this.tpsControlManager = ControlManagerCenter.getInstance().getTpsControlManager(); + this.rpcPushService = rpcPushService; + NotifyCenter.registerSubscriber(this); + } + + /** + * Pushes the retry task to the client connection manager for retrying the RPC push operation. + * + * @param retryTask The retry task containing the RPC push request + * @param connectionManager The connection manager for managing client connections + */ + private static void push(RpcPushTask retryTask, ConnectionManager connectionManager) { + FuzzyListenNotifyDiffRequest notifyRequest = retryTask.notifyRequest; + // Check if the maximum retry times have been reached + if (retryTask.isOverTimes()) { + // If over the maximum retry times, log a warning and unregister the client connection + Loggers.REMOTE_PUSH.warn( + "Push callback retry failed over times. groupKeyPattern={}, clientId={}, will unregister client.", + notifyRequest.getGroupKeyPattern(), retryTask.connectionId); + connectionManager.unregister(retryTask.connectionId); + } else if (connectionManager.getConnection(retryTask.connectionId) != null) { + // Schedule a retry task with an increasing delay based on the number of retries + // First time: delay 0s; second time: delay 2s; third time: delay 4s, and so on + ConfigExecutor.scheduleClientConfigNotifier(retryTask, retryTask.tryTimes * 2L, TimeUnit.SECONDS); + } else { + // If the client is already offline, ignore the task + Loggers.REMOTE_PUSH.warn("Client is already offline, ignore the task. groupKeyPattern={}, clientId={}", + notifyRequest.getGroupKeyPattern(), retryTask.connectionId); + } + } + + /** + * Handles the ConfigBatchFuzzyListenEvent. This method is responsible for processing batch fuzzy listen events and + * pushing corresponding notifications to clients. + * + * @param event The ConfigBatchFuzzyListenEvent to handle + */ + @Override + public void onEvent(ConfigBatchFuzzyListenEvent event) { + // Get the connection for the client + Connection connection = connectionManager.getConnection(event.getClientId()); + if (connection == null) { + Loggers.REMOTE_PUSH.warn( + "clientId not found, Config diff notification not sent. clientId={},keyGroupPattern={}", + event.getClientId(), event.getKeyGroupPattern()); + // If connection is not available, return + return; + } + + // Retrieve meta information for the connection + ConnectionMeta metaInfo = connection.getMetaInfo(); + String clientIp = metaInfo.getClientIp(); + String clientTag = metaInfo.getTag(); + + // Match client effective group keys based on the event pattern, client IP, and tag + Set matchGroupKeys = ConfigCacheService.matchClientEffectiveGroupKeys(event.getKeyGroupPattern(), + clientIp, clientTag); + + // Retrieve existing group keys for the client from the event + Set clientExistingGroupKeys = event.getClientExistingGroupKeys(); + + // Check if both matched and existing group keys are empty, if so, return + if (CollectionUtils.isEmpty(matchGroupKeys) && CollectionUtils.isEmpty(clientExistingGroupKeys)) { + return; + } + + // Calculate and merge configuration states based on matched and existing group keys + List configStates = calculateAndMergeToConfigState(matchGroupKeys, clientExistingGroupKeys); + + // If no config states are available, return + if (CollectionUtils.isEmpty(configStates)) { + return; + } + + int batchSize = ConfigCommonConfig.getInstance().getBatchSize(); + // Divide config states into batches + List> divideConfigStatesIntoBatches = divideConfigStatesIntoBatches(configStates, batchSize); + + // Calculate the number of batches and initialize push batch finish count + int originBatchSize = divideConfigStatesIntoBatches.size(); + AtomicInteger pushBatchFinishCount = new AtomicInteger(0); + + // Iterate over each batch of config states + for (List configStateList : divideConfigStatesIntoBatches) { + // Map config states to FuzzyListenNotifyDiffRequest.Context objects + Set contexts = configStateList.stream().map(state -> { + String[] parseKey = GroupKey.parseKey(state.getGroupKey()); + String dataId = parseKey[0]; + String group = parseKey[1]; + String tenant = parseKey.length > 2 ? parseKey[2] : Constants.DEFAULT_NAMESPACE_ID; + String changeType = event.isInitializing() ? Constants.ConfigChangeType.LISTEN_INIT + : (state.isExist() ? Constants.ConfigChangeType.ADD_CONFIG + : Constants.ConfigChangeType.DELETE_CONFIG); + return FuzzyListenNotifyDiffRequest.Context.build(tenant, group, dataId, changeType); + }).collect(Collectors.toSet()); + + // Remove namespace from the pattern + String patternWithoutNameSpace = GroupKeyPattern.getPatternRemovedNamespace(event.getKeyGroupPattern()); + + // Build FuzzyListenNotifyDiffRequest with contexts and pattern + FuzzyListenNotifyDiffRequest request = FuzzyListenNotifyDiffRequest.buildInitRequest(contexts, + patternWithoutNameSpace); + + int maxPushRetryTimes = ConfigCommonConfig.getInstance().getMaxPushRetryTimes(); + // Create RPC push task and push the request to the client + RpcPushTask rpcPushTask = new RpcPushTask(request, pushBatchFinishCount, originBatchSize, maxPushRetryTimes, + event.getClientId(), clientIp, metaInfo.getAppName()); + push(rpcPushTask, connectionManager); + } + } + + /** + * Calculates and merges the differences between the matched group keys and the client's existing group keys into a + * list of ConfigState objects. + * + * @param matchGroupKeys The matched group keys set + * @param clientExistingGroupKeys The client's existing group keys set + * @return The merged list of ConfigState objects representing the states to be added or removed + */ + private List calculateAndMergeToConfigState(Set matchGroupKeys, + Set clientExistingGroupKeys) { + // Calculate the set of group keys to be added and removed + Set addGroupKeys = new HashSet<>(); + if (CollectionUtils.isNotEmpty(matchGroupKeys)) { + addGroupKeys.addAll(matchGroupKeys); + } + if (CollectionUtils.isNotEmpty(clientExistingGroupKeys)) { + addGroupKeys.removeAll(clientExistingGroupKeys); + } + + Set removeGroupKeys = new HashSet<>(); + if (CollectionUtils.isNotEmpty(clientExistingGroupKeys)) { + removeGroupKeys.addAll(clientExistingGroupKeys); + } + if (CollectionUtils.isNotEmpty(matchGroupKeys)) { + removeGroupKeys.removeAll(matchGroupKeys); + } + + // Convert the group keys to be added and removed into corresponding ConfigState objects and merge them into a list + return Stream.concat(addGroupKeys.stream().map(groupKey -> new ConfigState(groupKey, true)), + removeGroupKeys.stream().map(groupKey -> new ConfigState(groupKey, false))) + .collect(Collectors.toList()); + } + + @Override + public Class subscribeType() { + return ConfigBatchFuzzyListenEvent.class; + } + + /** + * Divides a collection of items into batches. + * + * @param configStates The collection of items to be divided into batches + * @param batchSize The size of each batch + * @param The type of items in the collection + * @return A list of batches, each containing a sublist of items + */ + private List> divideConfigStatesIntoBatches(Collection configStates, int batchSize) { + // Initialize an index to track the current batch number + AtomicInteger index = new AtomicInteger(); + + // Group the elements into batches based on their index divided by the batch size + return new ArrayList<>( + configStates.stream().collect(Collectors.groupingBy(e -> index.getAndIncrement() / batchSize)) + .values()); + } + + /** + * ConfigState. + */ + public static class ConfigState { + + /** + * The group key associated with the configuration. + */ + private String groupKey; + + /** + * Indicates whether the configuration exists or not. + */ + private boolean exist; + + /** + * Constructs a new ConfigState instance with the given group key and existence flag. + * + * @param groupKey The group key associated with the configuration. + * @param exist {@code true} if the configuration exists, {@code false} otherwise. + */ + public ConfigState(String groupKey, boolean exist) { + this.groupKey = groupKey; + this.exist = exist; + } + + /** + * Retrieves the group key associated with the configuration. + * + * @return The group key. + */ + public String getGroupKey() { + return groupKey; + } + + /** + * Sets the group key associated with the configuration. + * + * @param groupKey The group key to set. + */ + public void setGroupKey(String groupKey) { + this.groupKey = groupKey; + } + + /** + * Checks whether the configuration exists or not. + * + * @return {@code true} if the configuration exists, {@code false} otherwise. + */ + public boolean isExist() { + return exist; + } + + /** + * Sets the existence flag of the configuration. + * + * @param exist {@code true} if the configuration exists, {@code false} otherwise. + */ + public void setExist(boolean exist) { + this.exist = exist; + } + } + + /** + * Represents a task for pushing FuzzyListenNotifyDiffRequest to clients. + */ + class RpcPushTask implements Runnable { + + /** + * The FuzzyListenNotifyDiffRequest to be pushed. + */ + FuzzyListenNotifyDiffRequest notifyRequest; + + /** + * The maximum number of times to retry pushing the request. + */ + int maxRetryTimes; + + /** + * The current number of attempts made to push the request. + */ + int tryTimes = 0; + + /** + * The ID of the connection associated with the client. + */ + String connectionId; + + /** + * The IP address of the client. + */ + String clientIp; + + /** + * The name of the client's application. + */ + String appName; + + /** + * The counter for tracking the number of finished push batches. + */ + AtomicInteger pushBatchFinishCount; + + /** + * The original size of the batch before splitting. + */ + int originBatchSize; + + /** + * Constructs a new RpcPushTask with the specified parameters. + * + * @param notifyRequest The FuzzyListenNotifyDiffRequest to be pushed + * @param pushBatchFinishCount The counter for tracking the number of finished push batches + * @param originBatchSize The original size of the batch before splitting + * @param maxRetryTimes The maximum number of times to retry pushing the request + * @param connectionId The ID of the connection associated with the client + * @param clientIp The IP address of the client + * @param appName The name of the client's application + */ + public RpcPushTask(FuzzyListenNotifyDiffRequest notifyRequest, AtomicInteger pushBatchFinishCount, + int originBatchSize, int maxRetryTimes, String connectionId, String clientIp, String appName) { + this.notifyRequest = notifyRequest; + this.pushBatchFinishCount = pushBatchFinishCount; + this.originBatchSize = originBatchSize; + this.maxRetryTimes = maxRetryTimes; + this.connectionId = connectionId; + this.clientIp = clientIp; + this.appName = appName; + } + + /** + * Checks if the maximum number of retry times has been reached. + * + * @return true if the maximum number of retry times has been reached, otherwise false + */ + public boolean isOverTimes() { + return maxRetryTimes > 0 && this.tryTimes >= maxRetryTimes; + } + + /** + * Executes the task, attempting to push the request to the client. + */ + @Override + public void run() { + tryTimes++; + TpsCheckRequest tpsCheckRequest = new TpsCheckRequest(); + + tpsCheckRequest.setPointName(FUZZY_LISTEN_CONFIG_DIFF_PUSH); + if (!tpsControlManager.check(tpsCheckRequest).isSuccess()) { + push(this, connectionManager); + } else { + rpcPushService.pushWithCallback(connectionId, notifyRequest, + new RpcPushCallback(this, tpsControlManager, connectionManager, pushBatchFinishCount, + originBatchSize), ConfigExecutor.getClientConfigNotifierServiceExecutor()); + } + } + } + + /** + * Represents a callback for handling the result of an RPC push operation. + */ + class RpcPushCallback extends AbstractPushCallBack { + + /** + * The RpcPushTask associated with the callback. + */ + RpcPushTask rpcPushTask; + + /** + * The TpsControlManager for checking TPS limits. + */ + TpsControlManager tpsControlManager; + + /** + * The ConnectionManager for managing client connections. + */ + ConnectionManager connectionManager; + + /** + * The counter for tracking the number of pushed batches. + */ + AtomicInteger pushBatchCount; + + /** + * The original size of the batch before splitting. + */ + int originBatchSize; + + /** + * Constructs a new RpcPushCallback with the specified parameters. + * + * @param rpcPushTask The RpcPushTask associated with the callback + * @param tpsControlManager The TpsControlManager for checking TPS limits + * @param connectionManager The ConnectionManager for managing client connections + * @param pushBatchCount The counter for tracking the number of pushed batches + * @param originBatchSize The original size of the batch before splitting + */ + public RpcPushCallback(RpcPushTask rpcPushTask, TpsControlManager tpsControlManager, + ConnectionManager connectionManager, AtomicInteger pushBatchCount, int originBatchSize) { + // Set the timeout for the callback + super(3000L); + this.rpcPushTask = rpcPushTask; + this.tpsControlManager = tpsControlManager; + this.connectionManager = connectionManager; + this.pushBatchCount = pushBatchCount; + this.originBatchSize = originBatchSize; + } + + /** + * Handles the successful completion of the RPC push operation. + */ + @Override + public void onSuccess() { + // Check TPS limits + TpsCheckRequest tpsCheckRequest = new TpsCheckRequest(); + tpsCheckRequest.setPointName(FUZZY_LISTEN_CONFIG_DIFF_PUSH_SUCCESS); + tpsControlManager.check(tpsCheckRequest); + + if (pushBatchCount.get() < originBatchSize) { + pushBatchCount.incrementAndGet(); + } else if (pushBatchCount.get() == originBatchSize) { + FuzzyListenNotifyDiffRequest request = FuzzyListenNotifyDiffRequest.buildInitFinishRequest( + rpcPushTask.notifyRequest.getGroupKeyPattern()); + push(new RpcPushTask(request, pushBatchCount, originBatchSize, 50, rpcPushTask.connectionId, + rpcPushTask.clientIp, rpcPushTask.appName), connectionManager); + } + } + + /** + * Handles the failure of the RPC push operation. + * + * @param e The exception thrown during the operation + */ + @Override + public void onFail(Throwable e) { + // Check TPS limits + TpsCheckRequest tpsCheckRequest = new TpsCheckRequest(); + tpsCheckRequest.setPointName(FUZZY_LISTEN_CONFIG_DIFF_PUSH_FAIL); + tpsControlManager.check(tpsCheckRequest); + + // Log the failure and retry the task + Loggers.REMOTE_PUSH.warn("Push fail, groupKeyPattern={}, clientId={}", + rpcPushTask.notifyRequest.getGroupKeyPattern(), rpcPushTask.connectionId, e); + push(rpcPushTask, connectionManager); + } + } +} diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/ConfigCacheService.java b/config/src/main/java/com/alibaba/nacos/config/server/service/ConfigCacheService.java index da9ed9ef5f..e6e0fe3f06 100644 --- a/config/src/main/java/com/alibaba/nacos/config/server/service/ConfigCacheService.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/ConfigCacheService.java @@ -17,6 +17,9 @@ package com.alibaba.nacos.config.server.service; import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.GroupKeyPattern; +import com.alibaba.nacos.common.utils.InternetAddressUtil; import com.alibaba.nacos.common.utils.MD5Utils; import com.alibaba.nacos.common.utils.StringUtils; import com.alibaba.nacos.config.server.model.CacheItem; @@ -31,9 +34,14 @@ import com.alibaba.nacos.sys.env.EnvUtil; import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import static com.alibaba.nacos.api.common.Constants.CLIENT_IP; import static com.alibaba.nacos.api.common.Constants.VIPSERVER_TAG; @@ -68,6 +76,37 @@ public static int groupCount() { return CACHE.size(); } + /** + * Matches the client effective group keys based on the specified group key pattern, client IP, and tag. + * + * @param groupKeyPattern The pattern to match group keys. + * @param clientIp The IP address of the client. + * @param tag The tag associated with the configuration. + * @return A set of group keys that match the pattern and are effective for the client. + */ + public static Set matchClientEffectiveGroupKeys(String groupKeyPattern, String clientIp, String tag) { + return CACHE.entrySet().stream() + .filter(entry -> GroupKeyPattern.isMatchPatternWithNamespace(entry.getKey(), groupKeyPattern)) + .filter(entry -> entry.getValue().effectiveForClient(tag, clientIp)).map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + /** + * Checks if the specified group key is present in the cache and effective for the client. + * + * @param groupKey The group key to check. + * @param clientIp The IP address of the client. + * @param tag The tag associated with the configuration. + * @return true if the group key is present in the cache and effective for the client, false otherwise. + */ + public static boolean containsAndEffectiveForClient(String groupKey, String clientIp, String tag) { + if (!CACHE.containsKey(groupKey)) { + return false; + } + CacheItem cacheItem = CACHE.get(groupKey); + return cacheItem.effectiveForClient(tag, clientIp); + } + /** * Save config file and update md5 value in cache. * @@ -200,8 +239,6 @@ public static boolean dumpGray(String dataId, String group, String tenant, Strin //check timestamp long localGrayLastModifiedTs = ConfigCacheService.getGrayLastModifiedTs(groupKey, grayName); - - boolean timestampOutdated = lastModifiedTs < localGrayLastModifiedTs; if (timestampOutdated) { DUMP_LOG.warn("[dump-gray-ignore] timestamp is outdated,groupKey={}", groupKey); return true; diff --git a/core/src/main/java/com/alibaba/nacos/core/paramcheck/impl/ConfigBatchFuzzyListenRequestParamsExtractor.java b/core/src/main/java/com/alibaba/nacos/core/paramcheck/impl/ConfigBatchFuzzyListenRequestParamsExtractor.java new file mode 100644 index 0000000000..73c3c01b83 --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/paramcheck/impl/ConfigBatchFuzzyListenRequestParamsExtractor.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-2023 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.core.paramcheck.impl; + +import com.alibaba.nacos.api.config.remote.request.ConfigBatchFuzzyListenRequest; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.remote.request.Request; +import com.alibaba.nacos.common.paramcheck.ParamInfo; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.core.paramcheck.AbstractRpcParamExtractor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Extractor for parameters of {@link ConfigBatchFuzzyListenRequest}. This extractor retrieves parameter information + * from the request object and constructs {@link ParamInfo} instances representing the namespace ID, group, and data IDs + * contained in the request's contexts. + * + * @author stone-98 + * @date 2024/3/5 + */ +public class ConfigBatchFuzzyListenRequestParamsExtractor extends AbstractRpcParamExtractor { + + /** + * Extracts parameter information from the given request. + * + * @param request The request object to extract parameter information from. + * @return A list of {@link ParamInfo} instances representing the extracted parameters. + * @throws NacosException If an error occurs while extracting parameter information. + */ + @Override + public List extractParam(Request request) throws NacosException { + ConfigBatchFuzzyListenRequest req = (ConfigBatchFuzzyListenRequest) request; + Set contexts = req.getContexts(); + List paramInfos = new ArrayList<>(); + if (contexts == null) { + return paramInfos; + } + for (ConfigBatchFuzzyListenRequest.Context context : contexts) { + // Extract namespace ID and group from the context + ParamInfo paramInfo1 = new ParamInfo(); + paramInfo1.setNamespaceId(context.getTenant()); + paramInfo1.setGroup(context.getGroup()); + paramInfos.add(paramInfo1); + + // Extract data IDs from the context if present + if (CollectionUtils.isNotEmpty(context.getDataIds())) { + for (String dataId : context.getDataIds()) { + ParamInfo paramInfo2 = new ParamInfo(); + paramInfo2.setDataId(dataId); + paramInfos.add(paramInfo2); + } + } + } + return paramInfos; + } +} diff --git a/example/src/main/java/com/alibaba/nacos/example/FuzzyListenExample.java b/example/src/main/java/com/alibaba/nacos/example/FuzzyListenExample.java new file mode 100644 index 0000000000..40622c4c4d --- /dev/null +++ b/example/src/main/java/com/alibaba/nacos/example/FuzzyListenExample.java @@ -0,0 +1,98 @@ +/* + * Copyright 1999-2023 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.example; + +import com.alibaba.nacos.api.config.ConfigFactory; +import com.alibaba.nacos.api.config.ConfigService; +import com.alibaba.nacos.api.config.listener.AbstractFuzzyListenListener; +import com.alibaba.nacos.api.config.listener.FuzzyListenConfigChangeEvent; +import com.alibaba.nacos.api.exception.NacosException; + +import java.util.Properties; + +/** + * Nacos config fuzzy listen example. + *

+ * Add the JVM parameter to run the NamingExample: + * {@code -DserverAddr=${nacos.server.ip}:${nacos.server.port} -Dnamespace=${namespaceId}} + *

+ *

+ * This example demonstrates how to use fuzzy listening for Nacos configuration. + *

+ *

+ * Fuzzy listening allows you to monitor configuration changes that match a specified pattern. + *

+ *

+ * In this example, we publish several configurations with names starting with "test", and then add a fuzzy listener to + * listen for changes to configurations with names starting with "test". + *

+ *

+ * After publishing the configurations, the example waits for a brief period, then cancels the fuzzy listening. + *

+ * + * @author stone-98 + * @date 2024/3/14 + */ +public class FuzzyListenExample { + + public static void main(String[] args) throws NacosException, InterruptedException { + // Set up properties for Nacos Config Service + Properties properties = new Properties(); + properties.setProperty("serverAddr", System.getProperty("serverAddr", "localhost")); + properties.setProperty("namespace", System.getProperty("namespace", "public")); + + // Create a Config Service instance + ConfigService configService = ConfigFactory.createConfigService(properties); + + int publicConfigNum = 10; + // Publish some configurations for testing + for (int i = 0; i < publicConfigNum; i++) { + boolean isPublishOk = configService.publishConfig("test" + i, "DEFAULT_GROUP", "content"); + System.out.println("[publish result] " + isPublishOk); + } + + // Define a fuzzy listener to handle configuration changes + AbstractFuzzyListenListener listener = new AbstractFuzzyListenListener() { + @Override + public void onEvent(FuzzyListenConfigChangeEvent event) { + System.out.println("[fuzzy listen config change]" + event.toString()); + } + }; + + // Add the fuzzy listener to monitor configurations starting with "test" + configService.addFuzzyListener("test*", "DEFAULT_GROUP", listener); + System.out.println("[Fuzzy listening started.]"); + + // Publish more configurations to trigger the listener + Thread.sleep(1000); + boolean isPublishOkOne = configService.publishConfig("test-one", "DEFAULT_GROUP", "content"); + System.out.println("[publish result] " + isPublishOkOne); + + boolean isPublishOkTwo = configService.publishConfig("nacos-test-two", "DEFAULT_GROUP", "content"); + System.out.println("[publish result] " + isPublishOkTwo); + + boolean isPublishOkThree = configService.publishConfig("test", "DEFAULT_GROUP", "content"); + System.out.println("[publish result] " + isPublishOkThree); + + // Wait briefly before canceling the fuzzy listening + Thread.sleep(1000); + System.out.println("Cancel fuzzy listen..."); + + // Sleep to keep the program running for observation + Thread.sleep(3000); + } +}