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 c8acd6336a3..0faa1bc469d 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 @@ -276,6 +276,66 @@ public static class Naming { public static final String CMDB_CONTEXT_TYPE = "CMDB"; } + public static final String FUZZY_WATCH_PATTERN_SPLITTER = ">>"; + + /** + * fuzzy watch sync type of watch init notify. + */ + public static final String FUZZY_WATCH_INIT_NOTIFY = "FUZZY_WATCH_INIT_NOTIFY"; + + /** + * fuzzy watch sync type of watch init notify finish. + */ + public static final String FINISH_FUZZY_WATCH_INIT_NOTIFY = "FINISH_FUZZY_WATCH_INIT_NOTIFY"; + + /** + * fuzzy watch sync type of watch diff sync notify. + */ + public static final String FUZZY_WATCH_DIFF_SYNC_NOTIFY = "FUZZY_WATCH_DIFF_SYNC_NOTIFY"; + + /** + * fuzzy watch sync type of watch resource changed. + */ + public static final String FUZZY_WATCH_RESOURCE_CHANGED = "FUZZY_WATCH_RESOURCE_CHANGED"; + + /** + * watch type of watch. + */ + public static final String WATCH_TYPE_WATCH = "WATCH"; + + /** + * watch type of cancel watch. + */ + public static final String WATCH_TYPE_CANCEL_WATCH = "CANCEL_WATCH"; + + /** + * The constants in config fuzzy watch changed type directory. + */ + public static class ConfigChangedType { + + public static final String ADD_CONFIG = "ADD_CONFIG"; + + public static final String DELETE_CONFIG = "DELETE_CONFIG"; + + public static final String CONFIG_CHANGED = "CONFIG_CHANGED"; + + } + + /** + * The constants in naming fuzzy watch changed type directory. + */ + public static class ServiceChangedType { + + public static final String ADD_SERVICE = "ADD_SERVICE"; + + public static final String DELETE_SERVICE = "DELETE_SERVICE"; + + public static final String INSTANCE_CHANGED = "INSTANCE_CHANGED"; + + public static final String HEART_BEAT = "HEART_BEAT"; + + } + /** * The constants in lock directory. */ 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 3adab5aa4a3..146c0bf75ac 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.FuzzyWatchEventWatcher; import com.alibaba.nacos.api.config.listener.Listener; import com.alibaba.nacos.api.exception.NacosException; +import java.util.Set; +import java.util.concurrent.Future; + /** * Config Service Interface. * @@ -160,4 +164,83 @@ boolean publishConfigCas(String dataId, String group, String content, String cas * @throws NacosException exception. */ void shutDown() 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 groupNamePattern The group name pattern representing the group and dataId patterns to subscribe to. + * @param watcher The fuzzy watcher to be added. + * @throws NacosException NacosException + * @since 3.0 + */ + void fuzzyWatch(String groupNamePattern, FuzzyWatchEventWatcher watcher) 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 groupNamePattern The pattern to match group name representing the group and dataId patterns to subscribe to. + * @param watcher The fuzzy listener to be added. + * @throws NacosException NacosException + * @since 3.0 + */ + void fuzzyWatch(String dataIdPattern, String groupNamePattern, FuzzyWatchEventWatcher watcher) + 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 groupNamePattern The group name pattern representing the group and dataId patterns to subscribe to. + * @param watcher The fuzzy watcher to be added. + * @return CompletableFuture containing collection of configs that match the specified fixed group name. + * @throws NacosException NacosException + * @since 3.0 + */ + Future> fuzzyWatchWithGroupKeys(String groupNamePattern, + FuzzyWatchEventWatcher watcher) 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 groupNamePattern The group name pattern representing the group and dataId patterns to subscribe to. + * @param watcher The fuzzy watcher to be added. + * @return CompletableFuture containing collection of configs that match the specified dataId pattern and fixed + * group name. + * @throws NacosException NacosException + * @since 3.0 + */ + Future> fuzzyWatchWithGroupKeys(String dataIdPattern, String groupNamePattern, + FuzzyWatchEventWatcher watcher) throws NacosException; + + /** + * Cancel fuzzy listen and remove the event listener for a specified fixed group name. + * + * @param groupNamePattern The group name pattern for fuzzy watch. + * @param watcher The event watcher to be removed. + * @throws NacosException If an error occurs during the cancellation process. + * @since 3.0 + */ + void cancelFuzzyWatch(String groupNamePattern, FuzzyWatchEventWatcher watcher) throws NacosException; + + /** + * Cancel fuzzy listen and remove the event listener for a specified service name pattern and fixed group name. + * + * @param dataIdPattern The pattern to match dataId for fuzzy watch. + * @param groupNamePattern The group name pattern for fuzzy watch. + * @param watcher The event listener to be removed. + * @throws NacosException If an error occurs during the cancellation process. + * @since 3.0 + */ + void cancelFuzzyWatch(String dataIdPattern, String groupNamePattern, FuzzyWatchEventWatcher watcher) + throws NacosException; + } diff --git a/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractFuzzyWatchEventWatcher.java b/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractFuzzyWatchEventWatcher.java new file mode 100644 index 00000000000..b62cca652af --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractFuzzyWatchEventWatcher.java @@ -0,0 +1,38 @@ +/* + * 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.concurrent.Executor; + +/** + * 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 AbstractFuzzyWatchEventWatcher implements FuzzyWatchEventWatcher { + + /** + * Get executor for execute this receive. + * + * @return Executor + */ + public Executor getExecutor() { + return null; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractListener.java b/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractListener.java index e14008d01d9..3f4c968f378 100644 --- a/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractListener.java +++ b/api/src/main/java/com/alibaba/nacos/api/config/listener/AbstractListener.java @@ -23,7 +23,6 @@ * * @author water.lyl */ -@SuppressWarnings("PMD.AbstractClassShouldStartWithAbstractNamingRule") public abstract class AbstractListener implements Listener { /** diff --git a/api/src/main/java/com/alibaba/nacos/api/config/listener/ConfigFuzzyWatchChangeEvent.java b/api/src/main/java/com/alibaba/nacos/api/config/listener/ConfigFuzzyWatchChangeEvent.java new file mode 100644 index 00000000000..ac90ec4caaf --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/listener/ConfigFuzzyWatchChangeEvent.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.listener; + +import com.alibaba.nacos.api.common.Constants; + +/** + * 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 ConfigFuzzyWatchChangeEvent { + + /** + * The group of the configuration that has changed. + */ + private String group; + + /** + * The data ID of the configuration that has changed. + */ + private String dataId; + + /** + * The namespace of the configuration that has changed. + */ + private String namespace; + + /** + * The change type of local watcher , contains {"ADD_CONFIG", "DELETE_CONFIG"}. + * {@link Constants.ConfigChangedType}. + */ + private String changedType; + + /** + * the sync type that trigger this changed,contains {"FUZZY_WATCH_INIT_NOTIFY","FUZZY_WATCH_RESOURCE_CHANGED", + * "FUZZY_WATCH_DIFF_SYNC_NOTIFY"}. + */ + private String syncType; + + /** + * 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 changedType The type of change that has occurred + */ + private ConfigFuzzyWatchChangeEvent(String namespace, String group, String dataId, String changedType, + String syncType) { + this.group = group; + this.dataId = dataId; + this.namespace = namespace; + this.changedType = changedType; + this.syncType = syncType; + } + + /** + * 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 changedType The type of change that has occurred + * @return A new FuzzyListenConfigChangeEvent instance + */ + public static ConfigFuzzyWatchChangeEvent build(String namespace, String group, String dataId, String changedType, + String syncType) { + return new ConfigFuzzyWatchChangeEvent(namespace, group, dataId, changedType, syncType); + } + + public String getNamespace() { + return namespace; + } + + public String getGroup() { + return group; + } + + public String getDataId() { + return dataId; + } + + public String getChangedType() { + return changedType; + } + + public String getSyncType() { + return syncType; + } + + @Override + public String toString() { + return "ConfigFuzzyWatchChangeEvent{" + "group='" + group + '\'' + ", dataId='" + dataId + '\'' + + ", namespace='" + namespace + '\'' + ", changedType='" + changedType + '\'' + ", syncType='" + + syncType + '\'' + '}'; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/listener/FuzzyWatchEventWatcher.java b/api/src/main/java/com/alibaba/nacos/api/config/listener/FuzzyWatchEventWatcher.java new file mode 100644 index 00000000000..56c1bbe5ed9 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/listener/FuzzyWatchEventWatcher.java @@ -0,0 +1,43 @@ +/* + * 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.concurrent.Executor; + +/** + * fuzzy watch config changes. + * + * @author stone-98 + * @date 2024/3/4 + */ +public interface FuzzyWatchEventWatcher { + + /** + * Callback method invoked when a fuzzy configuration change event occurs. + * + * @param event The fuzzy configuration change event + */ + void onEvent(ConfigFuzzyWatchChangeEvent event); + + /** + * Get executor for execute this receive. + * + * @return Executor + */ + Executor getExecutor(); + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/request/AbstractFuzzyWatchNotifyRequest.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/AbstractFuzzyWatchNotifyRequest.java new file mode 100644 index 00000000000..67a22264e70 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/AbstractFuzzyWatchNotifyRequest.java @@ -0,0 +1,38 @@ +/* + * 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 AbstractFuzzyWatchNotifyRequest extends ServerRequest { + + public AbstractFuzzyWatchNotifyRequest() { + } + + @Override + public String getModule() { + return CONFIG_MODULE; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigFuzzyWatchChangeNotifyRequest.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigFuzzyWatchChangeNotifyRequest.java new file mode 100644 index 00000000000..aaa64a705db --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigFuzzyWatchChangeNotifyRequest.java @@ -0,0 +1,83 @@ +/* + * 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 when a fuzzy watched configuration changed. + * + *

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 ConfigFuzzyWatchChangeNotifyRequest extends AbstractFuzzyWatchNotifyRequest { + + /** + * The groupKey of the configuration that has changed. + */ + private String groupKey; + + /** + * Indicates whether the configuration exists or not. + */ + private String changeType; + + /** + * Constructs an empty FuzzyListenNotifyChangeRequest. + */ + public ConfigFuzzyWatchChangeNotifyRequest() { + } + + /** + * Constructs a FuzzyListenNotifyChangeRequest with the specified parameters. + * + * @param groupKey The group of the configuration that has changed + * @param changeType Indicates whether the configuration exists or not + */ + public ConfigFuzzyWatchChangeNotifyRequest(String groupKey, String changeType) { + this.groupKey = groupKey; + this.changeType = changeType; + } + + public String getGroupKey() { + return groupKey; + } + + public void setGroupKey(String groupKey) { + this.groupKey = groupKey; + } + + public String getChangeType() { + return changeType; + } + + public void setChangeType(String changeType) { + this.changeType = changeType; + } + + /** + * Returns a string representation of the FuzzyListenNotifyChangeRequest. + * + * @return A string representation of the request + */ + @Override + public String toString() { + return "FuzzyListenNotifyChangeRequest{" + '\'' + ", groupKey='" + groupKey + '\'' + ", changeType=" + + changeType + '}'; + } + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigFuzzyWatchRequest.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigFuzzyWatchRequest.java new file mode 100644 index 00000000000..f61e36baac2 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigFuzzyWatchRequest.java @@ -0,0 +1,100 @@ +/* + * 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 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 ConfigFuzzyWatchRequest extends Request { + + /** + * The namespace or tenant associated with the configurations. + */ + private String groupKeyPattern; + + private Set receivedGroupKeys; + + /** + * Flag indicating whether to listen for changes. + */ + private String watchType; + + /** + * Flag indicating whether the client is initializing. + */ + private boolean isInitializing; + + /** + * Constructs an empty ConfigBatchFuzzyListenRequest. + */ + public ConfigFuzzyWatchRequest() { + } + + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + public void setGroupKeyPattern(String groupKeyPattern) { + this.groupKeyPattern = groupKeyPattern; + } + + public Set getReceivedGroupKeys() { + return receivedGroupKeys; + } + + public void setReceivedGroupKeys(Set receivedGroupKeys) { + this.receivedGroupKeys = receivedGroupKeys; + } + + public String getWatchType() { + return watchType; + } + + public void setWatchType(String watchType) { + this.watchType = watchType; + } + + public boolean isInitializing() { + return isInitializing; + } + + public void setInitializing(boolean initializing) { + isInitializing = initializing; + } + + /** + * Get the module name for this request. + * + * @return The module name + */ + @Override + public String getModule() { + return Constants.Config.CONFIG_MODULE; + } + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigFuzzyWatchSyncRequest.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigFuzzyWatchSyncRequest.java new file mode 100644 index 00000000000..e6ead123c78 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/request/ConfigFuzzyWatchSyncRequest.java @@ -0,0 +1,187 @@ +/* + * 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.Set; + +/** + * Represents a request to notify the difference between client and server side. + * + *

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 ConfigFuzzyWatchSyncRequest extends AbstractFuzzyWatchNotifyRequest { + + /** + * 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; + + /** + * see FUZZY_WATCH_INIT_NOTIFY,FINISH_FUZZY_WATCH_INIT_NOTIFY,FUZZY_WATCH_DIFF_SYNC_NOTIFY. + */ + private String syncType; + + private int totalBatch; + + private int currentBatch; + + public String getSyncType() { + return syncType; + } + + public void setSyncType(String syncType) { + this.syncType = syncType; + } + + public int getTotalBatch() { + return totalBatch; + } + + public void setTotalBatch(int totalBatch) { + this.totalBatch = totalBatch; + } + + public int getCurrentBatch() { + return currentBatch; + } + + public void setCurrentBatch(int currentBatch) { + this.currentBatch = currentBatch; + } + + /** + * Constructs an empty FuzzyListenNotifyDiffRequest. + */ + public ConfigFuzzyWatchSyncRequest() { + } + + /** + * Constructs a FuzzyListenNotifyDiffRequest with the specified parameters. + * + * @param groupKeyPattern The pattern used to match group keys for the configurations + * @param contexts The set of contexts containing information about the configurations + */ + private ConfigFuzzyWatchSyncRequest(String syncType, String groupKeyPattern, Set contexts, int totalBatch, + int currentBatch) { + this.groupKeyPattern = groupKeyPattern; + this.contexts = contexts; + this.syncType = syncType; + this.currentBatch = currentBatch; + this.totalBatch = totalBatch; + + } + + /** + * 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 ConfigFuzzyWatchSyncRequest buildSyncRequest(String syncType, Set contexts, + String groupKeyPattern, int totalBatch, int currentBatch) { + return new ConfigFuzzyWatchSyncRequest(syncType, groupKeyPattern, contexts, totalBatch, currentBatch); + } + + /** + * 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 ConfigFuzzyWatchSyncRequest buildInitFinishRequest(String groupKeyPattern) { + return new ConfigFuzzyWatchSyncRequest(Constants.FINISH_FUZZY_WATCH_INIT_NOTIFY, groupKeyPattern, null, 0, 0); + } + + 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 { + + String groupKey; + + /** + * see {@link com.alibaba.nacos.api.common.Constants.ConfigChangedType ADD_CONFIG&} ADD_CONFIG: a new config + * should be added for clientside . DELETE_CONFIG: a config should be removed for clientside . + */ + private String changedType; + + /** + * Constructs an empty Context object. + */ + public Context() { + } + + /** + * Builds a new context object with the provided parameters. + * + * @param groupKey The groupKey associated of the configuration. + * @param changedType The type of the configuration change event. + * @return A new context object initialized with the provided parameters. + */ + public static Context build(String groupKey, String changedType) { + Context context = new Context(); + context.setGroupKey(groupKey); + context.setChangedType(changedType); + return context; + } + + public String getGroupKey() { + return groupKey; + } + + public void setGroupKey(String groupKey) { + this.groupKey = groupKey; + } + + public String getChangedType() { + return changedType; + } + + public void setChangedType(String changedType) { + this.changedType = changedType; + } + } + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigFuzzyWatchChangeNotifyResponse.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigFuzzyWatchChangeNotifyResponse.java new file mode 100644 index 00000000000..abb8e8449d7 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigFuzzyWatchChangeNotifyResponse.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 ConfigFuzzyWatchChangeNotifyResponse extends Response { + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigFuzzyWatchResponse.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigFuzzyWatchResponse.java new file mode 100644 index 00000000000..bfcf065ed81 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigFuzzyWatchResponse.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 ConfigFuzzyWatchResponse extends Response { + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigFuzzyWatchSyncResponse.java b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigFuzzyWatchSyncResponse.java new file mode 100644 index 00000000000..9b91e010e2b --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/config/remote/response/ConfigFuzzyWatchSyncResponse.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 ConfigFuzzyWatchSyncResponse extends Response { + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/model/v2/ErrorCode.java b/api/src/main/java/com/alibaba/nacos/api/model/v2/ErrorCode.java index 4ee3ee2e188..c810a75440c 100644 --- a/api/src/main/java/com/alibaba/nacos/api/model/v2/ErrorCode.java +++ b/api/src/main/java/com/alibaba/nacos/api/model/v2/ErrorCode.java @@ -26,67 +26,67 @@ public enum ErrorCode { /** - * success. + * success. */ SUCCESS(0, "success"), /** - * parameter missing. + * parameter missing. */ PARAMETER_MISSING(10000, "parameter missing"), /** - * access denied. + * access denied. */ ACCESS_DENIED(10001, "access denied"), /** - * data access error. + * data access error. */ DATA_ACCESS_ERROR(10002, "data access error"), /** - * 'tenant' parameter error. + * 'tenant' parameter error. */ TENANT_PARAM_ERROR(20001, "'tenant' parameter error"), /** - * parameter validate error. + * parameter validate error. */ PARAMETER_VALIDATE_ERROR(20002, "parameter validate error"), /** - * MediaType Error. + * MediaType Error. */ MEDIA_TYPE_ERROR(20003, "MediaType Error"), /** - * resource not found. + * resource not found. */ RESOURCE_NOT_FOUND(20004, "resource not found"), /** - * resource conflict. + * resource conflict. */ RESOURCE_CONFLICT(20005, "resource conflict"), /** - * config listener is null. + * config listener is null. */ CONFIG_LISTENER_IS_NULL(20006, "config listener is null"), /** - * config listener error. + * config listener error. */ CONFIG_LISTENER_ERROR(20007, "config listener error"), /** - * invalid dataId. + * invalid dataId. */ INVALID_DATA_ID(20008, "invalid dataId"), /** - * parameter mismatch. + * parameter mismatch. */ PARAMETER_MISMATCH(20009, "parameter mismatch"), @@ -111,97 +111,97 @@ public enum ErrorCode { CONFIG_GRAY_NAME_UNRECOGNIZED_ERROR(20013, "config gray name not recognized"), /** - * service name error. + * service name error. */ SERVICE_NAME_ERROR(21000, "service name error"), /** - * weight error. + * weight error. */ WEIGHT_ERROR(21001, "weight error"), /** - * instance metadata error. + * instance metadata error. */ INSTANCE_METADATA_ERROR(21002, "instance metadata error"), /** - * instance not found. + * instance not found. */ INSTANCE_NOT_FOUND(21003, "instance not found"), /** - * instance error. + * instance error. */ INSTANCE_ERROR(21004, "instance error"), /** - * service metadata error. + * service metadata error. */ SERVICE_METADATA_ERROR(21005, "service metadata error"), /** - * selector error. + * selector error. */ SELECTOR_ERROR(21006, "selector error"), /** - * service already exist. + * service already exist. */ SERVICE_ALREADY_EXIST(21007, "service already exist"), /** - * service not exist. + * service not exist. */ SERVICE_NOT_EXIST(21008, "service not exist"), /** - * service delete failure. + * service delete failure. */ SERVICE_DELETE_FAILURE(21009, "service delete failure"), /** - * healthy param miss. + * healthy param miss. */ HEALTHY_PARAM_MISS(21010, "healthy param miss"), /** - * health check still running. + * health check still running. */ HEALTH_CHECK_STILL_RUNNING(21011, "health check still running"), /** - * illegal namespace. + * illegal namespace. */ ILLEGAL_NAMESPACE(22000, "illegal namespace"), /** - * namespace not exist. + * namespace not exist. */ NAMESPACE_NOT_EXIST(22001, "namespace not exist"), /** - * namespace already exist. + * namespace already exist. */ NAMESPACE_ALREADY_EXIST(22002, "namespace already exist"), /** - * illegal state. + * illegal state. */ ILLEGAL_STATE(23000, "illegal state"), /** - * node info error. + * node info error. */ NODE_INFO_ERROR(23001, "node info error"), /** - * node down failure. + * node down failure. */ NODE_DOWN_FAILURE(23002, "node down failure"), /** - * server error. + * server error. */ SERVER_ERROR(30000, "server error"), @@ -221,7 +221,11 @@ public enum ErrorCode { DATA_EMPTY(100005, "导入的文件数据为空"), - NO_SELECTED_CONFIG(100006, "没有选择任何配置"); + NO_SELECTED_CONFIG(100006, "没有选择任何配置"), + + FUZZY_WATCH_PATTERN_OVER_LIMIT(50310, "fuzzy watch pattern over limit"), + + FUZZY_WATCH_PATTERN_MATCH_GROUP_KEY_OVER_LIMIT(50311, "fuzzy watch pattern matched group key over limit"); private final Integer code; diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/NamingService.java b/api/src/main/java/com/alibaba/nacos/api/naming/NamingService.java index f10c25d3029..20034bae97e 100644 --- a/api/src/main/java/com/alibaba/nacos/api/naming/NamingService.java +++ b/api/src/main/java/com/alibaba/nacos/api/naming/NamingService.java @@ -17,6 +17,7 @@ package com.alibaba.nacos.api.naming; import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.listener.FuzzyWatchEventWatcher; import com.alibaba.nacos.api.naming.listener.EventListener; import com.alibaba.nacos.api.naming.pojo.Instance; import com.alibaba.nacos.api.naming.pojo.ListView; @@ -25,6 +26,7 @@ import com.alibaba.nacos.api.selector.AbstractSelector; import java.util.List; +import java.util.concurrent.Future; /** * Naming Service. @@ -554,7 +556,7 @@ void subscribe(String serviceName, String groupName, NamingSelector selector, Ev */ void unsubscribe(String serviceName, String groupName, List clusters, EventListener listener) throws NacosException; - + /** * Unsubscribe event listener of service. * @@ -577,6 +579,78 @@ void unsubscribe(String serviceName, String groupName, List clusters, Ev void unsubscribe(String serviceName, String groupName, NamingSelector selector, EventListener listener) throws NacosException; + + /** + * According to matching rules, watch services within a specific scope, and receive notifications when + * changes occur in the services within the scope. + * When given a fixed group name, watch changes in all services under this group. + * + * @param groupNamePattern group name pattern for fuzzy watch + * @param listener event listener + * @throws NacosException nacos exception + */ + void fuzzyWatch(String groupNamePattern, FuzzyWatchEventWatcher listener) throws NacosException; + + /** + * According to matching rules, watch services within a specific scope, and receive notifications when + * changes occur in the services within the scope. + * When provided with a fixed group name and pattern of service name, watch changes in services under + * this group that match the specified pattern. + * + * @param serviceNamePattern service name pattern for fuzzy watch + * @param groupNamePattern group name pattern for fuzzy watch + * @param listener event listener + * @throws NacosException nacos exception + */ + void fuzzyWatch(String serviceNamePattern, String groupNamePattern, + FuzzyWatchEventWatcher listener) throws NacosException; + + /** + * According to matching rules, watch services within a specific scope, and receive notifications when + * changes occur in the services within the scope. + * When given a fixed group name, watch changes in all services under this group. + * + * @param groupNamePattern group name pattern for fuzzy watch + * @param listener event listener + * @return matched service keys. + * @throws NacosException nacos exception + */ + Future> fuzzyWatchWithServiceKeys(String groupNamePattern, FuzzyWatchEventWatcher listener) throws NacosException; + + /** + * According to matching rules, watch services within a specific scope, and receive notifications when + * changes occur in the services within the scope. + * When provided with a fixed group name and pattern of service name, watch changes in services under + * this group that match the specified pattern. + * + * @param serviceNamePattern service name pattern for fuzzy watch + * @param groupNamePattern group name pattern for fuzzy watch + * @param listener event listener + * @return matched service keys. + * @throws NacosException nacos exception + */ + Future> fuzzyWatchWithServiceKeys(String serviceNamePattern, String groupNamePattern, + FuzzyWatchEventWatcher listener) throws NacosException; + + /** + * Cancel fuzzy watch, and remove event listener of a pattern. + * + * @param groupNamePattern group name for fuzzy watch + * @param listener event listener + * @throws NacosException nacos exception + */ + void cancelFuzzyWatch(String groupNamePattern, FuzzyWatchEventWatcher listener) throws NacosException; + + /** + * Cancel fuzzy watch, and remove event listener of a pattern. + * + * @param serviceNamePattern service name pattern for fuzzy watch + * @param groupNamePattern fixed group name for fuzzy watch + * @param listener event listener + * @throws NacosException nacos exception + */ + void cancelFuzzyWatch(String serviceNamePattern, String groupNamePattern, FuzzyWatchEventWatcher listener) throws NacosException; + /** * Get all service names from server. * diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/listener/AbstractFuzzyWatchEventWatcher.java b/api/src/main/java/com/alibaba/nacos/api/naming/listener/AbstractFuzzyWatchEventWatcher.java new file mode 100644 index 00000000000..fa10d05f1ea --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/listener/AbstractFuzzyWatchEventWatcher.java @@ -0,0 +1,32 @@ +/* + * 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.naming.listener; + +import java.util.concurrent.Executor; + +/** + * Abstract fuzzy watch event listener, to support handle event by user custom executor. + * + * @author tanyongquan + */ +public abstract class AbstractFuzzyWatchEventWatcher implements FuzzyWatchEventWatcher { + + @Override + public Executor getExecutor() { + return null; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/listener/FuzzyWatchChangeEvent.java b/api/src/main/java/com/alibaba/nacos/api/naming/listener/FuzzyWatchChangeEvent.java new file mode 100644 index 00000000000..c76e20d2ae9 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/listener/FuzzyWatchChangeEvent.java @@ -0,0 +1,83 @@ +/* + * 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.naming.listener; + +/** + * Fuzzy Watch Notify Event. + * + * @author tanyongquan + */ +public class FuzzyWatchChangeEvent implements Event { + + private String serviceName; + + private String groupName; + + private String namespace; + + private String changeType; + + private String syncType; + + public FuzzyWatchChangeEvent() { + } + + public FuzzyWatchChangeEvent(String serviceName, String groupName, String namespace, String changeType, + String syncType) { + this.changeType = changeType; + this.serviceName = serviceName; + this.groupName = groupName; + this.namespace = namespace; + this.syncType = syncType; + } + + public String getServiceName() { + return serviceName; + } + + public String getGroupName() { + return groupName; + } + + public String getNamespace() { + return namespace; + } + + /** + * The change type of local watcher , contains {"ADD_SERVICE", "DELETE_SERVICE"}. see Constants.ServiceChangedType + */ + public String getChangeType() { + return changeType; + } + + /** + * the sync type that trigger this changed,contains {"FUZZY_WATCH_INIT_NOTIFY","FUZZY_WATCH_RESOURCE_CHANGED", + * "FUZZY_WATCH_DIFF_SYNC_NOTIFY"}. + * + * @return + */ + public String getSyncType() { + return syncType; + } + + @Override + public String toString() { + return "FuzzyWatchChangeEvent{" + "serviceName='" + serviceName + '\'' + ", groupName='" + groupName + '\'' + + ", namespace='" + namespace + '\'' + ", changeType='" + changeType + '\'' + ", syncType='" + syncType + + '\'' + '}'; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/listener/FuzzyWatchEventWatcher.java b/api/src/main/java/com/alibaba/nacos/api/naming/listener/FuzzyWatchEventWatcher.java new file mode 100644 index 00000000000..edd6c2a3603 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/listener/FuzzyWatchEventWatcher.java @@ -0,0 +1,40 @@ +/* + * 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.naming.listener; + +import java.util.concurrent.Executor; + +/** + * Abstract fuzzy watch event listener, to support handle event by user custom executor. + * + * @author tanyongquan + */ +public interface FuzzyWatchEventWatcher { + + /** + * executor to notify event, using nacos internal notifier if null . + * @return + */ + Executor getExecutor(); + + /** + * handle FuzzyWatchChangeEvent. + * @param event fuzzy watch change event. + */ + void onEvent(FuzzyWatchChangeEvent event); + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/AbstractFuzzyWatchNotifyRequest.java b/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/AbstractFuzzyWatchNotifyRequest.java new file mode 100644 index 00000000000..c0a776d54bb --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/AbstractFuzzyWatchNotifyRequest.java @@ -0,0 +1,51 @@ +/* + * 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.naming.remote.request; + +import com.alibaba.nacos.api.remote.request.ServerRequest; + +import static com.alibaba.nacos.api.common.Constants.Naming.NAMING_MODULE; + +/** + * Abstract fuzzy watch notify request, including basic fuzzy watch notify information. + * + * @author tanyongquan + */ +public abstract class AbstractFuzzyWatchNotifyRequest extends ServerRequest { + + private String syncType; + + public AbstractFuzzyWatchNotifyRequest() { + } + + public AbstractFuzzyWatchNotifyRequest(String syncType) { + this.syncType = syncType; + } + + public String getSyncType() { + return syncType; + } + + public void setSyncType(String syncType) { + this.syncType = syncType; + } + + @Override + public String getModule() { + return NAMING_MODULE; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/NamingFuzzyWatchChangeNotifyRequest.java b/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/NamingFuzzyWatchChangeNotifyRequest.java new file mode 100644 index 00000000000..3abade19bc2 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/NamingFuzzyWatchChangeNotifyRequest.java @@ -0,0 +1,55 @@ +/* + * 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.naming.remote.request; + +/** + * Nacos fuzzy watch notify service change request, use it when one of the services changes. + * + * @author tanyongquan + */ +public class NamingFuzzyWatchChangeNotifyRequest extends AbstractFuzzyWatchNotifyRequest { + + private String serviceKey; + + private String changedType; + + public NamingFuzzyWatchChangeNotifyRequest() { + + } + + public NamingFuzzyWatchChangeNotifyRequest(String serviceKey, String changedType, String syncType) { + super(syncType); + this.serviceKey = serviceKey; + this.changedType = changedType; + } + + public String getServiceKey() { + return serviceKey; + } + + public void setServiceKey(String serviceKey) { + this.serviceKey = serviceKey; + } + + public String getChangedType() { + return changedType; + } + + public void setChangedType(String changedType) { + this.changedType = changedType; + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/NamingFuzzyWatchRequest.java b/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/NamingFuzzyWatchRequest.java new file mode 100644 index 00000000000..695348f8b00 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/NamingFuzzyWatchRequest.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.naming.remote.request; + +import com.alibaba.nacos.api.remote.request.Request; + +import java.util.Set; + +import static com.alibaba.nacos.api.common.Constants.Naming.NAMING_MODULE; + +/** + * Nacos naming fuzzy watch service request. + * + * @author tanyongquan + */ +public class NamingFuzzyWatchRequest extends Request { + + private boolean isInitializing; + + private String namespace; + + /** + * The namespace or tenant associated with the configurations. + */ + private String groupKeyPattern; + + private Set receivedGroupKeys; + + private String watchType; + + public NamingFuzzyWatchRequest() { + } + + public NamingFuzzyWatchRequest(String groupKeyPattern, String watchType) { + this.watchType = watchType; + this.groupKeyPattern = groupKeyPattern; + } + + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + public void setGroupKeyPattern(String groupKeyPattern) { + this.groupKeyPattern = groupKeyPattern; + } + + public String getWatchType() { + return watchType; + } + + public void setWatchType(String watchType) { + this.watchType = watchType; + } + + public Set getReceivedGroupKeys() { + return receivedGroupKeys; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public boolean isInitializing() { + return isInitializing; + } + + public void setInitializing(boolean initializing) { + isInitializing = initializing; + } + + public void setReceivedGroupKeys(Set receivedGroupKeys) { + this.receivedGroupKeys = receivedGroupKeys; + } + + @Override + public String getModule() { + return NAMING_MODULE; + } + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/NamingFuzzyWatchSyncRequest.java b/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/NamingFuzzyWatchSyncRequest.java new file mode 100644 index 00000000000..c7ac6ca52a4 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/remote/request/NamingFuzzyWatchSyncRequest.java @@ -0,0 +1,167 @@ +/* + * 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.naming.remote.request; + +import com.alibaba.nacos.api.common.Constants; + +import java.util.HashSet; +import java.util.Set; + +import static com.alibaba.nacos.api.common.Constants.Naming.NAMING_MODULE; + +/** + * fuzzy watch sync request from Nacos server. + * @author shiyiyue + */ +public class NamingFuzzyWatchSyncRequest extends AbstractFuzzyWatchNotifyRequest { + + /** + * The pattern used to match service keys for the services. + */ + private String groupKeyPattern; + + /** + * The set of contexts containing information about the service. + */ + private Set contexts; + + private int totalBatch; + + private int currentBatch; + + public NamingFuzzyWatchSyncRequest() { + + } + + public NamingFuzzyWatchSyncRequest(String pattern, String syncType, Set contexts) { + super(syncType); + this.groupKeyPattern = pattern; + this.contexts = contexts; + } + + public int getTotalBatch() { + return totalBatch; + } + + public void setTotalBatch(int totalBatch) { + this.totalBatch = totalBatch; + } + + public int getCurrentBatch() { + return currentBatch; + } + + public void setCurrentBatch(int currentBatch) { + this.currentBatch = currentBatch; + } + + public static NamingFuzzyWatchSyncRequest buildInitNotifyFinishRequest(String pattern) { + return new NamingFuzzyWatchSyncRequest(pattern, Constants.FINISH_FUZZY_WATCH_INIT_NOTIFY, new HashSet<>(1)); + } + + /** + * byuld SyncNotifyRequest. + * + * @param pattern pattern + * @param syncType syncType + * @param contexts contexts + * @param totalBatch totalBatch + * @param currentBatch currentBatch + * @return + */ + public static NamingFuzzyWatchSyncRequest buildSyncNotifyRequest(String pattern, String syncType, + Set contexts, int totalBatch, int currentBatch) { + NamingFuzzyWatchSyncRequest namingFuzzyWatchSyncRequest = new NamingFuzzyWatchSyncRequest(pattern, syncType, + contexts); + namingFuzzyWatchSyncRequest.currentBatch = currentBatch; + namingFuzzyWatchSyncRequest.totalBatch = totalBatch; + return namingFuzzyWatchSyncRequest; + } + + 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; + } + + @Override + public String getModule() { + return NAMING_MODULE; + } + + /** + * fuzzy watch sync context. + */ + public static class Context { + + /** + * service key. + */ + String serviceKey; + + /** + * changed type. + */ + private String changedType; + + /** + * Constructs an empty Context object. + */ + public Context() { + } + + /** + * Builds a new context object with the provided parameters. + * + * @param serviceKey The groupKey associated of the configuration. + * @param changedType The type of the configuration change event. + * @return A new context object initialized with the provided parameters. + */ + public static NamingFuzzyWatchSyncRequest.Context build(String serviceKey, String changedType) { + NamingFuzzyWatchSyncRequest.Context context = new NamingFuzzyWatchSyncRequest.Context(); + context.setServiceKey(serviceKey); + context.setChangedType(changedType); + return context; + } + + public String getServiceKey() { + return serviceKey; + } + + public void setServiceKey(String serviceKey) { + this.serviceKey = serviceKey; + } + + public String getChangedType() { + return changedType; + } + + public void setChangedType(String changedType) { + this.changedType = changedType; + } + } +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/remote/response/NamingFuzzyWatchChangeNotifyResponse.java b/api/src/main/java/com/alibaba/nacos/api/naming/remote/response/NamingFuzzyWatchChangeNotifyResponse.java new file mode 100644 index 00000000000..3fdeefe0971 --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/remote/response/NamingFuzzyWatchChangeNotifyResponse.java @@ -0,0 +1,28 @@ +/* + * 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.naming.remote.response; + +import com.alibaba.nacos.api.remote.response.Response; + +/** + * Response for notify fuzzy watcher. + * + * @author tanyongquan + */ +public class NamingFuzzyWatchChangeNotifyResponse extends Response { + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/remote/response/NamingFuzzyWatchResponse.java b/api/src/main/java/com/alibaba/nacos/api/naming/remote/response/NamingFuzzyWatchResponse.java new file mode 100644 index 00000000000..a18f81b66ec --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/remote/response/NamingFuzzyWatchResponse.java @@ -0,0 +1,48 @@ +/* + * 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.naming.remote.response; + +import com.alibaba.nacos.api.remote.response.Response; +import com.alibaba.nacos.api.remote.response.ResponseCode; + +/** + * Nacos naming fuzzy watch service response. + * + * @author tanyongquan + */ +public class NamingFuzzyWatchResponse extends Response { + + public NamingFuzzyWatchResponse() { + } + + public static NamingFuzzyWatchResponse buildSuccessResponse() { + return new NamingFuzzyWatchResponse(); + } + + /** + * Build fail response. + * + * @param message error message + * @return fail response + */ + public static NamingFuzzyWatchResponse buildFailResponse(String message) { + NamingFuzzyWatchResponse result = new NamingFuzzyWatchResponse(); + result.setErrorInfo(ResponseCode.FAIL.getCode(), message); + return result; + } + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/remote/response/NamingFuzzyWatchSyncResponse.java b/api/src/main/java/com/alibaba/nacos/api/naming/remote/response/NamingFuzzyWatchSyncResponse.java new file mode 100644 index 00000000000..90180a3b74d --- /dev/null +++ b/api/src/main/java/com/alibaba/nacos/api/naming/remote/response/NamingFuzzyWatchSyncResponse.java @@ -0,0 +1,48 @@ +/* + * 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.naming.remote.response; + +import com.alibaba.nacos.api.remote.response.Response; +import com.alibaba.nacos.api.remote.response.ResponseCode; + +/** + * Nacos naming fuzzy watch service response. + * + * @author tanyongquan + */ +public class NamingFuzzyWatchSyncResponse extends Response { + + public NamingFuzzyWatchSyncResponse() { + } + + public static NamingFuzzyWatchSyncResponse buildSuccessResponse() { + return new NamingFuzzyWatchSyncResponse(); + } + + /** + * Build fail response. + * + * @param message error message + * @return fail response + */ + public static NamingFuzzyWatchSyncResponse buildFailResponse(String message) { + NamingFuzzyWatchSyncResponse result = new NamingFuzzyWatchSyncResponse(); + result.setErrorInfo(ResponseCode.FAIL.getCode(), message); + return result; + } + +} diff --git a/api/src/main/java/com/alibaba/nacos/api/naming/utils/NamingUtils.java b/api/src/main/java/com/alibaba/nacos/api/naming/utils/NamingUtils.java index 074c41749e2..a74e94fb6e5 100644 --- a/api/src/main/java/com/alibaba/nacos/api/naming/utils/NamingUtils.java +++ b/api/src/main/java/com/alibaba/nacos/api/naming/utils/NamingUtils.java @@ -29,6 +29,7 @@ import java.util.regex.Pattern; import static com.alibaba.nacos.api.common.Constants.CLUSTER_NAME_PATTERN_STRING; +import static com.alibaba.nacos.api.common.Constants.DEFAULT_NAMESPACE_ID; import static com.alibaba.nacos.api.common.Constants.NUMBER_PATTERN_STRING; /** @@ -67,6 +68,23 @@ public static String getGroupedName(final String serviceName, final String group return resultGroupedName.intern(); } + public static String getServiceKey(String namespace, String group, String serviceName) { + if (StringUtils.isBlank(namespace)) { + namespace = DEFAULT_NAMESPACE_ID; + } + return namespace + Constants.SERVICE_INFO_SPLITER + group + Constants.SERVICE_INFO_SPLITER + serviceName; + } + + /** + * parse service key items for serviceKey. item[0] for namespace item[1] for group item[2] for service name + * + * @param serviceKey serviceKey. + * @return + */ + public static String[] parseServiceKey(String serviceKey) { + return serviceKey.split(Constants.SERVICE_INFO_SPLITER); + } + public static String getServiceName(final String serviceNameWithGroup) { if (StringUtils.isBlank(serviceNameWithGroup)) { return StringUtils.EMPTY; @@ -157,7 +175,8 @@ public static void checkInstanceIsLegal(Instance instance) throws NacosException throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.INSTANCE_ERROR, "Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'."); } - if (!StringUtils.isEmpty(instance.getClusterName()) && !CLUSTER_NAME_PATTERN.matcher(instance.getClusterName()).matches()) { + if (!StringUtils.isEmpty(instance.getClusterName()) && !CLUSTER_NAME_PATTERN.matcher(instance.getClusterName()) + .matches()) { throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.INSTANCE_ERROR, String.format("Instance 'clusterName' should be characters with only 0-9a-zA-Z-. (current: %s)", instance.getClusterName())); @@ -166,18 +185,21 @@ public static void checkInstanceIsLegal(Instance instance) throws NacosException /** * check batch register is Ephemeral. + * * @param instance instance * @throws NacosException NacosException */ public static void checkInstanceIsEphemeral(Instance instance) throws NacosException { if (!instance.isEphemeral()) { throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.INSTANCE_ERROR, - String.format("Batch registration does not allow persistent instance registration , Instance:%s", instance)); + String.format("Batch registration does not allow persistent instance registration , Instance:%s", + instance)); } } /** * Batch verify the validity of instances. + * * @param instances List of instances to be registered * @throws NacosException Nacos */ 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 618f0a47132..dbef9dda8b2 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 @@ -45,6 +45,13 @@ com.alibaba.nacos.api.config.remote.response.ConfigQueryResponse com.alibaba.nacos.api.config.remote.response.ConfigRemoveResponse com.alibaba.nacos.api.config.remote.request.cluster.ConfigChangeClusterSyncRequest com.alibaba.nacos.api.config.remote.response.cluster.ConfigChangeClusterSyncResponse +com.alibaba.nacos.api.config.remote.request.ConfigFuzzyWatchRequest +com.alibaba.nacos.api.config.remote.response.ConfigFuzzyWatchResponse +com.alibaba.nacos.api.config.remote.request.ConfigFuzzyWatchChangeNotifyRequest +com.alibaba.nacos.api.config.remote.response.ConfigFuzzyWatchChangeNotifyResponse +com.alibaba.nacos.api.config.remote.response.ConfigFuzzyWatchSyncResponse +com.alibaba.nacos.api.config.remote.request.ConfigFuzzyWatchSyncRequest + com.alibaba.nacos.api.naming.remote.request.BatchInstanceRequest com.alibaba.nacos.api.naming.remote.request.InstanceRequest com.alibaba.nacos.api.naming.remote.request.PersistentInstanceRequest @@ -58,5 +65,12 @@ 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 +com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchRequest +com.alibaba.nacos.api.naming.remote.response.NamingFuzzyWatchResponse +com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchChangeNotifyRequest +com.alibaba.nacos.api.naming.remote.response.NamingFuzzyWatchChangeNotifyResponse +com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchSyncRequest +com.alibaba.nacos.api.naming.remote.response.NamingFuzzyWatchSyncResponse + com.alibaba.nacos.api.lock.remote.request.LockOperationRequest com.alibaba.nacos.api.lock.remote.response.LockOperationResponse 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 ba17c03bb1a..76758bd7190 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,13 +21,14 @@ 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.FuzzyWatchEventWatcher; import com.alibaba.nacos.api.config.listener.Listener; import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.client.config.filter.impl.ConfigFilterChainManager; import com.alibaba.nacos.client.config.filter.impl.ConfigRequest; 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.ConfigFuzzyWatchContext; import com.alibaba.nacos.client.config.impl.ConfigServerListManager; import com.alibaba.nacos.client.config.impl.LocalConfigInfoProcessor; import com.alibaba.nacos.client.config.impl.LocalEncryptedDataKeyProcessor; @@ -42,6 +43,10 @@ import java.util.Collections; import java.util.Properties; +import java.util.Set; +import java.util.concurrent.Future; + +import static com.alibaba.nacos.api.common.Constants.ANY_PATTERN; /** * Config Impl. @@ -57,12 +62,6 @@ public class NacosConfigService implements ConfigService { private static final String DOWN = "DOWN"; - /** - * will be deleted in 2.0 later versions - */ - @Deprecated - ServerHttpAgent agent = null; - /** * long polling. */ @@ -84,8 +83,6 @@ public NacosConfigService(Properties properties) throws NacosException { serverListManager.start(); this.worker = new ClientWorker(this.configFilterChainManager, serverListManager, clientProperties); - // will be deleted in 2.0 later versions - agent = new ServerHttpAgent(serverListManager); } @@ -125,6 +122,55 @@ public void addListener(String dataId, String group, Listener listener) throws N worker.addTenantListeners(dataId, group, Collections.singletonList(listener)); } + @Override + public void fuzzyWatch(String groupNamePattern, FuzzyWatchEventWatcher watcher) throws NacosException { + doAddFuzzyWatch(ANY_PATTERN, groupNamePattern, watcher); + } + + @Override + public void fuzzyWatch(String dataIdPattern, String groupNamePattern, FuzzyWatchEventWatcher watcher) + throws NacosException { + doAddFuzzyWatch(dataIdPattern, groupNamePattern, watcher); + } + + @Override + public Future> fuzzyWatchWithGroupKeys(String groupNamePattern, FuzzyWatchEventWatcher watcher) + throws NacosException { + return doAddFuzzyWatch(ANY_PATTERN, groupNamePattern, watcher); + } + + @Override + public Future> fuzzyWatchWithGroupKeys(String dataIdPattern, String groupNamePattern, + FuzzyWatchEventWatcher watcher) throws NacosException { + return doAddFuzzyWatch(dataIdPattern, groupNamePattern, watcher); + } + + private Future> doAddFuzzyWatch(String dataIdPattern, String groupNamePattern, + FuzzyWatchEventWatcher watcher) throws NacosException { + ConfigFuzzyWatchContext configFuzzyWatchContext = worker.addTenantFuzzyWatcher(dataIdPattern, groupNamePattern, + watcher); + return configFuzzyWatchContext.createNewFuture(); + } + + @Override + public void cancelFuzzyWatch(String groupNamePattern, FuzzyWatchEventWatcher watcher) throws NacosException { + cancelFuzzyWatch(ANY_PATTERN, groupNamePattern, watcher); + } + + @Override + public void cancelFuzzyWatch(String dataIdPattern, String groupNamePattern, FuzzyWatchEventWatcher watcher) + throws NacosException { + doCancelFuzzyWatch(dataIdPattern, groupNamePattern, watcher); + } + + private void doCancelFuzzyWatch(String dataIdPattern, String groupNamePattern, FuzzyWatchEventWatcher watcher) + throws NacosException { + if (null == watcher) { + return; + } + worker.removeFuzzyListenListener(dataIdPattern, groupNamePattern, watcher); + } + @Override public boolean publishConfig(String dataId, String group, String content) throws NacosException { return publishConfig(dataId, group, content, ConfigType.getDefaultType().getType()); @@ -173,11 +219,11 @@ private String getConfigInner(String tenant, String dataId, String group, long t // changing config needed in the same time, while nacos server is down. String content = LocalConfigInfoProcessor.getFailover(worker.getAgentName(), dataId, group, tenant); if (content != null) { - LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}", - worker.getAgentName(), dataId, group, tenant); + 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(worker.getAgentName(), + dataId, group, tenant); cr.setEncryptedDataKey(encryptedDataKey); configFilterChainManager.doFilter(null, cr); content = cr.getContent(); @@ -199,15 +245,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); + 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(worker.getAgentName(), + dataId, group, tenant); cr.setEncryptedDataKey(encryptedDataKey); configFilterChainManager.doFilter(null, cr); content = cr.getContent(); @@ -239,8 +285,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 +297,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/ClientFuzzyWatchNotifyRequestHandler.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientFuzzyWatchNotifyRequestHandler.java new file mode 100644 index 00000000000..f6b9a934afd --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientFuzzyWatchNotifyRequestHandler.java @@ -0,0 +1,53 @@ +/* + * 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.client.config.impl; + +import com.alibaba.nacos.api.config.remote.request.ConfigFuzzyWatchChangeNotifyRequest; +import com.alibaba.nacos.api.config.remote.request.ConfigFuzzyWatchSyncRequest; +import com.alibaba.nacos.api.remote.request.Request; +import com.alibaba.nacos.api.remote.response.Response; +import com.alibaba.nacos.common.remote.client.Connection; +import com.alibaba.nacos.common.remote.client.ServerRequestHandler; + +/** + * fuzzy watch request from server . + * @author shiyiyue + */ +public class ClientFuzzyWatchNotifyRequestHandler implements ServerRequestHandler { + + ConfigFuzzyWatchGroupKeyHolder configFuzzyWatchGroupKeyHolder; + + public ClientFuzzyWatchNotifyRequestHandler(ConfigFuzzyWatchGroupKeyHolder configFuzzyWatchGroupKeyHolder) { + + this.configFuzzyWatchGroupKeyHolder = configFuzzyWatchGroupKeyHolder; + } + + @Override + public Response requestReply(Request request, Connection connection) { + //fuzzy watch diff reconciliation sync + if (request instanceof ConfigFuzzyWatchSyncRequest) { + return configFuzzyWatchGroupKeyHolder.handleFuzzyWatchNotifyDiffRequest( + (ConfigFuzzyWatchSyncRequest) request); + } + //fuzzy watch changed notify for a single config. include config changed or config delete. + if (request instanceof ConfigFuzzyWatchChangeNotifyRequest) { + return configFuzzyWatchGroupKeyHolder.handlerFuzzyWatchChangeNotifyRequest( + (ConfigFuzzyWatchChangeNotifyRequest) request); + } + return null; + } +} 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 de52a816ff7..56ade56deec 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,6 +19,7 @@ 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.FuzzyWatchEventWatcher; 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.ConfigBatchListenRequest; @@ -131,6 +132,8 @@ public class ClientWorker implements Closeable { private final DefaultLabelsCollectorManager defaultLabelsCollectorManager = new DefaultLabelsCollectorManager(); + private ConfigFuzzyWatchGroupKeyHolder configFuzzyWatchGroupKeyHolder; + private Map appLables = new HashMap<>(); private final ConfigFilterChainManager configFilterChainManager; @@ -286,6 +289,31 @@ public void removeTenantListener(String dataId, String group, Listener listener) } } + /** + * 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 groupPattern The group of the configuration. + * @param fuzzyWatchEventWatcher The configFuzzyWatcher to add. + * @throws NacosException If an error occurs while adding the listeners. + */ + public ConfigFuzzyWatchContext addTenantFuzzyWatcher(String dataIdPattern, String groupPattern, + FuzzyWatchEventWatcher fuzzyWatchEventWatcher) { + return configFuzzyWatchGroupKeyHolder.registerFuzzyWatcher(dataIdPattern, groupPattern, fuzzyWatchEventWatcher); + } + + /** + * 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 watcher The listener to remove. + * @throws NacosException If an error occurs while removing the listener. + */ + public void removeFuzzyListenListener(String dataIdPattern, String group, FuzzyWatchEventWatcher watcher) { + configFuzzyWatchGroupKeyHolder.removeFuzzyWatcher(dataIdPattern, group, watcher); + } + void removeCache(String dataId, String group, String tenant) { String groupKey = GroupKey.getKeyTenant(dataId, group, tenant); synchronized (cacheMap) { @@ -482,17 +510,20 @@ private String blank2defaultGroup(String group) { } @SuppressWarnings("PMD.ThreadPoolCreationRule") - public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ConfigServerListManager serverListManager, - final NacosClientProperties properties) throws NacosException { + public ClientWorker(final ConfigFilterChainManager configFilterChainManager, + ConfigServerListManager serverListManager, final NacosClientProperties properties) throws NacosException { this.configFilterChainManager = configFilterChainManager; init(properties); agent = new ConfigRpcTransportClient(properties, serverListManager); + + configFuzzyWatchGroupKeyHolder = new ConfigFuzzyWatchGroupKeyHolder(agent, uuid); ScheduledExecutorService executorService = Executors.newScheduledThreadPool(initWorkerThreadCount(properties), new NameThreadFactory("com.alibaba.nacos.client.Worker")); agent.setExecutor(executorService); agent.start(); + configFuzzyWatchGroupKeyHolder.start(); } @@ -696,6 +727,7 @@ private void initRpcClientHandler(final RpcClient rpcClientInner) { * Register Config Change /Config ReSync Handler */ rpcClientInner.registerServerRequestHandler((request, connection) -> { + //config change notify if (request instanceof ConfigChangeNotifyRequest) { return handleConfigChangeNotifyRequest((ConfigChangeNotifyRequest) request, rpcClientInner.getName()); @@ -710,18 +742,24 @@ private void initRpcClientHandler(final RpcClient rpcClientInner) { return null; }); + rpcClientInner.registerServerRequestHandler( + new ClientFuzzyWatchNotifyRequestHandler(configFuzzyWatchGroupKeyHolder)); + rpcClientInner.registerConnectionListener(new ConnectionEventListener() { @Override public void onConnected(Connection connection) { LOGGER.info("[{}] Connected,notify listen context...", rpcClientInner.getName()); notifyListenConfig(); + + LOGGER.info("[{}] Connected,notify fuzzy listen context...", rpcClientInner.getName()); + configFuzzyWatchGroupKeyHolder.notifyFuzzyWatchSync(); } @Override public void onDisConnect(Connection connection) { String taskId = rpcClientInner.getLabels().get("taskId"); - LOGGER.info("[{}] DisConnected,clear listen context...", rpcClientInner.getName()); + LOGGER.info("[{}] DisConnected,reset listen context", rpcClientInner.getName()); Collection values = cacheMap.get().values(); for (CacheData cacheData : values) { @@ -733,6 +771,9 @@ public void onDisConnect(Connection connection) { cacheData.setConsistentWithServer(false); } } + + LOGGER.info("[{}] DisConnected,reset fuzzy watch consistence status", rpcClientInner.getName()); + configFuzzyWatchGroupKeyHolder.resetConsistenceStatus(); } }); @@ -1089,7 +1130,7 @@ private boolean checkListenCache(Map> listenCachesMap) t return hasChangedKeys.get(); } - private RpcClient ensureRpcClient(String taskId) throws NacosException { + RpcClient ensureRpcClient(String taskId) throws NacosException { synchronized (ClientWorker.this) { Map labels = getLabels(); Map newLabels = new HashMap<>(labels); @@ -1103,7 +1144,7 @@ private RpcClient ensureRpcClient(String taskId) throws NacosException { rpcClient.setTenant(getTenant()); rpcClient.start(); } - + return rpcClient; } @@ -1202,7 +1243,7 @@ ConfigResponse queryConfigInner(RpcClient rpcClient, String dataId, String group } } - private Response requestProxy(RpcClient rpcClientInner, Request request) throws NacosException { + Response requestProxy(RpcClient rpcClientInner, Request request) throws NacosException { return requestProxy(rpcClientInner, request, requestTimeout); } @@ -1280,8 +1321,8 @@ public boolean publishConfig(String dataId, String group, String tenant, String this.getName(), dataId, group, tenant, response.getErrorCode(), response.getMessage()); return false; } else { - LOGGER.info("[{}] [publish-single] ok, dataId={}, group={}, tenant={}", getName(), - dataId, group, tenant); + LOGGER.info("[{}] [publish-single] ok, dataId={}, group={}, tenant={}", getName(), dataId, group, + tenant); return true; } } catch (Exception e) { diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatchContext.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatchContext.java new file mode 100644 index 00000000000..e9a14ee1cf4 --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatchContext.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.ConfigFuzzyWatchChangeEvent; +import com.alibaba.nacos.api.config.listener.FuzzyWatchEventWatcher; +import com.alibaba.nacos.client.config.common.GroupKey; +import com.alibaba.nacos.client.utils.LogUtils; +import com.alibaba.nacos.common.utils.ConcurrentHashSet; +import com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern; +import com.alibaba.nacos.common.utils.StringUtils; +import org.slf4j.Logger; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.ADD_CONFIG; +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.DELETE_CONFIG; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_DIFF_SYNC_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_INIT_NOTIFY; + +/** + * fuzzy wather context for a single group key pattern. + * + *

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 ConfigFuzzyWatchContext { + + /** + * Logger for FuzzyListenContext. + */ + private static final Logger LOGGER = LogUtils.logger(ConfigFuzzyWatchContext.class); + + /** + * Environment name. + */ + private String envName; + + /** + * Task ID. + */ + private int taskId; + + private String groupKeyPattern; + + /** + * Set of data IDs associated with the context. + */ + private Set receivedGroupKeys = new ConcurrentHashSet<>(); + + /** + * Flag indicating whether the context is consistent with the server. + */ + private final AtomicBoolean isConsistentWithServer = new AtomicBoolean(); + + /** + * Condition object for waiting initialization completion. + */ + final AtomicBoolean initializationCompleted = new AtomicBoolean(false); + + /** + * Flag indicating whether the context is discarded. + */ + private volatile boolean isDiscard = false; + + /** + * Set of listeners associated with the context. + */ + private Set configFuzzyWatcherWrappers = new HashSet<>(); + + /** + * Constructor with environment name, data ID pattern, and group. + * + * @param envName Environment name + * @param groupKeyPattern groupKeyPattern + */ + public ConfigFuzzyWatchContext(String envName, String groupKeyPattern) { + this.envName = envName; + this.groupKeyPattern = groupKeyPattern; + } + + /** + * 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 = configFuzzyWatcherWrappers; + } else { + for (ConfigFuzzyWatcherWrapper listener : configFuzzyWatcherWrappers) { + if (uuid.equals(listener.getUuid())) { + listenersToNotify.add(listener); + } + } + } + return listenersToNotify; + } + + /** + * Notify the listener with the specified data ID, type, and UUID. + * + * @param groupKey groupKey + * @param uuid UUID to filter listeners + */ + public void notifyWatcher(final String groupKey, final String changedType, final String syncType, + final String uuid) { + Set listenersToNotify = calculateListenersToNotify(uuid); + doNotifyWatchers(groupKey, changedType, syncType, listenersToNotify); + } + + /** + * Perform the notification for the specified data ID, type, and listeners. + * + * @param groupKey groupKey + * @param listenersToNotify Set of listeners to notify + */ + private void doNotifyWatchers(final String groupKey, final String changedType, final String syncType, + Set listenersToNotify) { + for (ConfigFuzzyWatcherWrapper watcher : listenersToNotify) { + doNotifyWatcher(groupKey, changedType, syncType, watcher); + } + } + + private void doNotifyWatcher(final String groupKey, final String changedType, final String syncType, + ConfigFuzzyWatcherWrapper configFuzzyWatcher) { + + if (ADD_CONFIG.equals(changedType) && configFuzzyWatcher.getSyncGroupKeys().contains(groupKey)) { + return; + } + + if (DELETE_CONFIG.equals(changedType) && !configFuzzyWatcher.getSyncGroupKeys().contains(groupKey)) { + return; + } + + String[] parseKey = GroupKey.parseKey(groupKey); + String dataId = parseKey[0]; + String group = parseKey[1]; + + String tenant = parseKey[2]; + + final String resetSyncType = initializationCompleted.get() ? syncType : FUZZY_WATCH_INIT_NOTIFY; + AbstractFuzzyNotifyTask job = new AbstractFuzzyNotifyTask() { + @Override + public void run() { + long start = System.currentTimeMillis(); + ConfigFuzzyWatchChangeEvent event = ConfigFuzzyWatchChangeEvent.build(tenant, group, dataId, + changedType, resetSyncType); + if (configFuzzyWatcher != null) { + configFuzzyWatcher.fuzzyWatchEventWatcher.onEvent(event); + } + LOGGER.info( + "[{}] [notify-fuzzy-watcher-ok] dataId={}, group={}, tenant={}, watcher={}, job run cost={} millis.", + envName, dataId, group, tenant, configFuzzyWatcher, (System.currentTimeMillis() - start)); + if (changedType.equals(DELETE_CONFIG)) { + configFuzzyWatcher.getSyncGroupKeys().remove(GroupKey.getKey(dataId, group, tenant)); + } else if (changedType.equals(ADD_CONFIG)) { + configFuzzyWatcher.getSyncGroupKeys().add(GroupKey.getKey(dataId, group, tenant)); + + } + } + }; + + try { + if (null != configFuzzyWatcher.fuzzyWatchEventWatcher.getExecutor()) { + LOGGER.info( + "[{}] [notify-fuzzy-watcher] task submitted to user executor, dataId={}, group={}, tenant={}, listener={}.", + envName, dataId, group, tenant, configFuzzyWatcher); + job.async = true; + configFuzzyWatcher.fuzzyWatchEventWatcher.getExecutor().execute(job); + } else { + LOGGER.info( + "[{}] [notify-fuzzy-watcher] task execute in nacos thread, dataId={}, group={}, tenant={}, listener={}.", + envName, dataId, group, tenant, configFuzzyWatcher); + job.run(); + } + } catch (Throwable t) { + LOGGER.error("[{}] [notify-fuzzy-watcher-error] dataId={}, group={}, tenant={}, listener={}, throwable={}.", + envName, dataId, group, tenant, configFuzzyWatcher, t.getCause()); + } + } + + /** + * Mark initialization as complete and notify waiting threads. + */ + public void markInitializationComplete() { + initializationCompleted.set(true); + synchronized (this) { + this.notifyAll(); + } + } + + /** + * Remove a watcher from the context. + * + * @param watcher watcher to be removed + */ + public void removeWatcher(FuzzyWatchEventWatcher watcher) { + + Iterator iterator = configFuzzyWatcherWrappers.iterator(); + while (iterator.hasNext()) { + ConfigFuzzyWatcherWrapper next = iterator.next(); + if (next.fuzzyWatchEventWatcher.equals(watcher)) { + iterator.remove(); + LOGGER.info("[{}] [remove-fuzzy-watcher-ok] groupKeyPattern={}, watcher={},uuid={} ", getEnvName(), + this.groupKeyPattern, watcher, next.getUuid()); + } + } + + } + + /** + * Add a watcher to the context. + * + * @param configFuzzyWatcherWrapper watcher to be added + */ + public boolean addWatcher(ConfigFuzzyWatcherWrapper configFuzzyWatcherWrapper) { + boolean added = configFuzzyWatcherWrappers.add(configFuzzyWatcherWrapper); + if (added) { + LOGGER.info("[{}] [add-fuzzy-watcher-ok] groupKeyPattern={}, watcher={},uuid={} ", getEnvName(), + this.groupKeyPattern, configFuzzyWatcherWrapper.fuzzyWatchEventWatcher, + configFuzzyWatcherWrapper.getUuid()); + } + return added; + } + + /** + * 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; + } + + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + /** + * Get the flag indicating whether the context is consistent with the server. + * + * @return AtomicBoolean indicating whether the context is consistent with the server + */ + public boolean isConsistentWithServer() { + return isConsistentWithServer.get(); + } + + public void setConsistentWithServer(boolean isConsistentWithServer) { + this.isConsistentWithServer.set(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 !initializationCompleted.get(); + } + + /** + * Get the set of data IDs associated with the context. zw + * + * @return Set of data IDs + */ + public Set getReceivedGroupKeys() { + return Collections.unmodifiableSet(receivedGroupKeys); + } + + public boolean addReceivedGroupKey(String groupKey) { + return receivedGroupKeys.add(groupKey); + } + + public boolean removeReceivedGroupKey(String groupKey) { + return receivedGroupKeys.remove(groupKey); + } + + /** + * Get the set of listeners associated with the context. + * + * @return Set of listeners + */ + public Set getConfigFuzzyWatcherWrappers() { + return configFuzzyWatcherWrappers; + } + + /** + * 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; + } + } + + void syncFuzzyWatchers() { + for (ConfigFuzzyWatcherWrapper configFuzzyWatcher : configFuzzyWatcherWrappers) { + Set receivedGroupKeysContext = receivedGroupKeys; + Set syncGroupKeys = configFuzzyWatcher.getSyncGroupKeys(); + List groupKeyStates = FuzzyGroupKeyPattern.diffGroupKeys( + receivedGroupKeysContext, syncGroupKeys); + for (FuzzyGroupKeyPattern.GroupKeyState groupKeyState : groupKeyStates) { + String changedType = groupKeyState.isExist() ? ADD_CONFIG : DELETE_CONFIG; + doNotifyWatcher(groupKeyState.getGroupKey(), changedType, FUZZY_WATCH_DIFF_SYNC_NOTIFY, + configFuzzyWatcher); + } + + } + } + + /** + * creat a new future of this context. + * @return + */ + public Future> createNewFuture() { + Future> completableFuture = new Future>() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + throw new UnsupportedOperationException("not support to cancel fuzzy watch"); + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return ConfigFuzzyWatchContext.this.initializationCompleted.get(); + } + + @Override + public Set get() throws InterruptedException, ExecutionException { + + if (!ConfigFuzzyWatchContext.this.initializationCompleted.get()) { + synchronized (ConfigFuzzyWatchContext.this) { + ConfigFuzzyWatchContext.this.wait(); + } + } + return new HashSet<>(ConfigFuzzyWatchContext.this.getReceivedGroupKeys()); + } + + public Set get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { + if (!ConfigFuzzyWatchContext.this.initializationCompleted.get()) { + synchronized (ConfigFuzzyWatchContext.this) { + ConfigFuzzyWatchContext.this.wait(unit.toMillis(timeout)); + } + } + + if (!ConfigFuzzyWatchContext.this.initializationCompleted.get()) { + throw new TimeoutException( + "fuzzy watch result future timeout for " + unit.toMillis(timeout) + " millis"); + } + return new HashSet<>(ConfigFuzzyWatchContext.this.getReceivedGroupKeys()); + } + }; + + return completableFuture; + } +} + diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatchGroupKeyHolder.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatchGroupKeyHolder.java new file mode 100644 index 00000000000..f64a8ccbff0 --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatchGroupKeyHolder.java @@ -0,0 +1,485 @@ +/* + * 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.client.config.impl; + +import com.alibaba.nacos.api.common.Constants; +import com.alibaba.nacos.api.config.listener.FuzzyWatchEventWatcher; +import com.alibaba.nacos.api.config.remote.request.ConfigFuzzyWatchChangeNotifyRequest; +import com.alibaba.nacos.api.config.remote.request.ConfigFuzzyWatchRequest; +import com.alibaba.nacos.api.config.remote.request.ConfigFuzzyWatchSyncRequest; +import com.alibaba.nacos.api.config.remote.response.ConfigFuzzyWatchChangeNotifyResponse; +import com.alibaba.nacos.api.config.remote.response.ConfigFuzzyWatchResponse; +import com.alibaba.nacos.api.config.remote.response.ConfigFuzzyWatchSyncResponse; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.client.config.common.GroupKey; +import com.alibaba.nacos.client.utils.LogUtils; +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.remote.client.RpcClient; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.ADD_CONFIG; +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.CONFIG_CHANGED; +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.DELETE_CONFIG; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_INIT_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_RESOURCE_CHANGED; +import static com.alibaba.nacos.api.common.Constants.WATCH_TYPE_CANCEL_WATCH; +import static com.alibaba.nacos.api.common.Constants.WATCH_TYPE_WATCH; + +/** + * config fuzzy watch context holder. + * @author shiyiyue + */ +public class ConfigFuzzyWatchGroupKeyHolder { + + private static final Logger LOGGER = LogUtils.logger(ClientWorker.class); + + private final ClientWorker.ConfigRpcTransportClient agent; + + private final String clientUuid; + + /** + * fuzzyListenExecuteBell. + */ + private final BlockingQueue fuzzyListenExecuteBell = new ArrayBlockingQueue<>(1); + + private final Object bellItem = new Object(); + + private final AtomicLong fuzzyListenLastAllSyncTime = new AtomicLong(System.currentTimeMillis()); + + private static final long FUZZY_LISTEN_ALL_SYNC_INTERNAL = 3 * 60 * 1000; + + private String taskId = "0"; + + /** + * fuzzyListenGroupKey -> fuzzyListenContext. + */ + private final AtomicReference> fuzzyListenContextMap = new AtomicReference<>( + new HashMap<>()); + + public ConfigFuzzyWatchGroupKeyHolder(ClientWorker.ConfigRpcTransportClient agent, String clientUuid) { + this.clientUuid = clientUuid; + this.agent = agent; + NotifyCenter.registerSubscriber(new Subscriber() { + @Override + public void onEvent(Event event) { + ConfigFuzzyWatchNotifyEvent configFuzzyWatchNotifyEvent = (ConfigFuzzyWatchNotifyEvent) event; + + //instance check + if (!configFuzzyWatchNotifyEvent.getClientUuid().equals(clientUuid)) { + return; + } + + ConfigFuzzyWatchContext context = fuzzyListenContextMap.get() + .get(configFuzzyWatchNotifyEvent.getGroupKeyPattern()); + if (context == null) { + return; + } + + context.notifyWatcher(configFuzzyWatchNotifyEvent.getGroupKey(), + configFuzzyWatchNotifyEvent.getChangedType(), configFuzzyWatchNotifyEvent.getSyncType(), + configFuzzyWatchNotifyEvent.getWatcherUuid()); + } + + @Override + public Class subscribeType() { + return ConfigFuzzyWatchNotifyEvent.class; + } + }); + } + + /** + * start. + */ + public void start() { + agent.executor.schedule(() -> { + while (!agent.executor.isShutdown() && !agent.executor.isTerminated()) { + try { + fuzzyListenExecuteBell.poll(5L, TimeUnit.SECONDS); + if (agent.executor.isShutdown() || agent.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 + } + notifyFuzzyWatchSync(); + } + } + }, 0L, TimeUnit.MILLISECONDS); + } + + /** + * Removes the fuzzy listen context for the specified data ID pattern and group. + * + * @param groupKeyPattern The pattern of the data ID. + */ + public void removeFuzzyListenContext(String groupKeyPattern) { + synchronized (fuzzyListenContextMap) { + Map copy = new HashMap<>(fuzzyListenContextMap.get()); + copy.remove(groupKeyPattern); + fuzzyListenContextMap.set(copy); + } + LOGGER.info("[{}] [fuzzy-watch-unsubscribe] {}", agent.getName(), groupKeyPattern); + } + + /** + * register fuzzy watcher. + * + * @param dataIdPattern dataIdPattern. + * @param groupPattern groupPattern. + * @param fuzzyWatchEventWatcher fuzzyWatchEventWatcher. + * @return + */ + public ConfigFuzzyWatchContext registerFuzzyWatcher(String dataIdPattern, String groupPattern, + FuzzyWatchEventWatcher fuzzyWatchEventWatcher) { + ConfigFuzzyWatchContext configFuzzyWatchContext = initFuzzyWatchContextIfAbsent(dataIdPattern, groupPattern); + ConfigFuzzyWatcherWrapper configFuzzyWatcherWrapper = new ConfigFuzzyWatcherWrapper(fuzzyWatchEventWatcher); + if (configFuzzyWatchContext.addWatcher(configFuzzyWatcherWrapper)) { + if (configFuzzyWatchContext.getReceivedGroupKeys() != null) { + for (String groupKey : configFuzzyWatchContext.getReceivedGroupKeys()) { + ConfigFuzzyWatchNotifyEvent configFuzzyWatchNotifyEvent = ConfigFuzzyWatchNotifyEvent.buildNotifyPatternAllListenersEvent( + groupKey, configFuzzyWatchContext.getGroupKeyPattern(), ADD_CONFIG, FUZZY_WATCH_INIT_NOTIFY, + configFuzzyWatcherWrapper.getUuid()); + NotifyCenter.publishEvent(configFuzzyWatchNotifyEvent); + } + } + } + return configFuzzyWatchContext; + } + + /** + * Retrieves the FuzzyListenContext for the given data ID pattern and group. + * + * @param dataIdPattern The data ID pattern. + * @param groupPattern The group name pattern. + * @return The corresponding FuzzyListenContext, or null if not found. + */ + public ConfigFuzzyWatchContext getFuzzyListenContext(String dataIdPattern, String groupPattern) { + return fuzzyListenContextMap.get() + .get(FuzzyGroupKeyPattern.generatePattern(dataIdPattern, groupPattern, agent.getTenant())); + } + + /** + * 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. + * @return A {@link ConfigFuzzyWatchSyncResponse} indicating the result of handling the request. + */ + ConfigFuzzyWatchSyncResponse handleFuzzyWatchNotifyDiffRequest(ConfigFuzzyWatchSyncRequest request) { + String groupKeyPattern = request.getGroupKeyPattern(); + ConfigFuzzyWatchContext context = fuzzyListenContextMap.get().get(groupKeyPattern); + if (Constants.FINISH_FUZZY_WATCH_INIT_NOTIFY.equals(request.getSyncType())) { + LOGGER.info("[{}] [fuzzy-watch] init-notify-finished, pattern ->{}, match group keys count {}", + agent.getName(), request.getGroupKeyPattern(), context.getReceivedGroupKeys().size()); + context.markInitializationComplete(); + return new ConfigFuzzyWatchSyncResponse(); + } + + LOGGER.info( + "[{}] [fuzzy-watch-diff-sync-push] pattern ->{},syncType={},,syncCount={},totalBatch={},currentBatch={}", + agent.getName(), request.getGroupKeyPattern(), request.getSyncType(), request.getContexts().size(), + request.getTotalBatch(), request.getCurrentBatch()); + + for (ConfigFuzzyWatchSyncRequest.Context requestContext : request.getContexts()) { + switch (requestContext.getChangedType()) { + case ADD_CONFIG: + if (context.addReceivedGroupKey(requestContext.getGroupKey())) { + LOGGER.info("[{}] [fuzzy-watch-diff-sync-push] local match group key added ,pattern ->{}, " + + "group key ->{},publish fuzzy watch notify event", agent.getName(), + request.getGroupKeyPattern(), requestContext.getGroupKey()); + NotifyCenter.publishEvent(ConfigFuzzyWatchNotifyEvent.buildNotifyPatternAllListenersEvent( + requestContext.getGroupKey(), request.getGroupKeyPattern(), + requestContext.getChangedType(), request.getSyncType(), this.clientUuid)); + } + break; + case DELETE_CONFIG: + if (context.removeReceivedGroupKey(requestContext.getGroupKey())) { + LOGGER.info("[{}] [fuzzy-watch-diff-sync-push] local match group key remove ,pattern ->{}, " + + "group key ->{},publish fuzzy watch notify event", agent.getName(), + request.getGroupKeyPattern(), requestContext.getGroupKey()); + NotifyCenter.publishEvent(ConfigFuzzyWatchNotifyEvent.buildNotifyPatternAllListenersEvent( + requestContext.getGroupKey(), request.getGroupKeyPattern(), + requestContext.getChangedType(), request.getSyncType(), this.clientUuid)); + } + break; + default: + LOGGER.warn("Invalid config change type: {}", requestContext.getChangedType()); + break; + } + } + return new ConfigFuzzyWatchSyncResponse(); + } + + /** + * Removes a fuzzy listen listener for the specified data ID pattern, group, and listener. + * + * @param dataIdPattern The pattern of the data ID. + * @param groupPattern The group of the configuration. + * @param watcher The listener to remove. + * @throws NacosException If an error occurs while removing the listener. + */ + public void removeFuzzyWatcher(String dataIdPattern, String groupPattern, FuzzyWatchEventWatcher watcher) { + ConfigFuzzyWatchContext configFuzzyWatchContext = getFuzzyListenContext(dataIdPattern, groupPattern); + if (configFuzzyWatchContext != null) { + synchronized (configFuzzyWatchContext) { + configFuzzyWatchContext.removeWatcher(watcher); + if (configFuzzyWatchContext.getConfigFuzzyWatcherWrappers().isEmpty()) { + configFuzzyWatchContext.setDiscard(true); + configFuzzyWatchContext.setConsistentWithServer(false); + } + } + } + } + + /** + * 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. + */ + ConfigFuzzyWatchChangeNotifyResponse handlerFuzzyWatchChangeNotifyRequest( + ConfigFuzzyWatchChangeNotifyRequest request) { + + LOGGER.info("[{}] [fuzzy-watch-change-notify-push] changeType={},groupKey={}", agent.getName(), + request.getChangeType(), request.getGroupKey()); + + Map listenContextMap = fuzzyListenContextMap.get(); + String[] groupItems = GroupKey.parseKey(request.getGroupKey()); + Set matchedPatterns = FuzzyGroupKeyPattern.filterMatchedPatterns(listenContextMap.keySet(), + groupItems[0], groupItems[1], groupItems[2]); + for (String matchedPattern : matchedPatterns) { + ConfigFuzzyWatchContext context = listenContextMap.get(matchedPattern); + if (ADD_CONFIG.equals(request.getChangeType()) || CONFIG_CHANGED.equals(request.getChangeType())) { + if (context.addReceivedGroupKey(request.getGroupKey())) { + LOGGER.info("[{}] [fuzzy-watch-change-notify-push] match group key added ,pattern={},groupKey={}", + agent.getName(), request.getChangeType(), request.getGroupKey()); + + NotifyCenter.publishEvent( + ConfigFuzzyWatchNotifyEvent.buildNotifyPatternAllListenersEvent(request.getGroupKey(), + matchedPattern, ADD_CONFIG, FUZZY_WATCH_RESOURCE_CHANGED, this.clientUuid)); + } + } else if (DELETE_CONFIG.equals(request.getChangeType()) && context.removeReceivedGroupKey( + request.getGroupKey())) { + NotifyCenter.publishEvent( + ConfigFuzzyWatchNotifyEvent.buildNotifyPatternAllListenersEvent(request.getGroupKey(), + matchedPattern, Constants.ConfigChangedType.DELETE_CONFIG, FUZZY_WATCH_RESOURCE_CHANGED, + this.clientUuid)); + + } + } + return new ConfigFuzzyWatchChangeNotifyResponse(); + } + + void notifyFuzzyWatchSync() { + fuzzyListenExecuteBell.offer(bellItem); + + } + + /** + * 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. + */ + public void executeConfigFuzzyListen() throws NacosException { + + // Obtain the current timestamp + long now = System.currentTimeMillis(); + + // Determine whether a full synchronization is needed + boolean needAllSync = now - fuzzyListenLastAllSyncTime.get() >= FUZZY_LISTEN_ALL_SYNC_INTERNAL; + + List needSyncContexts = new ArrayList<>(); + // Iterate through all fuzzy listen contexts + for (ConfigFuzzyWatchContext context : fuzzyListenContextMap.get().values()) { + // Check if the context is consistent with the server + if (context.isConsistentWithServer()) { + // Skip if a full synchronization is not needed + if (!needAllSync) { + continue; + } else { + context.syncFuzzyWatchers(); + } + } + + needSyncContexts.add(context); + } + + // Execute fuzzy listen operation for addition + doExecuteConfigFuzzyListen(needSyncContexts); + + // Update last all sync time if a full synchronization was performed + if (needAllSync) { + fuzzyListenLastAllSyncTime.set(now); + } + } + + void resetConsistenceStatus() { + Collection configFuzzyWatchContexts = fuzzyListenContextMap.get().values(); + + for (ConfigFuzzyWatchContext context : configFuzzyWatchContexts) { + context.setConsistentWithServer(false); + } + } + + /** + * 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 contextLists 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(List contextLists) throws NacosException { + // Return if the context map is null or empty + if (CollectionUtils.isEmpty(contextLists)) { + return; + } + + // List to hold futures for asynchronous tasks + List> listenFutures = new ArrayList<>(); + + RpcClient rpcClient = agent.ensureRpcClient(taskId); + + // Iterate through the context map and submit tasks for execution + for (ConfigFuzzyWatchContext entry : contextLists) { + ExecutorService executorService = agent.executor; + // Submit task for execution + Future future = executorService.submit(() -> { + ConfigFuzzyWatchRequest configFuzzyWatchRequest = buildFuzzyListenConfigRequest(entry); + try { + // Execute the fuzzy listen operation + ConfigFuzzyWatchResponse listenResponse = (ConfigFuzzyWatchResponse) agent.requestProxy(rpcClient, + configFuzzyWatchRequest); + if (listenResponse != null && listenResponse.isSuccess()) { + + if (entry.isDiscard()) { + removeFuzzyListenContext(entry.getGroupKeyPattern()); + } else { + entry.setConsistentWithServer(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 + notifyFuzzyWatchSync(); + } + }); + 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); + } + } + } + + /** + * Builds a request for fuzzy listen configuration. + * + * @param context The list of fuzzy listen contexts. + * @return A {@code ConfigBatchFuzzyListenRequest} object representing the request. + */ + private ConfigFuzzyWatchRequest buildFuzzyListenConfigRequest(ConfigFuzzyWatchContext context) { + ConfigFuzzyWatchRequest request = new ConfigFuzzyWatchRequest(); + request.setGroupKeyPattern(context.getGroupKeyPattern()); + request.setInitializing(context.isInitializing()); + request.setWatchType((context.isDiscard() && CollectionUtils.isEmpty(context.getConfigFuzzyWatcherWrappers())) + ? WATCH_TYPE_CANCEL_WATCH : WATCH_TYPE_WATCH); + request.setReceivedGroupKeys(context.getReceivedGroupKeys()); + return request; + } + + /** + * 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 groupPattern The group of the configuration. + * @return The fuzzy listen context for the specified data ID pattern and group. + */ + private ConfigFuzzyWatchContext initFuzzyWatchContextIfAbsent(String dataIdPattern, String groupPattern) { + ConfigFuzzyWatchContext context = getFuzzyListenContext(dataIdPattern, groupPattern); + if (context != null) { + return context; + } + synchronized (fuzzyListenContextMap) { + ConfigFuzzyWatchContext contextFromMap = getFuzzyListenContext(dataIdPattern, groupPattern); + if (contextFromMap != null) { + context = contextFromMap; + } else { + String groupKeyPattern = FuzzyGroupKeyPattern.generatePattern(dataIdPattern, groupPattern, + agent.getTenant()); + + context = new ConfigFuzzyWatchContext(agent.getName(), groupKeyPattern); + context.setConsistentWithServer(false); + Map copy = new HashMap<>(fuzzyListenContextMap.get()); + copy.put(groupKeyPattern, context); + LOGGER.info("[{}][fuzzy-watch] init fuzzy watch context , groupKeyPattern={} ,notify fuzzy watch sync ", + agent.getName(), groupKeyPattern); + fuzzyListenContextMap.set(copy); + notifyFuzzyWatchSync(); + } + } + + return context; + } + +} diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatchNotifyEvent.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatchNotifyEvent.java new file mode 100644 index 00000000000..3489e77ed40 --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatchNotifyEvent.java @@ -0,0 +1,133 @@ +/* + * 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 ConfigFuzzyWatchNotifyEvent extends Event { + + private String clientUuid; + + /** + * The uuid of this watcher for which that this notify event . + */ + private String watcherUuid; + + /** + * The groupKeyPattern of configuration. + */ + private String groupKeyPattern; + + private String groupKey; + + /** + * The type of notification (e.g., ADD_CONFIG, DELETE_CONFIG). + */ + private String changedType; + + private String syncType; + + /** + * Constructs a new FuzzyListenNotifyEvent. + */ + public ConfigFuzzyWatchNotifyEvent() { + } + + /** + * Constructs a new FuzzyListenNotifyEvent with the specified group, dataId, and type. + * + * @param groupKey The groupKey of the configuration. + * @param changedType The type of notification. + */ + private ConfigFuzzyWatchNotifyEvent(String groupKey, String changedType, String syncType, String groupKeyPattern, + String clientUuid, String watcherUuid) { + this.groupKey = groupKey; + this.syncType = syncType; + this.changedType = changedType; + this.groupKeyPattern = groupKeyPattern; + this.clientUuid = clientUuid; + this.watcherUuid = watcherUuid; + } + + /** + * Builds a new FuzzyListenNotifyEvent with the specified group, dataId, and type. + * + * @param groupKey The groupKey of the configuration. + * @return A new FuzzyListenNotifyEvent instance. + */ + public static ConfigFuzzyWatchNotifyEvent buildNotifyPatternAllListenersEvent(String groupKey, + String groupKeyPattern, String changedType, String syncType, String clientUuid) { + return buildNotifyPatternAllListenersEvent(groupKey, groupKeyPattern, changedType, syncType, clientUuid, null); + } + + /** + * Builds a new FuzzyListenNotifyEvent with the specified group, dataId, and type. + * + * @param groupKey The groupKey of the configuration. + * @return A new FuzzyListenNotifyEvent instance. + */ + public static ConfigFuzzyWatchNotifyEvent buildNotifyPatternAllListenersEvent(String groupKey, + String groupKeyPattern, String changedType, String syncType, String clientUuid, String watcherUuid) { + ConfigFuzzyWatchNotifyEvent configFuzzyWatchNotifyEvent = new ConfigFuzzyWatchNotifyEvent(groupKey, changedType, + syncType, groupKeyPattern, clientUuid, watcherUuid); + return configFuzzyWatchNotifyEvent; + } + + /** + * Gets the UUID (Unique Identifier) of the listener. + * + * @return The UUID of the listener. + */ + public String getWatcherUuid() { + return watcherUuid; + } + + public String getClientUuid() { + return clientUuid; + } + + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + public String getGroupKey() { + return groupKey; + } + + public String getSyncType() { + return syncType; + } + + /** + * Gets the type of notification. + * + * @return The type of notification. + */ + public String getChangedType() { + return changedType; + } +} diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatcherWrapper.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatcherWrapper.java new file mode 100644 index 00000000000..2d7ade95b71 --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigFuzzyWatcherWrapper.java @@ -0,0 +1,75 @@ +/* + * 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.client.config.impl; + +import com.alibaba.nacos.api.config.listener.FuzzyWatchEventWatcher; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +/** + * ConfigFuzzyWatcherWrapper. + * @author shiyiyue + */ +public class ConfigFuzzyWatcherWrapper { + + FuzzyWatchEventWatcher fuzzyWatchEventWatcher; + + public ConfigFuzzyWatcherWrapper(FuzzyWatchEventWatcher fuzzyWatchEventWatcher) { + this.fuzzyWatchEventWatcher = fuzzyWatchEventWatcher; + } + + /** + * Unique identifier for the listener. + */ + String uuid = UUID.randomUUID().toString(); + + private Set syncGroupKeys = new HashSet<>(); + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigFuzzyWatcherWrapper that = (ConfigFuzzyWatcherWrapper) o; + return Objects.equals(fuzzyWatchEventWatcher, that.fuzzyWatchEventWatcher) && Objects.equals(uuid, that.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(fuzzyWatchEventWatcher, uuid); + } + + Set getSyncGroupKeys() { + return syncGroupKeys; + } + + /** + * Get the UUID (Unique Identifier) of the listener. + * + * @return The UUID of the listener + */ + String getUuid() { + return uuid; + } + +} diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/NacosNamingService.java b/client/src/main/java/com/alibaba/nacos/client/naming/NacosNamingService.java index bf382f02e27..1b0b24d9884 100644 --- a/client/src/main/java/com/alibaba/nacos/client/naming/NacosNamingService.java +++ b/client/src/main/java/com/alibaba/nacos/client/naming/NacosNamingService.java @@ -21,6 +21,7 @@ import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.naming.NamingService; import com.alibaba.nacos.api.naming.listener.EventListener; +import com.alibaba.nacos.api.naming.listener.FuzzyWatchEventWatcher; import com.alibaba.nacos.api.naming.pojo.Instance; import com.alibaba.nacos.api.naming.pojo.ListView; import com.alibaba.nacos.api.naming.pojo.ServiceInfo; @@ -30,11 +31,14 @@ import com.alibaba.nacos.api.naming.utils.NamingUtils; import com.alibaba.nacos.api.selector.AbstractSelector; import com.alibaba.nacos.client.env.NacosClientProperties; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchContext; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchServiceListHolder; import com.alibaba.nacos.client.naming.cache.ServiceInfoHolder; import com.alibaba.nacos.client.naming.core.Balancer; import com.alibaba.nacos.client.naming.event.InstancesChangeEvent; import com.alibaba.nacos.client.naming.event.InstancesChangeNotifier; import com.alibaba.nacos.client.naming.event.InstancesDiff; +import com.alibaba.nacos.client.naming.event.NamingFuzzyWatchNotifyEvent; import com.alibaba.nacos.client.naming.remote.NamingClientProxy; import com.alibaba.nacos.client.naming.remote.NamingClientProxyDelegate; import com.alibaba.nacos.client.naming.selector.NamingSelectorFactory; @@ -47,6 +51,7 @@ import com.alibaba.nacos.client.utils.PreInitUtils; import com.alibaba.nacos.client.utils.ValidatorUtils; import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern; import com.alibaba.nacos.common.utils.JacksonUtils; import com.alibaba.nacos.common.utils.StringUtils; @@ -55,7 +60,9 @@ import java.util.List; import java.util.Properties; import java.util.UUID; +import java.util.concurrent.Future; +import static com.alibaba.nacos.api.common.Constants.ANY_PATTERN; import static com.alibaba.nacos.client.naming.selector.NamingSelectorFactory.getUniqueClusterString; import static com.alibaba.nacos.client.utils.LogUtils.NAMING_LOGGER; @@ -83,6 +90,8 @@ public class NacosNamingService implements NamingService { private ServiceInfoHolder serviceInfoHolder; + private NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder; + private InstancesChangeNotifier changeNotifier; private NamingClientProxy clientProxy; @@ -114,8 +123,12 @@ private void init(Properties properties) throws NacosException { NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384); NotifyCenter.registerSubscriber(changeNotifier); this.serviceInfoHolder = new ServiceInfoHolder(namespace, this.notifierEventScope, nacosClientProperties); + + NotifyCenter.registerToPublisher(NamingFuzzyWatchNotifyEvent.class, 16384); + this.namingFuzzyWatchServiceListHolder = new NamingFuzzyWatchServiceListHolder(this.notifierEventScope); + this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, nacosClientProperties, - changeNotifier); + changeNotifier, namingFuzzyWatchServiceListHolder); } @Deprecated @@ -362,7 +375,8 @@ private ServiceInfo getServiceInfoBySubscribe(String serviceName, String groupNa return serviceInfo; } - private ServiceInfo tryToSubscribe(String serviceName, String groupName, ServiceInfo cachedServiceInfo) throws NacosException { + private ServiceInfo tryToSubscribe(String serviceName, String groupName, ServiceInfo cachedServiceInfo) + throws NacosException { // not found in cache, service never subscribed. if (null == cachedServiceInfo) { return clientProxy.subscribe(serviceName, groupName, StringUtils.EMPTY); @@ -525,6 +539,66 @@ private void doUnsubscribe(String serviceName, String groupName, NamingSelector } } + @Override + public void fuzzyWatch(String fixedGroupName, FuzzyWatchEventWatcher listener) throws NacosException { + doFuzzyWatch(ANY_PATTERN, fixedGroupName, listener); + } + + @Override + public void fuzzyWatch(String serviceNamePattern, String groupNamePattern, FuzzyWatchEventWatcher listener) + throws NacosException { + doFuzzyWatch(serviceNamePattern, groupNamePattern, listener); + } + + @Override + public Future> fuzzyWatchWithServiceKeys(String fixedGroupName, FuzzyWatchEventWatcher listener) + throws NacosException { + return doFuzzyWatch(ANY_PATTERN, fixedGroupName, listener); + } + + @Override + public Future> fuzzyWatchWithServiceKeys(String serviceNamePattern, String groupNamePattern, + FuzzyWatchEventWatcher listener) throws NacosException { + return doFuzzyWatch(serviceNamePattern, groupNamePattern, listener); + } + + private Future> doFuzzyWatch(String serviceNamePattern, String groupNamePattern, + FuzzyWatchEventWatcher watcher) { + if (null == watcher) { + return null; + } + + String groupKeyPattern = FuzzyGroupKeyPattern.generatePattern(serviceNamePattern, groupNamePattern, namespace); + NamingFuzzyWatchContext namingFuzzyWatchContext = namingFuzzyWatchServiceListHolder.registerFuzzyWatcher( + groupKeyPattern, watcher); + return namingFuzzyWatchContext.createNewFuture(); + } + + @Override + public void cancelFuzzyWatch(String fixedGroupName, FuzzyWatchEventWatcher listener) throws NacosException { + doCancelFuzzyWatch(ANY_PATTERN, fixedGroupName, listener); + } + + @Override + public void cancelFuzzyWatch(String serviceNamePattern, String fixedGroupName, FuzzyWatchEventWatcher listener) + throws NacosException { + doCancelFuzzyWatch(serviceNamePattern, fixedGroupName, listener); + } + + private void doCancelFuzzyWatch(String serviceNamePattern, String groupNamePattern, FuzzyWatchEventWatcher watcher) + throws NacosException { + if (null == watcher) { + return; + } + String groupKeyPattern = FuzzyGroupKeyPattern.generatePattern(serviceNamePattern, groupNamePattern, namespace); + + NamingFuzzyWatchContext namingFuzzyWatchContext = namingFuzzyWatchServiceListHolder.getFuzzyWatchContext( + groupKeyPattern); + if (namingFuzzyWatchContext != null) { + namingFuzzyWatchContext.removeWatcher(watcher); + } + } + @Override public ListView getServicesOfServer(int pageNo, int pageSize) throws NacosException { return getServicesOfServer(pageNo, pageSize, Constants.DEFAULT_GROUP); @@ -561,6 +635,7 @@ public String getServerStatus() { public void shutDown() throws NacosException { serviceInfoHolder.shutdown(); clientProxy.shutdown(); + namingFuzzyWatchServiceListHolder.shutdown(); NotifyCenter.deregisterSubscriber(changeNotifier); } diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/cache/FuzzyWatchEventWatcherWrapper.java b/client/src/main/java/com/alibaba/nacos/client/naming/cache/FuzzyWatchEventWatcherWrapper.java new file mode 100644 index 00000000000..c607c8e9a44 --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/naming/cache/FuzzyWatchEventWatcherWrapper.java @@ -0,0 +1,66 @@ +/* + * 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.naming.cache; + +import com.alibaba.nacos.api.naming.listener.FuzzyWatchEventWatcher; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +/** + * fuzzy watcher wrapper. + * @author shiyiyue + */ +public class FuzzyWatchEventWatcherWrapper { + + FuzzyWatchEventWatcher fuzzyWatchEventWatcher; + + public FuzzyWatchEventWatcherWrapper(FuzzyWatchEventWatcher fuzzyWatchEventWatcher) { + this.fuzzyWatchEventWatcher = fuzzyWatchEventWatcher; + } + + String uuid = UUID.randomUUID().toString(); + + private Set syncServiceKeys = new HashSet<>(); + + final String getUuid() { + return uuid; + } + + Set getSyncServiceKeys() { + return syncServiceKeys; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FuzzyWatchEventWatcherWrapper that = (FuzzyWatchEventWatcherWrapper) o; + return Objects.equals(fuzzyWatchEventWatcher, that.fuzzyWatchEventWatcher); + } + + @Override + public int hashCode() { + return Objects.hash(fuzzyWatchEventWatcher); + } +} diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/cache/NamingFuzzyWatchContext.java b/client/src/main/java/com/alibaba/nacos/client/naming/cache/NamingFuzzyWatchContext.java new file mode 100644 index 00000000000..e5173614bda --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/naming/cache/NamingFuzzyWatchContext.java @@ -0,0 +1,369 @@ +/* + * 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.naming.cache; + +import com.alibaba.nacos.api.naming.listener.FuzzyWatchChangeEvent; +import com.alibaba.nacos.api.naming.listener.FuzzyWatchEventWatcher; +import com.alibaba.nacos.api.naming.pojo.ListView; +import com.alibaba.nacos.api.naming.utils.NamingUtils; +import com.alibaba.nacos.client.utils.LogUtils; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.ConcurrentHashSet; +import com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern; +import com.alibaba.nacos.common.utils.StringUtils; +import org.slf4j.Logger; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_DIFF_SYNC_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_INIT_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.ServiceChangedType.ADD_SERVICE; +import static com.alibaba.nacos.api.common.Constants.ServiceChangedType.DELETE_SERVICE; + +/** + * fuzzy wather context for a single group key pattern. + * + *

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 NamingFuzzyWatchContext { + + /** + * Logger for FuzzyListenContext. + */ + private static final Logger LOGGER = LogUtils.logger(NamingFuzzyWatchContext.class); + + /** + * Environment name. + */ + private String envName; + + private String groupKeyPattern; + + /** + * Set of service keys associated with the context. + */ + private Set receivedServiceKeys = new ConcurrentHashSet<>(); + + /** + * Flag indicating whether the context is consistent with the server. + */ + private final AtomicBoolean isConsistentWithServer = new AtomicBoolean(); + + /** + * Condition object for waiting initialization completion. + */ + final AtomicBoolean initializationCompleted = new AtomicBoolean(false); + + /** + * Flag indicating whether the context is discarded. + */ + private volatile boolean isDiscard = false; + + /** + * Set of listeners associated with the context. + */ + private final Set fuzzyWatchEventWatcherWrappers = new HashSet<>(); + + /** + * Constructor with environment name, data ID pattern, and group. + * + * @param envName Environment name + * @param groupKeyPattern groupKeyPattern + */ + public NamingFuzzyWatchContext(String envName, String groupKeyPattern) { + this.envName = envName; + this.groupKeyPattern = groupKeyPattern; + } + + private void doNotifyWatcher(final String serviceKey, final String changedType, final String syncType, + FuzzyWatchEventWatcherWrapper fuzzyWatchEventWatcherWrapper) { + + if (ADD_SERVICE.equals(changedType) && fuzzyWatchEventWatcherWrapper.getSyncServiceKeys() + .contains(serviceKey)) { + return; + } + + if (DELETE_SERVICE.equals(changedType) && !fuzzyWatchEventWatcherWrapper.getSyncServiceKeys() + .contains(serviceKey)) { + return; + } + + String[] serviceKeyItems = NamingUtils.parseServiceKey(serviceKey); + String namespace = serviceKeyItems[0]; + String groupName = serviceKeyItems[1]; + String serviceName = serviceKeyItems[2]; + + final String resetSyncType = !initializationCompleted.get() ? FUZZY_WATCH_INIT_NOTIFY : syncType; + + Runnable job = () -> { + long start = System.currentTimeMillis(); + FuzzyWatchChangeEvent event = new FuzzyWatchChangeEvent(serviceName, groupName, namespace, changedType, + resetSyncType); + if (fuzzyWatchEventWatcherWrapper != null) { + fuzzyWatchEventWatcherWrapper.fuzzyWatchEventWatcher.onEvent(event); + } + LOGGER.info( + "[{}] [notify-watcher-ok] serviceName={}, groupName={}, namespace={}, watcher={},changedType={}, job run cost={} millis.", + envName, serviceName, groupName, namespace, fuzzyWatchEventWatcherWrapper.fuzzyWatchEventWatcher, + changedType, (System.currentTimeMillis() - start)); + if (changedType.equals(DELETE_SERVICE)) { + fuzzyWatchEventWatcherWrapper.getSyncServiceKeys() + .remove(NamingUtils.getServiceKey(namespace, groupName, serviceName)); + } else if (changedType.equals(ADD_SERVICE)) { + fuzzyWatchEventWatcherWrapper.getSyncServiceKeys() + .add(NamingUtils.getServiceKey(namespace, groupName, serviceName)); + } + }; + + try { + if (null != fuzzyWatchEventWatcherWrapper.fuzzyWatchEventWatcher.getExecutor()) { + LOGGER.info( + "[{}] [notify-watcher] task submitted to user executor, serviceName={}, groupName={}, namespace={}, listener={}.", + envName, serviceName, groupName, namespace, fuzzyWatchEventWatcherWrapper); + fuzzyWatchEventWatcherWrapper.fuzzyWatchEventWatcher.getExecutor().execute(job); + } else { + LOGGER.info( + "[{}] [notify-watcher] task execute in nacos thread, serviceName={}, groupName={}, namespace={}, listener={}.", + envName, serviceName, groupName, namespace, fuzzyWatchEventWatcherWrapper); + job.run(); + } + } catch (Throwable t) { + LOGGER.error( + "[{}] [notify-watcher-error] serviceName={}, groupName={}, namespace={}, listener={}, throwable={}.", + envName, serviceName, groupName, namespace, fuzzyWatchEventWatcherWrapper, t.getCause()); + } + } + + /** + * Mark initialization as complete and notify waiting threads. + */ + public void markInitializationComplete() { + LOGGER.info("[{}] [fuzzy-watch] pattern init notify finish pattern={},match service count {}", envName, + groupKeyPattern, receivedServiceKeys.size()); + initializationCompleted.set(true); + synchronized (this) { + notifyAll(); + } + } + + /** + * Remove a watcher from the context. + * + * @param watcher watcher to be removed + */ + public void removeWatcher(FuzzyWatchEventWatcher watcher) { + Iterator iterator = fuzzyWatchEventWatcherWrappers.iterator(); + while (iterator.hasNext()) { + FuzzyWatchEventWatcherWrapper next = iterator.next(); + if (next.fuzzyWatchEventWatcher.equals(watcher)) { + iterator.remove(); + LOGGER.info("[{}] [remove-watcher-ok] groupKeyPattern={}, watcher={},uuid={} ", getEnvName(), + this.groupKeyPattern, watcher, next.getUuid()); + } + } + + } + + /** + * 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; + } + + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + /** + * Get the flag indicating whether the context is consistent with the server. + * + * @return AtomicBoolean indicating whether the context is consistent with the server + */ + public boolean isConsistentWithServer() { + return isConsistentWithServer.get(); + } + + public void setConsistentWithServer(boolean isConsistentWithServer) { + this.isConsistentWithServer.set(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 !initializationCompleted.get(); + } + + /** + * Get the set of data IDs associated with the context. + * + * @return Set of data IDs + */ + public Set getReceivedServiceKeys() { + return Collections.unmodifiableSet(receivedServiceKeys); + } + + public boolean addReceivedServiceKey(String serviceKey) { + return receivedServiceKeys.add(serviceKey); + } + + public boolean removeReceivedServiceKey(String serviceKey) { + return receivedServiceKeys.remove(serviceKey); + } + + /** + * Get the set of listeners associated with the context. + * + * @return Set of listeners + */ + public Set getFuzzyWatchEventWatcherWrappers() { + return fuzzyWatchEventWatcherWrappers; + } + + void syncFuzzyWatchers() { + for (FuzzyWatchEventWatcherWrapper namingFuzzyWatcher : fuzzyWatchEventWatcherWrappers) { + Set receivedServiceKeysContext = this.getReceivedServiceKeys(); + Set syncGroupKeys = namingFuzzyWatcher.getSyncServiceKeys(); + List groupKeyStates = FuzzyGroupKeyPattern.diffGroupKeys( + receivedServiceKeysContext, syncGroupKeys); + for (FuzzyGroupKeyPattern.GroupKeyState groupKeyState : groupKeyStates) { + String changedType = groupKeyState.isExist() ? ADD_SERVICE : DELETE_SERVICE; + doNotifyWatcher(groupKeyState.getGroupKey(), changedType, FUZZY_WATCH_DIFF_SYNC_NOTIFY, + namingFuzzyWatcher); + } + } + } + + void notifyFuzzyWatchers(String serviceKey, String changedType, String syncType, String watcherUuid) { + for (FuzzyWatchEventWatcherWrapper namingFuzzyWatcher : filterWatchers(watcherUuid)) { + doNotifyWatcher(serviceKey, changedType, syncType, namingFuzzyWatcher); + } + } + + private Set filterWatchers(String uuid) { + if (StringUtils.isBlank(uuid) || CollectionUtils.isEmpty(getFuzzyWatchEventWatcherWrappers())) { + return getFuzzyWatchEventWatcherWrappers(); + } else { + return getFuzzyWatchEventWatcherWrappers().stream().filter(a -> a.getUuid().equals(uuid)) + .collect(Collectors.toSet()); + } + } + + /** + * create a new future of this context. + * @return + */ + public Future> createNewFuture() { + Future> completableFuture = new Future>() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + throw new UnsupportedOperationException("not support to cancel fuzzy watch"); + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return NamingFuzzyWatchContext.this.initializationCompleted.get(); + } + + @Override + public ListView get() throws InterruptedException { + synchronized (NamingFuzzyWatchContext.this) { + while (!NamingFuzzyWatchContext.this.initializationCompleted.get()) { + NamingFuzzyWatchContext.this.wait(); + } + } + + ListView result = new ListView<>(); + result.setData(Arrays.asList(NamingFuzzyWatchContext.this.receivedServiceKeys.toArray(new String[0]))); + result.setCount(result.getData().size()); + return result; + } + + @Override + public ListView get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { + + if (!NamingFuzzyWatchContext.this.initializationCompleted.get()) { + synchronized (NamingFuzzyWatchContext.this) { + NamingFuzzyWatchContext.this.wait(unit.toMillis(timeout)); + } + } + + if (!NamingFuzzyWatchContext.this.initializationCompleted.get()) { + throw new TimeoutException( + "fuzzy watch result future timeout for " + unit.toMillis(timeout) + " millis"); + } + + ListView result = new ListView<>(); + result.setData(Arrays.asList(NamingFuzzyWatchContext.this.receivedServiceKeys.toArray(new String[0]))); + result.setCount(result.getData().size()); + return result; + } + }; + return completableFuture; + } +} diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/cache/NamingFuzzyWatchServiceListHolder.java b/client/src/main/java/com/alibaba/nacos/client/naming/cache/NamingFuzzyWatchServiceListHolder.java new file mode 100644 index 00000000000..64fcc984eec --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/naming/cache/NamingFuzzyWatchServiceListHolder.java @@ -0,0 +1,340 @@ +/* + * 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.naming.cache; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.listener.FuzzyWatchEventWatcher; +import com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchRequest; +import com.alibaba.nacos.api.naming.remote.response.NamingFuzzyWatchResponse; +import com.alibaba.nacos.client.naming.event.NamingFuzzyWatchNotifyEvent; +import com.alibaba.nacos.client.naming.remote.gprc.NamingGrpcClientProxy; +import com.alibaba.nacos.client.utils.LogUtils; +import com.alibaba.nacos.common.executor.NameThreadFactory; +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 org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_INIT_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.ServiceChangedType.ADD_SERVICE; +import static com.alibaba.nacos.api.common.Constants.WATCH_TYPE_CANCEL_WATCH; +import static com.alibaba.nacos.api.common.Constants.WATCH_TYPE_WATCH; + +/** + * Naming client fuzzy watch service list holder. + * + * @author tanyongquan + */ +public class NamingFuzzyWatchServiceListHolder extends Subscriber { + + private static final Logger LOGGER = LogUtils.logger(NamingFuzzyWatchServiceListHolder.class); + + private String notifierEventScope; + + private NamingGrpcClientProxy namingGrpcClientProxy; + + /** + * fuzzyListenExecuteBell. + */ + private final BlockingQueue fuzzyWatchExecuteBell = new ArrayBlockingQueue<>(1); + + private final Object bellItem = new Object(); + + private final AtomicLong fuzzyWatchLastAllSyncTime = new AtomicLong(System.currentTimeMillis()); + + private static final long FUZZY_LISTEN_ALL_SYNC_INTERNAL = 3 * 60 * 1000; + + ScheduledExecutorService executorService; + + /** + * The contents of {@code patternMatchMap} are Map{pattern -> Set[matched services]}. + */ + private Map fuzzyMatchContextMap = new ConcurrentHashMap<>(); + + public NamingFuzzyWatchServiceListHolder(String notifierEventScope) { + this.notifierEventScope = notifierEventScope; + NotifyCenter.registerSubscriber(this); + } + + /** + * shut down. + */ + public void shutdown() { + if (executorService != null && !executorService.isShutdown()) { + executorService.shutdown(); + } + } + + /** + * start. + */ + @SuppressWarnings("PMD.ThreadPoolCreationRule") + public void start() { + + executorService = Executors.newSingleThreadScheduledExecutor( + new NameThreadFactory("com.alibaba.nacos.client.naming.fuzzy.watch.Worker")); + executorService.submit(() -> { + while (!executorService.isShutdown() && !executorService.isTerminated()) { + try { + fuzzyWatchExecuteBell.poll(5L, TimeUnit.SECONDS); + if (executorService.isShutdown() || executorService.isTerminated()) { + continue; + } + executeNamingFuzzyWatch(); + } catch (Throwable e) { + LOGGER.error("[rpc-fuzzy-watch-execute] rpc fuzzy watch exception", e); + try { + Thread.sleep(50L); + } catch (InterruptedException interruptedException) { + //ignore + } + notifyFuzzyWatchSync(); + } + } + }); + } + + public void registerNamingGrpcClientProxy(NamingGrpcClientProxy namingGrpcClientProxy) { + this.namingGrpcClientProxy = namingGrpcClientProxy; + } + + public NamingFuzzyWatchContext getFuzzyWatchContext(String groupKeyPattern) { + return fuzzyMatchContextMap.get(groupKeyPattern); + } + + /** + * Add a watcher to the context. + * + * @param watcher watcher to be added + */ + public NamingFuzzyWatchContext registerFuzzyWatcher(String groupKeyPattern, FuzzyWatchEventWatcher watcher) { + NamingFuzzyWatchContext namingFuzzyWatchContext = initFuzzyWatchContextIfNeed(groupKeyPattern); + + FuzzyWatchEventWatcherWrapper fuzzyWatchEventWatcherWrapper = new FuzzyWatchEventWatcherWrapper(watcher); + if (namingFuzzyWatchContext.getFuzzyWatchEventWatcherWrappers().add(fuzzyWatchEventWatcherWrapper)) { + LOGGER.info(" [add-watcher-ok] groupKeyPattern={}, watcher={},uuid={} ", groupKeyPattern, watcher, + fuzzyWatchEventWatcherWrapper.getUuid()); + if (CollectionUtils.isNotEmpty(namingFuzzyWatchContext.getReceivedServiceKeys())) { + for (String serviceKey : namingFuzzyWatchContext.getReceivedServiceKeys()) { + NamingFuzzyWatchNotifyEvent namingFuzzyWatchNotifyEvent = NamingFuzzyWatchNotifyEvent.build( + notifierEventScope, groupKeyPattern, serviceKey, ADD_SERVICE, FUZZY_WATCH_INIT_NOTIFY, + fuzzyWatchEventWatcherWrapper.getUuid()); + NotifyCenter.publishEvent(namingFuzzyWatchNotifyEvent); + } + } + } + return namingFuzzyWatchContext; + } + + /** + * init fuzzy watch context. + * @param groupKeyPattern groupKeyPattern. + * @return fuzzy context. + */ + public NamingFuzzyWatchContext initFuzzyWatchContextIfNeed(String groupKeyPattern) { + if (!fuzzyMatchContextMap.containsKey(groupKeyPattern)) { + synchronized (fuzzyMatchContextMap) { + if (fuzzyMatchContextMap.containsKey(groupKeyPattern)) { + return fuzzyMatchContextMap.get(groupKeyPattern); + } + LOGGER.info("[fuzzy-watch] init fuzzy watch context for pattern {}", groupKeyPattern); + fuzzyMatchContextMap.putIfAbsent(groupKeyPattern, + new NamingFuzzyWatchContext(notifierEventScope, groupKeyPattern)); + notifyFuzzyWatchSync(); + } + } + return fuzzyMatchContextMap.get(groupKeyPattern); + } + + /** + * remove fuzzy watch context for pattern. + * @param groupKeyPattern group key pattern. + */ + public synchronized void removePatternMatchCache(String groupKeyPattern) { + NamingFuzzyWatchContext namingFuzzyWatchContext = fuzzyMatchContextMap.get(groupKeyPattern); + if (namingFuzzyWatchContext == null) { + return; + } + if (namingFuzzyWatchContext.isDiscard() && namingFuzzyWatchContext.getFuzzyWatchEventWatcherWrappers() + .isEmpty()) { + LOGGER.info("[fuzzy-watch] remove fuzzy watch context for pattern {}", groupKeyPattern); + fuzzyMatchContextMap.remove(groupKeyPattern); + } + } + + /** + * notify sync fuzzy watch with server. + */ + void notifyFuzzyWatchSync() { + fuzzyWatchExecuteBell.offer(bellItem); + } + + /** + * 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. + */ + public void executeNamingFuzzyWatch() throws NacosException { + + // Obtain the current timestamp + long now = System.currentTimeMillis(); + + // Determine whether a full synchronization is needed + boolean needAllSync = now - fuzzyWatchLastAllSyncTime.get() >= FUZZY_LISTEN_ALL_SYNC_INTERNAL; + + List needSyncContexts = new ArrayList<>(); + // Iterate through all fuzzy listen contexts + for (NamingFuzzyWatchContext context : fuzzyMatchContextMap.values()) { + // Check if the context is consistent with the server + if (context.isConsistentWithServer()) { + // Skip if a full synchronization is not needed + if (!needAllSync) { + continue; + } else { + context.syncFuzzyWatchers(); + } + } + + needSyncContexts.add(context); + } + + // Execute fuzzy listen operation for addition + doExecuteNamingFuzzyWatch(needSyncContexts); + + // Update last all sync time if a full synchronization was performed + if (needAllSync) { + fuzzyWatchLastAllSyncTime.set(now); + } + } + + public void resetConsistenceStatus() { + fuzzyMatchContextMap.values() + .forEach(fuzzyWatcherContext -> fuzzyWatcherContext.setConsistentWithServer(false)); + } + + /** + * 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 contextLists 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 doExecuteNamingFuzzyWatch(List contextLists) throws NacosException { + // Return if the context map is null or empty + if (CollectionUtils.isEmpty(contextLists)) { + return; + } + + // Iterate through the context map and submit tasks for execution + for (NamingFuzzyWatchContext entry : contextLists) { + // Submit task for execution + NamingFuzzyWatchRequest configFuzzyWatchRequest = buildFuzzyWatchNamingRequest(entry); + try { + + // Execute the fuzzy listen operation + NamingFuzzyWatchResponse listenResponse = namingGrpcClientProxy.fuzzyWatchRequest( + configFuzzyWatchRequest); + if (listenResponse != null && listenResponse.isSuccess()) { + + if (configFuzzyWatchRequest.getWatchType().equals(WATCH_TYPE_CANCEL_WATCH)) { + removePatternMatchCache(entry.getGroupKeyPattern()); + } else { + entry.setConsistentWithServer(true); + } + + } + } catch (NacosException e) { + // Log error and retry after a short delay + LOGGER.error(" fuzzy watch request fail.", e); + try { + Thread.sleep(500L); + } catch (InterruptedException interruptedException) { + // Ignore interruption + } + // Retry notification + notifyFuzzyWatchSync(); + } + } + + } + + private NamingFuzzyWatchRequest buildFuzzyWatchNamingRequest(NamingFuzzyWatchContext namingFuzzyWatchContext) { + NamingFuzzyWatchRequest namingFuzzyWatchRequest = new NamingFuzzyWatchRequest(); + namingFuzzyWatchRequest.setInitializing(namingFuzzyWatchContext.isInitializing()); + namingFuzzyWatchRequest.setNamespace(namingGrpcClientProxy.getNamespaceId()); + namingFuzzyWatchRequest.setReceivedGroupKeys(namingFuzzyWatchContext.getReceivedServiceKeys()); + namingFuzzyWatchRequest.setGroupKeyPattern(namingFuzzyWatchContext.getGroupKeyPattern()); + if (namingFuzzyWatchContext.isDiscard() && namingFuzzyWatchContext.getFuzzyWatchEventWatcherWrappers() + .isEmpty()) { + namingFuzzyWatchRequest.setWatchType(WATCH_TYPE_CANCEL_WATCH); + } else { + namingFuzzyWatchRequest.setWatchType(WATCH_TYPE_WATCH); + } + return namingFuzzyWatchRequest; + } + + public Map getFuzzyMatchContextMap() { + return fuzzyMatchContextMap; + } + + @Override + public void onEvent(NamingFuzzyWatchNotifyEvent event) { + if (!event.scope().equals(notifierEventScope)) { + return; + } + + String changedType = event.getChangedType(); + String syncType = event.getSyncType(); + + String serviceKey = event.getServiceKey(); + String pattern = event.getPattern(); + String watchUuid = event.getWatcherUuid(); + NamingFuzzyWatchContext namingFuzzyWatchContext = fuzzyMatchContextMap.get(pattern); + if (namingFuzzyWatchContext == null) { + return; + } + namingFuzzyWatchContext.notifyFuzzyWatchers(serviceKey, changedType, syncType, watchUuid); + + } + + @Override + public Class subscribeType() { + return NamingFuzzyWatchNotifyEvent.class; + } + + public String getNotifierEventScope() { + return notifierEventScope; + } +} diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/event/NamingFuzzyWatchNotifyEvent.java b/client/src/main/java/com/alibaba/nacos/client/naming/event/NamingFuzzyWatchNotifyEvent.java new file mode 100644 index 00000000000..072215f5bce --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/naming/event/NamingFuzzyWatchNotifyEvent.java @@ -0,0 +1,88 @@ +/* + * 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.naming.event; + +import com.alibaba.nacos.common.notify.Event; + +/** + * Watch notify event, including service change/watch initial. + * + * @author tanyongquan + */ +public class NamingFuzzyWatchNotifyEvent extends Event { + + private final String scope; + + private String watcherUuid; + + private String serviceKey; + + private String pattern; + + private final String changedType; + + private final String syncType; + + private NamingFuzzyWatchNotifyEvent(String scope, String pattern, String serviceKey, String changedType, + String syncType, String watcherUuid) { + this.scope = scope; + this.pattern = pattern; + this.serviceKey = serviceKey; + this.changedType = changedType; + this.syncType = syncType; + this.watcherUuid = watcherUuid; + } + + public static NamingFuzzyWatchNotifyEvent build(String eventScope, String pattern, String serviceKey, + String changedType, String syncType) { + return new NamingFuzzyWatchNotifyEvent(eventScope, pattern, serviceKey, changedType, syncType, null); + } + + public static NamingFuzzyWatchNotifyEvent build(String eventScope, String pattern, String serviceKey, + String changedType, String syncType, String watcherUuid) { + return new NamingFuzzyWatchNotifyEvent(eventScope, pattern, serviceKey, changedType, syncType, watcherUuid); + } + + public String getPattern() { + return pattern; + } + + public String getChangedType() { + return changedType; + } + + @Override + public String scope() { + return this.scope; + } + + public String getWatcherUuid() { + return watcherUuid; + } + + public String getServiceKey() { + return serviceKey; + } + + public String getScope() { + return scope; + } + + public String getSyncType() { + return syncType; + } +} diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegate.java b/client/src/main/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegate.java index efd7bdabf46..bea1ee38ee9 100644 --- a/client/src/main/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegate.java +++ b/client/src/main/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegate.java @@ -25,6 +25,7 @@ import com.alibaba.nacos.api.naming.utils.NamingUtils; import com.alibaba.nacos.api.selector.AbstractSelector; import com.alibaba.nacos.client.env.NacosClientProperties; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchServiceListHolder; import com.alibaba.nacos.client.naming.cache.ServiceInfoHolder; import com.alibaba.nacos.client.naming.core.NamingServerListManager; import com.alibaba.nacos.client.naming.core.ServiceInfoUpdateService; @@ -68,7 +69,8 @@ public class NamingClientProxyDelegate implements NamingClientProxy { private ScheduledExecutorService executorService; public NamingClientProxyDelegate(String namespace, ServiceInfoHolder serviceInfoHolder, - NacosClientProperties properties, InstancesChangeNotifier changeNotifier) throws NacosException { + NacosClientProperties properties, InstancesChangeNotifier changeNotifier, + NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder) throws NacosException { this.serviceInfoUpdateService = new ServiceInfoUpdateService(properties, serviceInfoHolder, this, changeNotifier); this.serverListManager = new NamingServerListManager(properties, namespace); @@ -79,7 +81,7 @@ public NamingClientProxyDelegate(String namespace, ServiceInfoHolder serviceInfo initSecurityProxy(properties); this.httpClientProxy = new NamingHttpClientProxy(namespace, securityProxy, serverListManager, properties); this.grpcClientProxy = new NamingGrpcClientProxy(namespace, securityProxy, serverListManager, properties, - serviceInfoHolder); + serviceInfoHolder, namingFuzzyWatchServiceListHolder); } private void initSecurityProxy(NacosClientProperties properties) { @@ -193,7 +195,8 @@ public boolean serverHealthy() { } private NamingClientProxy getExecuteClientProxy(Instance instance) { - if (instance.isEphemeral() || grpcClientProxy.isAbilitySupportedByServer(AbilityKey.SERVER_SUPPORT_PERSISTENT_INSTANCE_BY_GRPC)) { + if (instance.isEphemeral() || grpcClientProxy.isAbilitySupportedByServer( + AbilityKey.SERVER_SUPPORT_PERSISTENT_INSTANCE_BY_GRPC)) { return grpcClientProxy; } return httpClientProxy; diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingFuzzyWatchNotifyRequestHandler.java b/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingFuzzyWatchNotifyRequestHandler.java new file mode 100644 index 00000000000..aa40d048a01 --- /dev/null +++ b/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingFuzzyWatchNotifyRequestHandler.java @@ -0,0 +1,126 @@ +/* + * 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.client.naming.remote.gprc; + +import com.alibaba.nacos.api.common.Constants; +import com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchChangeNotifyRequest; +import com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchSyncRequest; +import com.alibaba.nacos.api.naming.remote.response.NamingFuzzyWatchChangeNotifyResponse; +import com.alibaba.nacos.api.naming.utils.NamingUtils; +import com.alibaba.nacos.api.remote.request.Request; +import com.alibaba.nacos.api.remote.response.Response; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchContext; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchServiceListHolder; +import com.alibaba.nacos.client.naming.event.NamingFuzzyWatchNotifyEvent; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.remote.client.Connection; +import com.alibaba.nacos.common.remote.client.ServerRequestHandler; +import com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern; + +import java.util.Collection; + +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_RESOURCE_CHANGED; + +/** + * handle fuzzy watch request from server. + * @author shiyiyue + */ +public class NamingFuzzyWatchNotifyRequestHandler implements ServerRequestHandler { + + NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder; + + public NamingFuzzyWatchNotifyRequestHandler(NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder) { + this.namingFuzzyWatchServiceListHolder = namingFuzzyWatchServiceListHolder; + NotifyCenter.registerToPublisher(NamingFuzzyWatchNotifyEvent.class, 1000); + } + + @Override + public Response requestReply(Request request, Connection connection) { + + if (request instanceof NamingFuzzyWatchSyncRequest) { + NamingFuzzyWatchSyncRequest watchNotifySyncRequest = (NamingFuzzyWatchSyncRequest) request; + NamingFuzzyWatchContext namingFuzzyWatchContext = namingFuzzyWatchServiceListHolder.getFuzzyMatchContextMap() + .get(watchNotifySyncRequest.getGroupKeyPattern()); + if (namingFuzzyWatchContext != null) { + Collection serviceKeys = watchNotifySyncRequest.getContexts(); + if (watchNotifySyncRequest.getSyncType().equals(Constants.FUZZY_WATCH_INIT_NOTIFY) + || watchNotifySyncRequest.getSyncType().equals(Constants.FUZZY_WATCH_DIFF_SYNC_NOTIFY)) { + for (NamingFuzzyWatchSyncRequest.Context serviceKey : serviceKeys) { + // may have a 'change event' sent to client before 'init event' + if (namingFuzzyWatchContext.addReceivedServiceKey(serviceKey.getServiceKey())) { + NotifyCenter.publishEvent(NamingFuzzyWatchNotifyEvent.build( + namingFuzzyWatchServiceListHolder.getNotifierEventScope(), + watchNotifySyncRequest.getGroupKeyPattern(), serviceKey.getServiceKey(), + serviceKey.getChangedType(), watchNotifySyncRequest.getSyncType())); + } + } + } else if (watchNotifySyncRequest.getSyncType().equals(Constants.FINISH_FUZZY_WATCH_INIT_NOTIFY)) { + namingFuzzyWatchContext.markInitializationComplete(); + } + } + + return new NamingFuzzyWatchChangeNotifyResponse(); + + } else if (request instanceof NamingFuzzyWatchChangeNotifyRequest) { + NamingFuzzyWatchChangeNotifyRequest notifyChangeRequest = (NamingFuzzyWatchChangeNotifyRequest) request; + String[] serviceKeyItems = NamingUtils.parseServiceKey(notifyChangeRequest.getServiceKey()); + String namespace = serviceKeyItems[0]; + String groupName = serviceKeyItems[1]; + String serviceName = serviceKeyItems[2]; + + Collection matchedPattern = FuzzyGroupKeyPattern.filterMatchedPatterns( + namingFuzzyWatchServiceListHolder.getFuzzyMatchContextMap().keySet(), serviceName, groupName, + namespace); + String serviceChangeType = notifyChangeRequest.getChangedType(); + + switch (serviceChangeType) { + case Constants.ServiceChangedType.ADD_SERVICE: + case Constants.ServiceChangedType.INSTANCE_CHANGED: + for (String pattern : matchedPattern) { + NamingFuzzyWatchContext namingFuzzyWatchContext = namingFuzzyWatchServiceListHolder.getFuzzyMatchContextMap() + .get(pattern); + if (namingFuzzyWatchContext != null && namingFuzzyWatchContext.addReceivedServiceKey( + ((NamingFuzzyWatchChangeNotifyRequest) request).getServiceKey())) { + //publish local service add event + NotifyCenter.publishEvent(NamingFuzzyWatchNotifyEvent.build( + namingFuzzyWatchServiceListHolder.getNotifierEventScope(), pattern, + notifyChangeRequest.getServiceKey(), Constants.ServiceChangedType.ADD_SERVICE, + FUZZY_WATCH_RESOURCE_CHANGED)); + } + } + break; + case Constants.ServiceChangedType.DELETE_SERVICE: + for (String pattern : matchedPattern) { + NamingFuzzyWatchContext namingFuzzyWatchContext = namingFuzzyWatchServiceListHolder.getFuzzyMatchContextMap() + .get(pattern); + if (namingFuzzyWatchContext != null && namingFuzzyWatchContext.removeReceivedServiceKey( + notifyChangeRequest.getServiceKey())) { + NotifyCenter.publishEvent(NamingFuzzyWatchNotifyEvent.build( + namingFuzzyWatchServiceListHolder.getNotifierEventScope(), pattern, + notifyChangeRequest.getServiceKey(), Constants.ServiceChangedType.DELETE_SERVICE, + FUZZY_WATCH_RESOURCE_CHANGED)); + } + } + break; + default: + break; + } + return new NamingFuzzyWatchChangeNotifyResponse(); + } + return null; + } +} diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxy.java b/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxy.java index 508b76f0ab3..29d7e480486 100644 --- a/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxy.java +++ b/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxy.java @@ -29,24 +29,28 @@ import com.alibaba.nacos.api.naming.remote.request.AbstractNamingRequest; import com.alibaba.nacos.api.naming.remote.request.BatchInstanceRequest; import com.alibaba.nacos.api.naming.remote.request.InstanceRequest; +import com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchRequest; import com.alibaba.nacos.api.naming.remote.request.PersistentInstanceRequest; import com.alibaba.nacos.api.naming.remote.request.ServiceListRequest; import com.alibaba.nacos.api.naming.remote.request.ServiceQueryRequest; import com.alibaba.nacos.api.naming.remote.request.SubscribeServiceRequest; import com.alibaba.nacos.api.naming.remote.response.BatchInstanceResponse; +import com.alibaba.nacos.api.naming.remote.response.NamingFuzzyWatchResponse; import com.alibaba.nacos.api.naming.remote.response.QueryServiceResponse; import com.alibaba.nacos.api.naming.remote.response.ServiceListResponse; import com.alibaba.nacos.api.naming.remote.response.SubscribeServiceResponse; import com.alibaba.nacos.api.naming.utils.NamingUtils; import com.alibaba.nacos.api.remote.RemoteConstants; +import com.alibaba.nacos.api.remote.request.Request; import com.alibaba.nacos.api.remote.response.Response; import com.alibaba.nacos.api.remote.response.ResponseCode; import com.alibaba.nacos.api.selector.AbstractSelector; import com.alibaba.nacos.api.selector.SelectorType; +import com.alibaba.nacos.client.address.ServerListChangeEvent; import com.alibaba.nacos.client.env.NacosClientProperties; import com.alibaba.nacos.client.monitor.MetricsMonitor; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchServiceListHolder; import com.alibaba.nacos.client.naming.cache.ServiceInfoHolder; -import com.alibaba.nacos.client.address.ServerListChangeEvent; import com.alibaba.nacos.client.naming.remote.AbstractNamingClientProxy; import com.alibaba.nacos.client.naming.remote.gprc.redo.NamingGrpcRedoService; import com.alibaba.nacos.client.naming.remote.gprc.redo.data.BatchInstanceRedoData; @@ -94,7 +98,8 @@ public class NamingGrpcClientProxy extends AbstractNamingClientProxy { private final NamingGrpcRedoService redoService; public NamingGrpcClientProxy(String namespaceId, SecurityProxy securityProxy, ServerListFactory serverListFactory, - NacosClientProperties properties, ServiceInfoHolder serviceInfoHolder) throws NacosException { + NacosClientProperties properties, ServiceInfoHolder serviceInfoHolder, + NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder) throws NacosException { super(securityProxy); this.namespaceId = namespaceId; this.uuid = UUID.randomUUID().toString(); @@ -103,18 +108,23 @@ public NamingGrpcClientProxy(String namespaceId, SecurityProxy securityProxy, Se labels.put(RemoteConstants.LABEL_SOURCE, RemoteConstants.LABEL_SOURCE_SDK); labels.put(RemoteConstants.LABEL_MODULE, RemoteConstants.LABEL_MODULE_NAMING); labels.put(Constants.APPNAME, AppNameUtils.getAppName()); + namingFuzzyWatchServiceListHolder.registerNamingGrpcClientProxy(this); this.rpcClient = RpcClientFactory.createClient(uuid, ConnectionType.GRPC, labels, RpcClientTlsConfigFactory.getInstance().createSdkConfig(properties.asProperties())); - this.redoService = new NamingGrpcRedoService(this, properties); + this.redoService = new NamingGrpcRedoService(this, namingFuzzyWatchServiceListHolder, properties); NAMING_LOGGER.info("Create naming rpc client for uuid->{}", uuid); - start(serverListFactory, serviceInfoHolder); + start(serverListFactory, serviceInfoHolder, namingFuzzyWatchServiceListHolder); } - private void start(ServerListFactory serverListFactory, ServiceInfoHolder serviceInfoHolder) throws NacosException { + private void start(ServerListFactory serverListFactory, ServiceInfoHolder serviceInfoHolder, + NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder) throws NacosException { rpcClient.serverListFactory(serverListFactory); rpcClient.registerConnectionListener(redoService); rpcClient.registerServerRequestHandler(new NamingPushRequestHandler(serviceInfoHolder)); + rpcClient.registerServerRequestHandler( + new NamingFuzzyWatchNotifyRequestHandler(namingFuzzyWatchServiceListHolder)); rpcClient.start(); + namingFuzzyWatchServiceListHolder.start(); NotifyCenter.registerSubscriber(this); } @@ -268,9 +278,8 @@ public void doRegisterServiceForPersistent(String serviceName, String groupName, @Override public void deregisterService(String serviceName, String groupName, Instance instance) throws NacosException { - NAMING_LOGGER - .info("[DEREGISTER-SERVICE] {} deregistering service {} with instance: {}", namespaceId, serviceName, - instance); + NAMING_LOGGER.info("[DEREGISTER-SERVICE] {} deregistering service {} with instance: {}", namespaceId, + serviceName, instance); if (instance.isEphemeral()) { deregisterServiceForEphemeral(serviceName, groupName, instance); } else { @@ -438,12 +447,31 @@ public boolean isAbilitySupportedByServer(AbilityKey abilityKey) { return rpcClient.getConnectionAbility(abilityKey) == AbilityStatus.SUPPORTED; } - private T requestToServer(AbstractNamingRequest request, Class responseClass) + /** + * Execute unsubscribe operation. + * + * @param namingFuzzyWatchRequest namingFuzzyWatchRequest + * @throws NacosException nacos exception + */ + public NamingFuzzyWatchResponse fuzzyWatchRequest(NamingFuzzyWatchRequest namingFuzzyWatchRequest) throws NacosException { + return requestToServer(namingFuzzyWatchRequest, NamingFuzzyWatchResponse.class); + } + + private T requestToServer(Request request, Class responseClass) throws NacosException { Response response = null; try { - request.putAllHeader( - getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName())); + if (request instanceof AbstractNamingRequest) { + request.putAllHeader(getSecurityHeaders(((AbstractNamingRequest) request).getNamespace(), + ((AbstractNamingRequest) request).getGroupName(), + ((AbstractNamingRequest) request).getServiceName())); + } else if (request instanceof NamingFuzzyWatchRequest) { + request.putAllHeader( + getSecurityHeaders(((NamingFuzzyWatchRequest) request).getNamespace(), null, null)); + } else { + throw new NacosException(400, "unknown naming request type"); + } + response = requestTimeout < 0 ? rpcClient.request(request) : rpcClient.request(request, requestTimeout); if (ResponseCode.SUCCESS.getCode() != response.getResultCode()) { // If the 403 login operation is triggered, refresh the accessToken of the client @@ -475,7 +503,7 @@ private T requestToServer(AbstractNamingRequest request, Cl * successful. * @param response The response object containing registration result information, or null if registration failed. */ - private void recordRequestFailedMetrics(AbstractNamingRequest request, Exception exception, Response response) { + private void recordRequestFailedMetrics(Request request, Exception exception, Response response) { if (Objects.isNull(response)) { MetricsMonitor.getNamingRequestFailedMonitor(request.getClass().getSimpleName(), MONITOR_LABEL_NONE, MONITOR_LABEL_NONE, exception.getClass().getSimpleName()).inc(); @@ -508,4 +536,8 @@ private void shutDownAndRemove(String uuid) { public boolean isEnable() { return rpcClient.isRunning(); } + + public String getNamespaceId() { + return namespaceId; + } } diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/redo/NamingGrpcRedoService.java b/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/redo/NamingGrpcRedoService.java index aad883ddabe..7527b9d00ce 100644 --- a/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/redo/NamingGrpcRedoService.java +++ b/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/redo/NamingGrpcRedoService.java @@ -22,6 +22,7 @@ import com.alibaba.nacos.api.naming.pojo.ServiceInfo; import com.alibaba.nacos.api.naming.utils.NamingUtils; import com.alibaba.nacos.client.env.NacosClientProperties; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchServiceListHolder; import com.alibaba.nacos.client.naming.remote.gprc.NamingGrpcClientProxy; import com.alibaba.nacos.client.naming.remote.gprc.redo.data.BatchInstanceRedoData; import com.alibaba.nacos.client.naming.remote.gprc.redo.data.InstanceRedoData; @@ -59,12 +60,16 @@ public class NamingGrpcRedoService implements ConnectionEventListener { private final ConcurrentMap subscribes = new ConcurrentHashMap<>(); + private final NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder; + private final ScheduledExecutorService redoExecutor; private volatile boolean connected = false; - public NamingGrpcRedoService(NamingGrpcClientProxy clientProxy, NacosClientProperties properties) { + public NamingGrpcRedoService(NamingGrpcClientProxy clientProxy, + NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder, NacosClientProperties properties) { setProperties(properties); + this.namingFuzzyWatchServiceListHolder = namingFuzzyWatchServiceListHolder; this.redoExecutor = new ScheduledThreadPoolExecutor(redoThreadCount, new NameThreadFactory(REDO_THREAD_NAME)); this.redoExecutor.scheduleWithFixedDelay(new RedoScheduledTask(clientProxy, this), redoDelayTime, redoDelayTime, TimeUnit.MILLISECONDS); @@ -100,6 +105,9 @@ public void onDisConnect(Connection connection) { synchronized (subscribes) { subscribes.values().forEach(subscriberRedoData -> subscriberRedoData.setRegistered(false)); } + synchronized (namingFuzzyWatchServiceListHolder) { + namingFuzzyWatchServiceListHolder.resetConsistenceStatus(); + } LogUtils.NAMING_LOGGER.warn("mark to redo completed"); } diff --git a/client/src/main/resources/META-INF/native-image/com.alibaba.nacos/nacos-client/reflect-config.json b/client/src/main/resources/META-INF/native-image/com.alibaba.nacos/nacos-client/reflect-config.json index 3734fbb2843..da2ba912161 100644 --- a/client/src/main/resources/META-INF/native-image/com.alibaba.nacos/nacos-client/reflect-config.json +++ b/client/src/main/resources/META-INF/native-image/com.alibaba.nacos/nacos-client/reflect-config.json @@ -535,14 +535,23 @@ {"name":"getServiceName","parameterTypes":[] } ] }, +{ + "name":"com.alibaba.nacos.api.naming.remote.request.AbstractFuzzyWatchNotifyRequest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[ {"name":"getInstances","parameterTypes":[] }, + {"name":"getType","parameterTypes":[] }] +}, { "name":"com.alibaba.nacos.api.naming.remote.request.BatchInstanceRequest", "allDeclaredFields":true, "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true, "methods":[ - {"name":"getInstances","parameterTypes":[] }, - {"name":"getType","parameterTypes":[] } + {"name":"","parameterTypes":[] }, + {"name":"getModule","parameterTypes":[] }, + {"name":"getServiceChangedType","parameterTypes":[] } ] }, { @@ -603,6 +612,28 @@ {"name":"isHealthyOnly","parameterTypes":[] } ] }, +{ + "name":"com.alibaba.nacos.api.naming.remote.request.FuzzyWatchNotifyChangeRequest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[ + {"name":"","parameterTypes":[] }, + {"name":"getServiceName","parameterTypes":[] }, + {"name":"getGroupName","parameterTypes":[] } + ] +}, +{ + "name":"com.alibaba.nacos.api.naming.remote.request.FuzzyWatchNotifyInitRequest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[ + {"name":"","parameterTypes":[] }, + {"name":"getServicesName","parameterTypes":[] }, + {"name":"getPattern","parameterTypes":[] } + ] +}, { "name":"com.alibaba.nacos.api.naming.remote.request.SubscribeServiceRequest", "allDeclaredFields":true, 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 7e56dde22d4..6dcc4e368cf 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 @@ -39,6 +39,7 @@ import java.lang.reflect.Field; import java.util.Arrays; +import java.util.Collections; import java.util.Properties; import java.util.concurrent.Executor; @@ -98,10 +99,12 @@ void testGetConfigFromFailOver() throws NacosException { final String group = "2"; final String tenant = "public"; - MockedStatic localConfigInfoProcessorMockedStatic = Mockito.mockStatic(LocalConfigInfoProcessor.class); + MockedStatic localConfigInfoProcessorMockedStatic = Mockito.mockStatic( + LocalConfigInfoProcessor.class); try { String contentFailOver = "failOverContent" + System.currentTimeMillis(); - localConfigInfoProcessorMockedStatic.when(() -> LocalConfigInfoProcessor.getFailover(any(), eq(dataId), eq(group), eq(tenant))) + localConfigInfoProcessorMockedStatic.when( + () -> LocalConfigInfoProcessor.getFailover(any(), eq(dataId), eq(group), eq(tenant))) .thenReturn(contentFailOver); final int timeout = 3000; @@ -118,18 +121,22 @@ void testGetConfigFromLocalCache() throws NacosException { final String group = "2"; final String tenant = "public"; - MockedStatic localConfigInfoProcessorMockedStatic = Mockito.mockStatic(LocalConfigInfoProcessor.class); + MockedStatic localConfigInfoProcessorMockedStatic = Mockito.mockStatic( + LocalConfigInfoProcessor.class); try { String contentFailOver = "localCacheContent" + System.currentTimeMillis(); //fail over null - localConfigInfoProcessorMockedStatic.when(() -> LocalConfigInfoProcessor.getFailover(any(), eq(dataId), eq(group), eq(tenant))) + localConfigInfoProcessorMockedStatic.when( + () -> LocalConfigInfoProcessor.getFailover(any(), eq(dataId), eq(group), eq(tenant))) .thenReturn(null); //snapshot content - localConfigInfoProcessorMockedStatic.when(() -> LocalConfigInfoProcessor.getSnapshot(any(), eq(dataId), eq(group), eq(tenant))) + localConfigInfoProcessorMockedStatic.when( + () -> LocalConfigInfoProcessor.getSnapshot(any(), eq(dataId), eq(group), eq(tenant))) .thenReturn(contentFailOver); //form server error. final int timeout = 3000; - Mockito.when(mockWoker.getServerConfig(dataId, group, tenant, timeout, false)).thenThrow(new NacosException()); + Mockito.when(mockWoker.getServerConfig(dataId, group, tenant, timeout, false)) + .thenThrow(new NacosException()); final String config = nacosConfigService.getConfig(dataId, group, timeout); assertEquals(contentFailOver, config); @@ -145,10 +152,12 @@ void testGetConfig403() throws NacosException { final String group = "2"; final String tenant = "public"; - MockedStatic localConfigInfoProcessorMockedStatic = Mockito.mockStatic(LocalConfigInfoProcessor.class); + MockedStatic localConfigInfoProcessorMockedStatic = Mockito.mockStatic( + LocalConfigInfoProcessor.class); try { //fail over null - localConfigInfoProcessorMockedStatic.when(() -> LocalConfigInfoProcessor.getFailover(any(), eq(dataId), eq(group), eq(tenant))) + localConfigInfoProcessorMockedStatic.when( + () -> LocalConfigInfoProcessor.getFailover(any(), eq(dataId), eq(group), eq(tenant))) .thenReturn(null); //form server error. @@ -217,8 +226,8 @@ public void removeCache(String dataId, String group) { } @Override - public ConfigResponse queryConfig(String dataId, String group, String tenant, long readTimeous, boolean notify) - throws NacosException { + public ConfigResponse queryConfig(String dataId, String group, String tenant, long readTimeous, + boolean notify) throws NacosException { ConfigResponse configResponse = new ConfigResponse(); configResponse.setContent(content); configResponse.setDataId(dataId); @@ -228,8 +237,9 @@ public ConfigResponse queryConfig(String dataId, String group, String tenant, lo } @Override - public boolean publishConfig(String dataId, String group, String tenant, String appName, String tag, String betaIps, - String content, String encryptedDataKey, String casMd5, String type) throws NacosException { + public boolean publishConfig(String dataId, String group, String tenant, String appName, String tag, + String betaIps, String content, String encryptedDataKey, String casMd5, String type) + throws NacosException { return false; } @@ -243,7 +253,12 @@ public boolean removeConfig(String dataId, String group, String tenant, String t final String config = nacosConfigService.getConfigAndSignListener(dataId, group, timeout, listener); assertEquals(content, config); - Mockito.verify(mockWoker, Mockito.times(1)).addTenantListenersWithContent(dataId, group, content, null, Arrays.asList(listener)); + 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)); } @Test @@ -273,12 +288,14 @@ void testPublishConfig() throws NacosException { String content = "123"; String namespace = "public"; String type = ConfigType.getDefaultType().getType(); - Mockito.when(mockWoker.publishConfig(dataId, group, namespace, null, null, null, content, "", null, type)).thenReturn(true); + Mockito.when(mockWoker.publishConfig(dataId, group, namespace, null, null, null, content, "", null, type)) + .thenReturn(true); final boolean b = nacosConfigService.publishConfig(dataId, group, content); assertTrue(b); - Mockito.verify(mockWoker, Mockito.times(1)).publishConfig(dataId, group, namespace, null, null, null, content, "", null, type); + Mockito.verify(mockWoker, Mockito.times(1)) + .publishConfig(dataId, group, namespace, null, null, null, content, "", null, type); } @Test @@ -289,12 +306,14 @@ void testPublishConfig2() throws NacosException { String namespace = "public"; String type = ConfigType.PROPERTIES.getType(); - Mockito.when(mockWoker.publishConfig(dataId, group, namespace, null, null, null, content, "", null, type)).thenReturn(true); + Mockito.when(mockWoker.publishConfig(dataId, group, namespace, null, null, null, content, "", null, type)) + .thenReturn(true); final boolean b = nacosConfigService.publishConfig(dataId, group, content, type); assertTrue(b); - Mockito.verify(mockWoker, Mockito.times(1)).publishConfig(dataId, group, namespace, null, null, null, content, "", null, type); + Mockito.verify(mockWoker, Mockito.times(1)) + .publishConfig(dataId, group, namespace, null, null, null, content, "", null, type); } @Test @@ -306,12 +325,14 @@ void testPublishConfigCas() throws NacosException { String casMd5 = "96147704e3cb8be8597d55d75d244a02"; String type = ConfigType.getDefaultType().getType(); - Mockito.when(mockWoker.publishConfig(dataId, group, namespace, null, null, null, content, "", casMd5, type)).thenReturn(true); + Mockito.when(mockWoker.publishConfig(dataId, group, namespace, null, null, null, content, "", casMd5, type)) + .thenReturn(true); final boolean b = nacosConfigService.publishConfigCas(dataId, group, content, casMd5); assertTrue(b); - Mockito.verify(mockWoker, Mockito.times(1)).publishConfig(dataId, group, namespace, null, null, null, content, "", casMd5, type); + Mockito.verify(mockWoker, Mockito.times(1)) + .publishConfig(dataId, group, namespace, null, null, null, content, "", casMd5, type); } @Test @@ -323,12 +344,14 @@ void testPublishConfigCas2() throws NacosException { String casMd5 = "96147704e3cb8be8597d55d75d244a02"; String type = ConfigType.PROPERTIES.getType(); - Mockito.when(mockWoker.publishConfig(dataId, group, namespace, null, null, null, content, "", casMd5, type)).thenReturn(true); + Mockito.when(mockWoker.publishConfig(dataId, group, namespace, null, null, null, content, "", casMd5, type)) + .thenReturn(true); final boolean b = nacosConfigService.publishConfigCas(dataId, group, content, casMd5, type); assertTrue(b); - Mockito.verify(mockWoker, Mockito.times(1)).publishConfig(dataId, group, namespace, null, null, null, content, "", casMd5, type); + Mockito.verify(mockWoker, Mockito.times(1)) + .publishConfig(dataId, group, namespace, null, null, null, content, "", casMd5, type); } @Test @@ -383,4 +406,4 @@ void testShutDown() { nacosConfigService.shutDown(); }); } -} \ No newline at end of file +} diff --git a/client/src/test/java/com/alibaba/nacos/client/naming/remote/AbstractNamingClientProxyTest.java b/client/src/test/java/com/alibaba/nacos/client/naming/remote/AbstractNamingClientProxyTest.java index 7f570c2780b..cba87c980c2 100644 --- a/client/src/test/java/com/alibaba/nacos/client/naming/remote/AbstractNamingClientProxyTest.java +++ b/client/src/test/java/com/alibaba/nacos/client/naming/remote/AbstractNamingClientProxyTest.java @@ -25,8 +25,8 @@ import com.alibaba.nacos.api.naming.pojo.Service; import com.alibaba.nacos.api.naming.pojo.ServiceInfo; import com.alibaba.nacos.api.selector.AbstractSelector; -import com.alibaba.nacos.client.auth.ram.utils.SignUtil; import com.alibaba.nacos.client.address.ServerListChangeEvent; +import com.alibaba.nacos.client.auth.ram.utils.SignUtil; import com.alibaba.nacos.client.security.SecurityProxy; import com.alibaba.nacos.client.utils.AppNameUtils; import com.alibaba.nacos.common.notify.Event; diff --git a/client/src/test/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegateTest.java b/client/src/test/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegateTest.java index bdc3ac6cec1..7e1031cba34 100644 --- a/client/src/test/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegateTest.java +++ b/client/src/test/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegateTest.java @@ -27,6 +27,7 @@ import com.alibaba.nacos.api.selector.ExpressionSelector; import com.alibaba.nacos.api.selector.NoneSelector; import com.alibaba.nacos.client.env.NacosClientProperties; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchServiceListHolder; import com.alibaba.nacos.client.naming.cache.ServiceInfoHolder; import com.alibaba.nacos.client.naming.event.InstancesChangeNotifier; import com.alibaba.nacos.client.naming.remote.gprc.NamingGrpcClientProxy; @@ -65,6 +66,9 @@ class NamingClientProxyDelegateTest { @Mock NamingGrpcClientProxy mockGrpcClient; + @Mock + NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder; + NamingClientProxyDelegate delegate; InstancesChangeNotifier notifier; @@ -77,7 +81,8 @@ void setUp() throws NacosException, NoSuchFieldException, IllegalAccessException props.setProperty("serverAddr", "localhost"); nacosClientProperties = NacosClientProperties.PROTOTYPE.derive(props); notifier = new InstancesChangeNotifier(); - delegate = new NamingClientProxyDelegate(TEST_NAMESPACE, holder, nacosClientProperties, notifier); + delegate = new NamingClientProxyDelegate(TEST_NAMESPACE, holder, nacosClientProperties, notifier, + namingFuzzyWatchServiceListHolder); Field grpcClientProxyField = NamingClientProxyDelegate.class.getDeclaredField("grpcClientProxy"); grpcClientProxyField.setAccessible(true); grpcClientProxyField.set(delegate, mockGrpcClient); diff --git a/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxyTest.java b/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxyTest.java index bdb9472cfbc..0efeba8f6db 100644 --- a/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxyTest.java +++ b/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxyTest.java @@ -47,6 +47,7 @@ import com.alibaba.nacos.api.selector.NoneSelector; import com.alibaba.nacos.client.env.NacosClientProperties; import com.alibaba.nacos.client.naming.cache.ServiceInfoHolder; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchServiceListHolder; import com.alibaba.nacos.client.address.ServerListChangeEvent; import com.alibaba.nacos.client.naming.remote.gprc.redo.NamingGrpcRedoService; import com.alibaba.nacos.client.security.SecurityProxy; @@ -120,6 +121,9 @@ class NamingGrpcClientProxyTest { @Mock private ServiceInfoHolder holder; + @Mock + private NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder; + @Mock private RpcClient rpcClient; @@ -145,7 +149,8 @@ void setUp() throws NacosException, NoSuchFieldException, IllegalAccessException prop = new Properties(); final NacosClientProperties nacosClientProperties = NacosClientProperties.PROTOTYPE.derive(prop); - client = new NamingGrpcClientProxy(NAMESPACE_ID, proxy, factory, nacosClientProperties, holder); + client = new NamingGrpcClientProxy(NAMESPACE_ID, proxy, factory, nacosClientProperties, holder, + namingFuzzyWatchServiceListHolder); Field uuidField = NamingGrpcClientProxy.class.getDeclaredField("uuid"); uuidField.setAccessible(true); @@ -699,7 +704,8 @@ public void close() { @Test void testConfigAppNameLabels() throws Exception { final NacosClientProperties nacosClientProperties = NacosClientProperties.PROTOTYPE.derive(prop); - client = new NamingGrpcClientProxy(NAMESPACE_ID, proxy, factory, nacosClientProperties, holder); + client = new NamingGrpcClientProxy(NAMESPACE_ID, proxy, factory, nacosClientProperties, holder, + namingFuzzyWatchServiceListHolder); Field rpcClientField = NamingGrpcClientProxy.class.getDeclaredField("rpcClient"); rpcClientField.setAccessible(true); RpcClient rpcClient = (RpcClient) rpcClientField.get(client); diff --git a/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/NamingPushRequestHandlerTest.java b/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/NamingPushRequestHandlerTest.java index 341c26e1dcd..40855f67ae6 100644 --- a/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/NamingPushRequestHandlerTest.java +++ b/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/NamingPushRequestHandlerTest.java @@ -25,6 +25,7 @@ import com.alibaba.nacos.api.remote.request.Request; import com.alibaba.nacos.api.remote.response.Response; import com.alibaba.nacos.client.naming.cache.ServiceInfoHolder; + import com.alibaba.nacos.client.naming.remote.TestConnection; import com.alibaba.nacos.common.remote.client.RpcClient; import org.junit.jupiter.api.Test; @@ -54,6 +55,7 @@ void testRequestReply() { @Test void testRequestReplyOtherType() { ServiceInfoHolder holder = mock(ServiceInfoHolder.class); + NamingPushRequestHandler handler = new NamingPushRequestHandler(holder); assertNull(handler.requestReply(new HealthCheckRequest(), new TestConnection(new RpcClient.ServerInfo()))); } diff --git a/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/redo/NamingGrpcRedoServiceTest.java b/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/redo/NamingGrpcRedoServiceTest.java index 61c31fc51ef..60fe8c8eab8 100644 --- a/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/redo/NamingGrpcRedoServiceTest.java +++ b/client/src/test/java/com/alibaba/nacos/client/naming/remote/gprc/redo/NamingGrpcRedoServiceTest.java @@ -19,6 +19,7 @@ import com.alibaba.nacos.api.PropertyKeyConst; import com.alibaba.nacos.api.naming.pojo.Instance; import com.alibaba.nacos.client.env.NacosClientProperties; +import com.alibaba.nacos.client.naming.cache.NamingFuzzyWatchServiceListHolder; import com.alibaba.nacos.client.naming.remote.TestConnection; import com.alibaba.nacos.client.naming.remote.gprc.NamingGrpcClientProxy; import com.alibaba.nacos.client.naming.remote.gprc.redo.data.BatchInstanceRedoData; @@ -56,13 +57,16 @@ class NamingGrpcRedoServiceTest { @Mock private NamingGrpcClientProxy clientProxy; + @Mock + private NamingFuzzyWatchServiceListHolder namingFuzzyWatchServiceListHolder; + private NamingGrpcRedoService redoService; @BeforeEach void setUp() throws Exception { Properties prop = new Properties(); NacosClientProperties nacosClientProperties = NacosClientProperties.PROTOTYPE.derive(prop); - redoService = new NamingGrpcRedoService(clientProxy, nacosClientProperties); + redoService = new NamingGrpcRedoService(clientProxy, namingFuzzyWatchServiceListHolder, nacosClientProperties); ScheduledExecutorService redoExecutor = (ScheduledExecutorService) ReflectUtils.getFieldValue(redoService, "redoExecutor"); redoExecutor.shutdownNow(); @@ -95,7 +99,7 @@ void testCustomProperties() throws Exception { prop.setProperty(PropertyKeyConst.REDO_DELAY_THREAD_COUNT, "2"); NacosClientProperties nacosClientProperties = NacosClientProperties.PROTOTYPE.derive(prop); - NamingGrpcRedoService redoService = new NamingGrpcRedoService(clientProxy, nacosClientProperties); + NamingGrpcRedoService redoService = new NamingGrpcRedoService(clientProxy, namingFuzzyWatchServiceListHolder, nacosClientProperties); Field redoThreadCountField = NamingGrpcRedoService.class.getDeclaredField("redoThreadCount"); redoThreadCountField.setAccessible(true); diff --git a/common/src/main/java/com/alibaba/nacos/common/task/BatchTaskCounter.java b/common/src/main/java/com/alibaba/nacos/common/task/BatchTaskCounter.java new file mode 100644 index 00000000000..1d049d14cb0 --- /dev/null +++ b/common/src/main/java/com/alibaba/nacos/common/task/BatchTaskCounter.java @@ -0,0 +1,73 @@ +/* + * 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.task; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * batch task counter. + * + * @author shiyiyue + */ +public class BatchTaskCounter { + + List batchCounter; + + public BatchTaskCounter(int totalBatch) { + initBatchCounter(totalBatch); + } + + /** + * init counter. + * @param totalBatch totalBatch. + */ + private void initBatchCounter(int totalBatch) { + batchCounter = new ArrayList<>(totalBatch); + for (int i = 0; i < totalBatch; i++) { + batchCounter.add(i, new AtomicBoolean(false)); + } + } + + /** + * set bath succeed. + * @param batch succeed batch. + */ + public void batchSuccess(int batch) { + if (batch <= batchCounter.size()) { + batchCounter.get(batch - 1).set(true); + } + } + + /** + * check all completed. + * @return + */ + public boolean batchCompleted() { + for (AtomicBoolean atomicBoolean : batchCounter) { + if (!atomicBoolean.get()) { + return false; + } + } + return true; + } + + public int getTotalBatch() { + return batchCounter.size(); + } +} diff --git a/common/src/main/java/com/alibaba/nacos/common/utils/FuzzyGroupKeyPattern.java b/common/src/main/java/com/alibaba/nacos/common/utils/FuzzyGroupKeyPattern.java new file mode 100644 index 00000000000..fae7accbd90 --- /dev/null +++ b/common/src/main/java/com/alibaba/nacos/common/utils/FuzzyGroupKeyPattern.java @@ -0,0 +1,240 @@ +/* + * 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 java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.alibaba.nacos.api.common.Constants.ALL_PATTERN; +import static com.alibaba.nacos.api.common.Constants.DEFAULT_NAMESPACE_ID; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_PATTERN_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 FuzzyGroupKeyPattern { + + /** + * Generates a fuzzy listen group key pattern based on the given dataId pattern, group, and optional tenant. pattern + * result as: fixNamespace>>groupPattern>>dataIdPattern + * + * @param resourcePattern The pattern for matching dataIds or service names. + * @param groupPattern The groupPattern associated with the groups. + * @param fixNamespace (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 generatePattern(final String resourcePattern, final String groupPattern, String fixNamespace) { + if (StringUtils.isBlank(resourcePattern)) { + throw new IllegalArgumentException("Param 'resourcePattern' is illegal, resourcePattern is blank"); + } + if (StringUtils.isBlank(groupPattern)) { + throw new IllegalArgumentException("Param 'groupPattern' is illegal, group is blank"); + } + if (StringUtils.isBlank(fixNamespace)) { + fixNamespace = DEFAULT_NAMESPACE_ID; + } + StringBuilder sb = new StringBuilder(); + sb.append(fixNamespace); + sb.append(FUZZY_WATCH_PATTERN_SPLITTER); + sb.append(groupPattern); + sb.append(FUZZY_WATCH_PATTERN_SPLITTER); + sb.append(resourcePattern); + return sb.toString().intern(); + } + + /** + * Given a dataId, group, and a collection of completed group key patterns, returns the patterns that match. + * + * @param resourceName The dataId or service name to match. + * @param group The group to match. + * @param namespace 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 filterMatchedPatterns(Collection groupKeyPatterns, String resourceName, + String group, String namespace) { + if (CollectionUtils.isEmpty(groupKeyPatterns)) { + return new HashSet<>(1); + } + Set matchedPatternList = new HashSet<>(); + for (String keyPattern : groupKeyPatterns) { + if (matchPattern(keyPattern, resourceName, group, namespace)) { + matchedPatternList.add(keyPattern); + } + } + return matchedPatternList; + } + + /** + * check if the resource match the groupKeyPattern. + * @param resourceName The dataId or service name to match. + * @param group The group to match. + * @param namespace The group to match. + * @param groupKeyPattern The pattern to match. + * @return matched or not. + */ + public static boolean matchPattern(String groupKeyPattern, String resourceName, String group, String namespace) { + if (StringUtils.isBlank(namespace)) { + namespace = DEFAULT_NAMESPACE_ID; + } + String[] splitPatterns = groupKeyPattern.split(FUZZY_WATCH_PATTERN_SPLITTER); + return splitPatterns[0].equals(namespace) && itemMatched(splitPatterns[1], group) && itemMatched( + splitPatterns[2], resourceName); + } + + public static String getNamespaceFromPattern(String groupKeyPattern) { + return groupKeyPattern.split(FUZZY_WATCH_PATTERN_SPLITTER)[0]; + } + + /** + * check pattern matched the resource. + * @param pattern pattern contain *. + * @param resource resource to check. + * @return + */ + private static boolean itemMatched(String pattern, String resource) { + + //accurate match without * + if (!pattern.contains(ALL_PATTERN)) { + return pattern.equals(resource); + } + + //match for '*' pattern + if (pattern.equals(ALL_PATTERN)) { + return true; + } + + //match for *{string}* + if (pattern.startsWith(ALL_PATTERN) && pattern.endsWith(ALL_PATTERN)) { + String pureString = pattern.replace(ALL_PATTERN, ""); + return resource.contains(pureString); + } + + //match for postfix match *{string} + if (pattern.startsWith(ALL_PATTERN)) { + String pureString = pattern.replace(ALL_PATTERN, ""); + return resource.endsWith(pureString); + } + + //match for prefix match {string}* + if (pattern.endsWith(ALL_PATTERN)) { + String pureString = pattern.replace(ALL_PATTERN, ""); + return resource.startsWith(pureString); + } + + return false; + } + + /** + * Calculates and merges the differences between the matched group keys and the client's existing group keys into a + * list of ConfigState objects. + * + * @param basedGroupKeys The matched group keys set + * @param followedGroupKeys The followed existing group keys set + * @return a different list of GroupKeyState objects representing the states which the followed sets should be added + * or removed GroupKeyState#exist true presents follow set should add,GroupKeyState#exist false presents follow set + * should removed. + */ + public static List diffGroupKeys(Set basedGroupKeys, Set followedGroupKeys) { + // Calculate the set of group keys to be added and removed + Set addGroupKeys = new HashSet<>(); + if (CollectionUtils.isNotEmpty(basedGroupKeys)) { + addGroupKeys.addAll(basedGroupKeys); + } + if (CollectionUtils.isNotEmpty(followedGroupKeys)) { + addGroupKeys.removeAll(followedGroupKeys); + } + + Set removeGroupKeys = new HashSet<>(); + if (CollectionUtils.isNotEmpty(followedGroupKeys)) { + removeGroupKeys.addAll(followedGroupKeys); + } + if (CollectionUtils.isNotEmpty(basedGroupKeys)) { + removeGroupKeys.removeAll(basedGroupKeys); + } + + // 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 GroupKeyState(groupKey, true)), + removeGroupKeys.stream().map(groupKey -> new GroupKeyState(groupKey, false))) + .collect(Collectors.toList()); + } + + public static class GroupKeyState { + + String groupKey; + + 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 GroupKeyState(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; + } + } +} diff --git a/common/src/test/java/com/alibaba/nacos/common/utils/FuzzyGroupKeyPatternTest.java b/common/src/test/java/com/alibaba/nacos/common/utils/FuzzyGroupKeyPatternTest.java new file mode 100644 index 00000000000..374d429eac0 --- /dev/null +++ b/common/src/test/java/com/alibaba/nacos/common/utils/FuzzyGroupKeyPatternTest.java @@ -0,0 +1,43 @@ +/* + * 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.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * GroupKeyPatternUtilsTest. + * + * @author stone-98 + * @date 2024/3/19 + */ +public class FuzzyGroupKeyPatternTest { + + @Test + public void testGetGroupKeyPattern() { + String dataIdPattern = "examplePattern*"; + String group = "exampleGroup"; + String namespace = "exampleNamespace"; + + String groupKeyPattern = FuzzyGroupKeyPattern.generatePattern(dataIdPattern, group, namespace); + + assertEquals("exampleNamespace>>exampleGroup@@examplePattern*", groupKeyPattern); + } + +} + diff --git a/config/pom.xml b/config/pom.xml index eeaaf2f0d3d..2e80019da5d 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -141,6 +141,10 @@ hamcrest test + + com.alibaba.nacos + nacos-control-plugin + 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 ac6e286877d..b6af4a6f805 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,10 @@ public class ConfigCommonConfig extends AbstractDynamicConfig { private int maxPushRetryTimes = 50; + private long pushTimeout = 3000L; + + private int batchSize = 20; + private boolean derbyOpsEnabled = false; private ConfigCommonConfig() { @@ -52,6 +56,22 @@ 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 +83,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); + batchSize = EnvUtil.getProperty("nacos.config.push.batchSize", Integer.class, 20); derbyOpsEnabled = EnvUtil.getProperty("nacos.config.derby.ops.enabled", Boolean.class, false); } diff --git a/config/src/main/java/com/alibaba/nacos/config/server/model/event/ConfigCancelFuzzyWatchEvent.java b/config/src/main/java/com/alibaba/nacos/config/server/model/event/ConfigCancelFuzzyWatchEvent.java new file mode 100644 index 00000000000..77fbace8cc4 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/model/event/ConfigCancelFuzzyWatchEvent.java @@ -0,0 +1,76 @@ +/* + * 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; + +/** + * 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 ConfigCancelFuzzyWatchEvent extends Event { + + private static final long serialVersionUID = 1953965691384930209L; + + /** + * ID of the client making the request. + */ + private String connectionId; + + /** + * Pattern for matching group keys. + */ + private String groupKeyPattern; + + /** + * Constructs a new ConfigBatchFuzzyListenEvent with the specified parameters. + * + * @param connectionId ID of the client making the request + * @param groupKeyPattern Pattern for matching group keys + */ + public ConfigCancelFuzzyWatchEvent(String connectionId, String groupKeyPattern) { + this.connectionId = connectionId; + this.groupKeyPattern = groupKeyPattern; + } + + /** + * Get the ID of the client making the request. + * + * @return The client ID + */ + public String getConnectionId() { + return connectionId; + } + + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + /** + * Set the ID of the client making the request. + * + * @param connectionId The client ID to be set + */ + public void setConnectionId(String connectionId) { + this.connectionId = connectionId; + } + +} diff --git a/config/src/main/java/com/alibaba/nacos/config/server/model/event/ConfigFuzzyWatchEvent.java b/config/src/main/java/com/alibaba/nacos/config/server/model/event/ConfigFuzzyWatchEvent.java new file mode 100644 index 00000000000..c6a75210c50 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/model/event/ConfigFuzzyWatchEvent.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 ConfigFuzzyWatchEvent extends Event { + + private static final long serialVersionUID = 1953965691384930209L; + + /** + * ID of the client making the request. + */ + private String connectionId; + + /** + * Pattern for matching group keys. + */ + private String groupKeyPattern; + + /** + * 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 connectionId ID of the client making the request + * @param clientExistingGroupKeys Set of existing group keys associated with the client + * @param groupKeyPattern Pattern for matching group keys + * @param isInitializing Flag indicating whether the client is initializing + */ + public ConfigFuzzyWatchEvent(String connectionId, Set clientExistingGroupKeys, String groupKeyPattern, + boolean isInitializing) { + this.connectionId = connectionId; + this.clientExistingGroupKeys = clientExistingGroupKeys; + this.groupKeyPattern = groupKeyPattern; + this.isInitializing = isInitializing; + } + + /** + * Get the ID of the client making the request. + * + * @return The client ID + */ + public String getConnectionId() { + return connectionId; + } + + /** + * Set the ID of the client making the request. + * + * @param connectionId The client ID to be set + */ + public void setConnectionId(String connectionId) { + this.connectionId = connectionId; + } + + /** + * Get the pattern for matching group keys. + * + * @return The key group pattern + */ + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + /** + * Set the pattern for matching group keys. + * + * @param groupKeyPattern The key group pattern to be set + */ + public void setGroupKeyPattern(String groupKeyPattern) { + this.groupKeyPattern = groupKeyPattern; + } + + /** + * 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/ConfigConnectionEventListener.java b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigConnectionEventListener.java index bd9ce128f0b..a007b1c463b 100644 --- a/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigConnectionEventListener.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigConnectionEventListener.java @@ -16,6 +16,7 @@ package com.alibaba.nacos.config.server.remote; +import com.alibaba.nacos.config.server.service.ConfigFuzzyWatchContextService; import com.alibaba.nacos.core.remote.ClientConnectionEventListener; import com.alibaba.nacos.core.remote.Connection; import com.alibaba.nacos.core.utils.Loggers; @@ -32,8 +33,12 @@ public class ConfigConnectionEventListener extends ClientConnectionEventListener final ConfigChangeListenContext configChangeListenContext; - public ConfigConnectionEventListener(ConfigChangeListenContext configChangeListenContext) { + final ConfigFuzzyWatchContextService configFuzzyWatchContextService; + + public ConfigConnectionEventListener(ConfigChangeListenContext configChangeListenContext, + ConfigFuzzyWatchContextService configFuzzyWatchContextService) { this.configChangeListenContext = configChangeListenContext; + this.configFuzzyWatchContextService = configFuzzyWatchContextService; } @Override @@ -46,6 +51,7 @@ public void clientDisConnected(Connection connect) { String connectionId = connect.getMetaInfo().getConnectionId(); Loggers.REMOTE_DIGEST.info("[{}]client disconnected,clear config listen context", connectionId); configChangeListenContext.clearContextForConnectionId(connectionId); + configFuzzyWatchContextService.clearFuzzyWatchContext(connectionId); } } diff --git a/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigFuzzyWatchChangeNotifier.java b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigFuzzyWatchChangeNotifier.java new file mode 100644 index 00000000000..05b9200172c --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigFuzzyWatchChangeNotifier.java @@ -0,0 +1,217 @@ +/* + * 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.ConfigFuzzyWatchChangeNotifyRequest; +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.service.ConfigFuzzyWatchContextService; +import com.alibaba.nacos.config.server.utils.ConfigExecutor; +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; + +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.CONFIG_CHANGED; +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.DELETE_CONFIG; + +/** + * 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 +public class ConfigFuzzyWatchChangeNotifier extends Subscriber { + + private static final String POINT_FUZZY_WATCH_CONFIG_PUSH = "POINT_FUZZY_WATCH_CONFIG_PUSH"; + + private static final String POINT_FUZZY_WATCH_CONFIG_PUSH_SUCCESS = "POINT_FUZZY_WATCH_CONFIG_PUSH_SUCCESS"; + + private static final String POINT_FUZZY_WATCH_CONFIG_PUSH_FAIL = "POINT_FUZZY_WATCH_CONFIG_PUSH_FAIL"; + + private final ConnectionManager connectionManager; + + private final RpcPushService rpcPushService; + + private final TpsControlManager tpsControlManager; + + private final ConfigFuzzyWatchContextService configFuzzyWatchContextService; + + /** + * Constructs RpcFuzzyListenConfigChangeNotifier with the specified dependencies. + * + * @param connectionManager The manager for connections. + * @param rpcPushService The service for RPC push. + */ + public ConfigFuzzyWatchChangeNotifier(ConnectionManager connectionManager, RpcPushService rpcPushService, + ConfigFuzzyWatchContextService configFuzzyWatchContextService) { + this.connectionManager = connectionManager; + this.rpcPushService = rpcPushService; + this.tpsControlManager = ControlManagerCenter.getInstance().getTpsControlManager(); + this.configFuzzyWatchContextService = configFuzzyWatchContextService; + NotifyCenter.registerSubscriber(this); + } + + @Override + public void onEvent(LocalDataChangeEvent event) { + + boolean exists = ConfigCacheService.getContentCache(event.groupKey) != null; + //can not recognize add or update, set config_changed here + String changedType = exists ? CONFIG_CHANGED : DELETE_CONFIG; + boolean needNotify = configFuzzyWatchContextService.syncGroupKeyContext(event.groupKey, changedType); + if (needNotify) { + for (String clientId : configFuzzyWatchContextService.getMatchedClients(event.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 appName = metaInfo.getAppName(); + + ConfigFuzzyWatchChangeNotifyRequest request = new ConfigFuzzyWatchChangeNotifyRequest(event.groupKey, + changedType); + 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) { + ConfigFuzzyWatchChangeNotifyRequest notifyRequest = retryTask.notifyRequest; + if (retryTask.isOverTimes()) { + Loggers.REMOTE_PUSH.warn( + "push callback retry fail over times.groupKey={},,clientId={}, will unregister client.", + notifyRequest.getGroupKey(), 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={},groupKey={},tenant={},clientId={}", + notifyRequest.getGroupKey(), retryTask.connectionId); + } + } + + /** + * Represents a task for pushing notification to remote clients. + */ + class RpcPushTask implements Runnable { + + ConfigFuzzyWatchChangeNotifyRequest 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(ConfigFuzzyWatchChangeNotifyRequest 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_WATCH_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_WATCH_CONFIG_PUSH_SUCCESS); + tpsControlManager.check(tpsCheckRequest); + } + + @Override + public void onFail(Throwable e) { + TpsCheckRequest tpsCheckRequest = new TpsCheckRequest(); + tpsCheckRequest.setPointName(POINT_FUZZY_WATCH_CONFIG_PUSH_FAIL); + tpsControlManager.check(tpsCheckRequest); + Loggers.REMOTE_PUSH.warn("Push fail, groupKey={}, clientId={}", notifyRequest.getGroupKey(), + connectionId, e); + push(RpcPushTask.this); + } + + }, ConfigExecutor.getClientConfigNotifierServiceExecutor()); + } + } + } +} diff --git a/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigFuzzyWatchRequestHandler.java b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigFuzzyWatchRequestHandler.java new file mode 100644 index 00000000000..5426b5accb2 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigFuzzyWatchRequestHandler.java @@ -0,0 +1,95 @@ +/* + * 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.ConfigFuzzyWatchRequest; +import com.alibaba.nacos.api.config.remote.response.ConfigFuzzyWatchResponse; +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.config.server.model.event.ConfigCancelFuzzyWatchEvent; +import com.alibaba.nacos.config.server.model.event.ConfigFuzzyWatchEvent; +import com.alibaba.nacos.config.server.service.ConfigFuzzyWatchContextService; +import com.alibaba.nacos.core.control.TpsControl; +import com.alibaba.nacos.core.paramcheck.ExtractorManager; +import com.alibaba.nacos.core.paramcheck.impl.ConfigFuzzyWatchRequestParamsExtractor; +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.stereotype.Component; + +import java.util.Set; + +import static com.alibaba.nacos.api.common.Constants.WATCH_TYPE_CANCEL_WATCH; +import static com.alibaba.nacos.api.common.Constants.WATCH_TYPE_WATCH; + +/** + * 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 ConfigFuzzyWatchRequestHandler extends RequestHandler { + + private ConfigFuzzyWatchContextService configFuzzyWatchContextService; + + public ConfigFuzzyWatchRequestHandler(ConfigFuzzyWatchContextService configFuzzyWatchContextService) { + this.configFuzzyWatchContextService = configFuzzyWatchContextService; + } + + /** + * 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 = "ConfigFuzzyWatch") + @Secured(action = ActionTypes.READ, signType = SignType.CONFIG) + @ExtractorManager.Extractor(rpcExtractor = ConfigFuzzyWatchRequestParamsExtractor.class) + public ConfigFuzzyWatchResponse handle(ConfigFuzzyWatchRequest request, RequestMeta meta) throws NacosException { + String connectionId = StringPool.get(meta.getConnectionId()); + String groupKeyPattern = request.getGroupKeyPattern(); + if (WATCH_TYPE_WATCH.equals(request.getWatchType())) { + // Add client to the fuzzy listening context + configFuzzyWatchContextService.addFuzzyListen(groupKeyPattern, connectionId); + // Get existing group keys for the client and publish initialization event + Set clientExistingGroupKeys = request.getReceivedGroupKeys(); + NotifyCenter.publishEvent(new ConfigFuzzyWatchEvent(connectionId, clientExistingGroupKeys, groupKeyPattern, + request.isInitializing())); + } else if (WATCH_TYPE_CANCEL_WATCH.equals(request.getWatchType())) { + NotifyCenter.publishEvent(new ConfigCancelFuzzyWatchEvent(connectionId, groupKeyPattern)); + } + + // Return response + return new ConfigFuzzyWatchResponse(); + } +} diff --git a/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigFuzzyWatchSyncNotifier.java b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigFuzzyWatchSyncNotifier.java new file mode 100644 index 00000000000..4710cddb3a2 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigFuzzyWatchSyncNotifier.java @@ -0,0 +1,367 @@ +/* + * 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.ConfigFuzzyWatchSyncRequest; +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.SmartSubscriber; +import com.alibaba.nacos.common.task.BatchTaskCounter; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern; +import com.alibaba.nacos.config.server.configuration.ConfigCommonConfig; +import com.alibaba.nacos.config.server.model.event.ConfigCancelFuzzyWatchEvent; +import com.alibaba.nacos.config.server.model.event.ConfigFuzzyWatchEvent; +import com.alibaba.nacos.config.server.service.ConfigFuzzyWatchContextService; +import com.alibaba.nacos.config.server.utils.ConfigExecutor; +import com.alibaba.nacos.core.remote.ConnectionManager; +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.LinkedList; +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 static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_DIFF_SYNC_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_INIT_NOTIFY; + +/** + * Handles batch fuzzy listen events and pushes corresponding notifications to clients. + * + * @author stone-98 + * @date 2024/3/18 + */ +@Component(value = "configFuzzyWatchSyncNotifier") +public class ConfigFuzzyWatchSyncNotifier extends SmartSubscriber { + + 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; + + private final ConfigFuzzyWatchContextService configFuzzyWatchContextService; + + public ConfigFuzzyWatchSyncNotifier(ConnectionManager connectionManager, RpcPushService rpcPushService, + ConfigFuzzyWatchContextService configFuzzyWatchContextService) { + this.connectionManager = connectionManager; + this.tpsControlManager = ControlManagerCenter.getInstance().getTpsControlManager(); + this.rpcPushService = rpcPushService; + this.configFuzzyWatchContextService = configFuzzyWatchContextService; + 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(FuzzyWatchRpcPushTask retryTask, ConnectionManager connectionManager) { + ConfigFuzzyWatchSyncRequest 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 + */ + public void handleFuzzyWatchEvent(ConfigFuzzyWatchEvent event) { + + // Match client effective group keys based on the event pattern, client IP, and tag + Set matchGroupKeys = configFuzzyWatchContextService.matchGroupKeys(event.getGroupKeyPattern()); + + // Retrieve existing group keys for the client from the event + Set clientExistingGroupKeys = event.getClientExistingGroupKeys(); + + // Calculate and merge configuration states based on matched and existing group keys + List configStates = FuzzyGroupKeyPattern.diffGroupKeys(matchGroupKeys, + clientExistingGroupKeys); + + if (CollectionUtils.isEmpty(configStates)) { + if (event.isInitializing()) { + ConfigFuzzyWatchSyncRequest request = ConfigFuzzyWatchSyncRequest.buildInitFinishRequest( + event.getGroupKeyPattern()); + int maxPushRetryTimes = ConfigCommonConfig.getInstance().getMaxPushRetryTimes(); + // Create RPC push task and push the request to the client + FuzzyWatchRpcPushTask fuzzyWatchRpcPushTask = new FuzzyWatchRpcPushTask(request, null, + maxPushRetryTimes, event.getConnectionId()); + push(fuzzyWatchRpcPushTask, connectionManager); + } + + } else { + String syncType = event.isInitializing() ? FUZZY_WATCH_INIT_NOTIFY : FUZZY_WATCH_DIFF_SYNC_NOTIFY; + + 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 totalBatch = divideConfigStatesIntoBatches.size(); + BatchTaskCounter batchTaskCounter = new BatchTaskCounter(divideConfigStatesIntoBatches.size()); + int currentBatch = 1; + for (List configStateList : divideConfigStatesIntoBatches) { + // Map config states to FuzzyListenNotifyDiffRequest.Context objects + Set contexts = configStateList.stream().map(state -> { + + String changeType = state.isExist() ? Constants.ConfigChangedType.ADD_CONFIG + : Constants.ConfigChangedType.DELETE_CONFIG; + return ConfigFuzzyWatchSyncRequest.Context.build(state.getGroupKey(), changeType); + }).collect(Collectors.toSet()); + + ConfigFuzzyWatchSyncRequest request = ConfigFuzzyWatchSyncRequest.buildSyncRequest(syncType, contexts, + event.getGroupKeyPattern(), totalBatch, currentBatch); + int maxPushRetryTimes = ConfigCommonConfig.getInstance().getMaxPushRetryTimes(); + // Create RPC push task and push the request to the client + FuzzyWatchRpcPushTask fuzzyWatchRpcPushTask = new FuzzyWatchRpcPushTask(request, batchTaskCounter, + maxPushRetryTimes, event.getConnectionId()); + push(fuzzyWatchRpcPushTask, connectionManager); + currentBatch++; + } + } + + } + + @Override + public List> subscribeTypes() { + List> result = new LinkedList<>(); + result.add(ConfigFuzzyWatchEvent.class); + result.add(ConfigCancelFuzzyWatchEvent.class); + return result; + } + + @Override + public void onEvent(Event event) { + if (event instanceof ConfigFuzzyWatchEvent) { + handleFuzzyWatchEvent((ConfigFuzzyWatchEvent) event); + } + + if (event instanceof ConfigCancelFuzzyWatchEvent) { + // Remove client from the fuzzy listening context + configFuzzyWatchContextService.removeFuzzyListen(((ConfigCancelFuzzyWatchEvent) event).getGroupKeyPattern(), + ((ConfigCancelFuzzyWatchEvent) event).getConnectionId()); + } + + } + + /** + * 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()); + } + + /** + * Represents a task for pushing FuzzyListenNotifyDiffRequest to clients. + */ + class FuzzyWatchRpcPushTask implements Runnable { + + /** + * The FuzzyListenNotifyDiffRequest to be pushed. + */ + ConfigFuzzyWatchSyncRequest 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; + + BatchTaskCounter batchTaskCounter; + + /** + * Constructs a new RpcPushTask with the specified parameters. + * + * @param notifyRequest The FuzzyListenNotifyDiffRequest to be pushed + * @param batchTaskCounter The batchTaskCounter counter for tracking the number of finished push batches + * @param maxRetryTimes The maximum number of times to retry pushing the request + * @param connectionId The ID of the connection associated with the client + */ + public FuzzyWatchRpcPushTask(ConfigFuzzyWatchSyncRequest notifyRequest, BatchTaskCounter batchTaskCounter, + int maxRetryTimes, String connectionId) { + this.notifyRequest = notifyRequest; + this.batchTaskCounter = batchTaskCounter; + this.maxRetryTimes = maxRetryTimes; + this.connectionId = connectionId; + } + + /** + * 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 FuzzyWatchRpcPushCallback(this, tpsControlManager, connectionManager, batchTaskCounter), + ConfigExecutor.getClientConfigNotifierServiceExecutor()); + } + } + } + + /** + * Represents a callback for handling the result of an RPC push operation. + */ + class FuzzyWatchRpcPushCallback extends AbstractPushCallBack { + + /** + * The RpcPushTask associated with the callback. + */ + FuzzyWatchRpcPushTask fuzzyWatchRpcPushTask; + + /** + * The TpsControlManager for checking TPS limits. + */ + TpsControlManager tpsControlManager; + + /** + * The ConnectionManager for managing client connections. + */ + ConnectionManager connectionManager; + + BatchTaskCounter batchTaskCounter; + + /** + * Constructs a new RpcPushCallback with the specified parameters. + * + * @param fuzzyWatchRpcPushTask The RpcPushTask associated with the callback + * @param tpsControlManager The TpsControlManager for checking TPS limits + * @param connectionManager The ConnectionManager for managing client connections + * @param batchTaskCounter The batchTaskCounter counter + */ + public FuzzyWatchRpcPushCallback(FuzzyWatchRpcPushTask fuzzyWatchRpcPushTask, + TpsControlManager tpsControlManager, ConnectionManager connectionManager, + BatchTaskCounter batchTaskCounter) { + super(3000L); + this.fuzzyWatchRpcPushTask = fuzzyWatchRpcPushTask; + this.tpsControlManager = tpsControlManager; + this.connectionManager = connectionManager; + this.batchTaskCounter = batchTaskCounter; + + } + + /** + * 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 (batchTaskCounter != null) { + batchTaskCounter.batchSuccess(fuzzyWatchRpcPushTask.notifyRequest.getCurrentBatch()); + if (batchTaskCounter.batchCompleted() && fuzzyWatchRpcPushTask.notifyRequest.getSyncType() + .equals(FUZZY_WATCH_INIT_NOTIFY)) { + ConfigFuzzyWatchSyncRequest request = ConfigFuzzyWatchSyncRequest.buildInitFinishRequest( + fuzzyWatchRpcPushTask.notifyRequest.getGroupKeyPattern()); + push(new FuzzyWatchRpcPushTask(request, null, 50, fuzzyWatchRpcPushTask.connectionId), + 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={}", + fuzzyWatchRpcPushTask.notifyRequest.getGroupKeyPattern(), fuzzyWatchRpcPushTask.connectionId, e); + push(fuzzyWatchRpcPushTask, 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 bb49cb84311..acba470e826 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 @@ -63,7 +63,7 @@ public class ConfigCacheService { /** * groupKey -> cacheItem. */ - private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); public static int groupCount() { return CACHE.size(); diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/ConfigFuzzyWatchContextService.java b/config/src/main/java/com/alibaba/nacos/config/server/service/ConfigFuzzyWatchContextService.java new file mode 100644 index 00000000000..bccc74d5152 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/ConfigFuzzyWatchContextService.java @@ -0,0 +1,255 @@ +/* + * 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.config.server.service; + +import com.alibaba.nacos.api.exception.runtime.NacosRuntimeException; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern; +import com.alibaba.nacos.config.server.utils.GroupKey; +import com.alibaba.nacos.config.server.utils.GroupKey2; +import com.alibaba.nacos.config.server.utils.LogUtil; +import com.alibaba.nacos.core.utils.GlobalExecutor; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.ADD_CONFIG; +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.CONFIG_CHANGED; +import static com.alibaba.nacos.api.common.Constants.ConfigChangedType.DELETE_CONFIG; +import static com.alibaba.nacos.api.model.v2.ErrorCode.FUZZY_WATCH_PATTERN_MATCH_GROUP_KEY_OVER_LIMIT; +import static com.alibaba.nacos.api.model.v2.ErrorCode.FUZZY_WATCH_PATTERN_OVER_LIMIT; + +/** + * fuzzy watch context for config. + * + * @author shiyiyue + */ +@Component +public class ConfigFuzzyWatchContextService { + + /** + * groupKeyPattern -> watched client id set. + */ + private final Map> watchedClients = new ConcurrentHashMap<>(); + + /** + * groupKeyPattern -> matched groupKeys set. + */ + private final Map> matchedGroupKeys = new ConcurrentHashMap<>(); + + private static final int FUZZY_WATCH_MAX_PATTERN_COUNT = 50; + + private static final int FUZZY_WATCH_MAX_PATTERN_MATCHED_GROUP_KEY_COUNT = 1000; + + public ConfigFuzzyWatchContextService() { + + GlobalExecutor.scheduleWithFixDelayByCommon(() -> trimFuzzyWatchContext(), 30000); + } + + /** + * trim fuzzy watch context.
1.remove watchedClients if watched client is empty. 2.remove matchedServiceKeys + * if watchedClients is null. pattern matchedServiceKeys will be removed in second period to avoid frequently + * matchedServiceKeys init. + */ + private void trimFuzzyWatchContext() { + try { + Iterator>> iterator = matchedGroupKeys.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> next = iterator.next(); + Set watchedClients = this.watchedClients.get(next.getKey()); + + if (watchedClients == null) { + iterator.remove(); + LogUtil.DEFAULT_LOG.info( + "[fuzzy-watch] no watchedClients context for pattern {},remove matchedGroupKeys context", + next.getKey()); + } else if (watchedClients.isEmpty()) { + LogUtil.DEFAULT_LOG.info("[fuzzy-watch] no client watched pattern {},remove watchedClients context", + next.getKey()); + this.watchedClients.remove(next.getKey()); + } + } + } catch (Throwable throwable) { + LogUtil.DEFAULT_LOG.warn("[fuzzy-watch] trim fuzzy watch context fail", throwable); + } + } + + /** + * get matched exist group keys with the groupKeyPattern. return null if not matched. + * + * @param groupKeyPattern groupKeyPattern. + * @return + */ + public Set matchGroupKeys(String groupKeyPattern) { + return matchedGroupKeys.get(groupKeyPattern); + } + + /** + * sync group key change to fuzzy context. + * + * @param groupKey groupKey. + * @param changedType changedType. + * @return need notify ot not. + */ + public boolean syncGroupKeyContext(String groupKey, String changedType) { + + boolean needNotify = false; + + String[] groupKeyItems = GroupKey.parseKey(groupKey); + String dataId = groupKeyItems[0]; + String group = groupKeyItems[1]; + String namespace = groupKeyItems[2]; + Iterator>> iterator = matchedGroupKeys.entrySet().iterator(); + boolean tryAdd = changedType.equals(ADD_CONFIG) || changedType.equals(CONFIG_CHANGED); + boolean tryRemove = changedType.equals(DELETE_CONFIG); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + if (FuzzyGroupKeyPattern.matchPattern(entry.getKey(), dataId, group, namespace)) { + if (tryAdd && entry.getValue().add(groupKey)) { + needNotify = true; + } + if (tryRemove && entry.getValue().remove(groupKey)) { + needNotify = true; + } + } + } + return needNotify; + } + + /** + * 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. + * @return A set of group keys that match the pattern and are effective for the client. + */ + private void initMatchGroupKeys(String groupKeyPattern) { + if (matchedGroupKeys.containsKey(groupKeyPattern)) { + return; + } + + if (matchedGroupKeys.size() >= FUZZY_WATCH_MAX_PATTERN_COUNT) { + LogUtil.DEFAULT_LOG.warn( + "[fuzzy-watch] pattern count is over limit ,pattern {} init fail,current count is {}", + groupKeyPattern, matchedGroupKeys.size()); + throw new NacosRuntimeException(FUZZY_WATCH_PATTERN_OVER_LIMIT.getCode(), + FUZZY_WATCH_PATTERN_OVER_LIMIT.getMsg()); + } + + matchedGroupKeys.computeIfAbsent(groupKeyPattern, k -> new HashSet<>()); + Set matchedGroupKeys = this.matchedGroupKeys.get(groupKeyPattern); + long matchBeginTime = System.currentTimeMillis(); + for (String groupKey : ConfigCacheService.CACHE.keySet()) { + String[] groupKeyItems = GroupKey.parseKey(groupKey); + if (FuzzyGroupKeyPattern.matchPattern(groupKeyPattern, groupKeyItems[0], groupKeyItems[1], + groupKeyItems[2])) { + + if (matchedGroupKeys.size() >= FUZZY_WATCH_MAX_PATTERN_MATCHED_GROUP_KEY_COUNT) { + LogUtil.DEFAULT_LOG.warn("[fuzzy-watch] pattern matched service count is over limit , " + + "other services will stop notify for pattern {} ,current count is {}", groupKeyPattern, + matchedGroupKeys.size()); + + throw new NacosRuntimeException(FUZZY_WATCH_PATTERN_MATCH_GROUP_KEY_OVER_LIMIT.getCode(), + FUZZY_WATCH_PATTERN_MATCH_GROUP_KEY_OVER_LIMIT.getMsg()); + } + matchedGroupKeys.add(groupKey); + } + } + LogUtil.DEFAULT_LOG.info("[fuzzy-watch] pattern {} match {} group keys, cost {}ms", groupKeyPattern, + matchedGroupKeys.size(), System.currentTimeMillis() - matchBeginTime); + + } + + /** + * 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) { + + initMatchGroupKeys(groupKeyPattern); + // Add the connection ID to the set associated with the key pattern in keyPatternContext + watchedClients.computeIfAbsent(groupKeyPattern, k -> new HashSet<>()); + watchedClients.get(groupKeyPattern).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 = watchedClients.get(groupKeyPattern); + if (CollectionUtils.isNotEmpty(connectIds)) { + // Remove the connection ID from the set if it exists + connectIds.remove(connectionId); + } + } + + /** + * remove watch context for connection id. + * + * @param connectionId connection id. + */ + public void clearFuzzyWatchContext(String connectionId) { + for (Map.Entry> keyPatternContextEntry : watchedClients.entrySet()) { + Set connectionIds = keyPatternContextEntry.getValue(); + if (CollectionUtils.isNotEmpty(connectionIds)) { + connectionIds.remove(connectionId); + } + } + } + + /** + * 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 getMatchedClients(String groupKey) { + // Initialize a set to store the matched connection IDs + Set connectIds = new HashSet<>(); + // Iterate over each key pattern in the context + Iterator>> watchClientIterator = watchedClients.entrySet().iterator(); + + String[] groupItems = GroupKey2.parseKey(groupKey); + + while (watchClientIterator.hasNext()) { + Map.Entry> watchClientEntry = watchClientIterator.next(); + + String keyPattern = watchClientEntry.getKey(); + if (FuzzyGroupKeyPattern.matchPattern(keyPattern, groupItems[0], groupItems[1], groupItems[2])) { + if (CollectionUtils.isNotEmpty(watchClientEntry.getValue())) { + connectIds.addAll(watchClientEntry.getValue()); + } + } + } + return connectIds; + } + +} diff --git a/core/src/main/java/com/alibaba/nacos/core/paramcheck/impl/ConfigFuzzyWatchRequestParamsExtractor.java b/core/src/main/java/com/alibaba/nacos/core/paramcheck/impl/ConfigFuzzyWatchRequestParamsExtractor.java new file mode 100644 index 00000000000..d396986896e --- /dev/null +++ b/core/src/main/java/com/alibaba/nacos/core/paramcheck/impl/ConfigFuzzyWatchRequestParamsExtractor.java @@ -0,0 +1,56 @@ +/* + * 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.ConfigFuzzyWatchRequest; +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.FuzzyGroupKeyPattern; +import com.alibaba.nacos.core.paramcheck.AbstractRpcParamExtractor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Extractor for parameters of {@link ConfigFuzzyWatchRequest}. 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 ConfigFuzzyWatchRequestParamsExtractor 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 { + ConfigFuzzyWatchRequest req = (ConfigFuzzyWatchRequest) request; + List paramInfos = new ArrayList<>(); + // Extract namespace ID and group from the context + ParamInfo paramInfo1 = new ParamInfo(); + paramInfo1.setNamespaceId(FuzzyGroupKeyPattern.getNamespaceFromPattern(req.getGroupKeyPattern())); + paramInfos.add(paramInfo1); + return paramInfos; + } +} diff --git a/core/src/main/java/com/alibaba/nacos/core/utils/GlobalExecutor.java b/core/src/main/java/com/alibaba/nacos/core/utils/GlobalExecutor.java index 275b50f1b98..7559b326ccb 100644 --- a/core/src/main/java/com/alibaba/nacos/core/utils/GlobalExecutor.java +++ b/core/src/main/java/com/alibaba/nacos/core/utils/GlobalExecutor.java @@ -72,6 +72,13 @@ public static void scheduleByCommon(Runnable runnable, long delayMs) { COMMON_EXECUTOR.schedule(runnable, delayMs, TimeUnit.MILLISECONDS); } + public static void scheduleWithFixDelayByCommon(Runnable runnable, long delayMs) { + if (COMMON_EXECUTOR.isShutdown()) { + return; + } + COMMON_EXECUTOR.scheduleWithFixedDelay(runnable, delayMs, delayMs, TimeUnit.MILLISECONDS); + } + public static void submitLoadDataTask(Runnable runnable) { DISTRO_EXECUTOR.submit(runnable); } diff --git a/core/src/main/java/com/alibaba/nacos/core/utils/StringPool.java b/core/src/main/java/com/alibaba/nacos/core/utils/StringPool.java index d7f7fca3545..69920b9e0cd 100644 --- a/core/src/main/java/com/alibaba/nacos/core/utils/StringPool.java +++ b/core/src/main/java/com/alibaba/nacos/core/utils/StringPool.java @@ -31,7 +31,11 @@ public class StringPool { private static Cache groupKeyCache = CacheBuilder.newBuilder().maximumSize(5000000) - .expireAfterAccess(180, TimeUnit.SECONDS).build(); + .expireAfterAccess(60, TimeUnit.SECONDS).build(); + + static { + GlobalExecutor.scheduleWithFixDelayByCommon(() -> groupKeyCache.cleanUp(), 30000); + } /** * get singleton string value from the pool. diff --git a/example/src/main/java/com/alibaba/nacos/example/ConfigFuzzyWatchExample.java b/example/src/main/java/com/alibaba/nacos/example/ConfigFuzzyWatchExample.java new file mode 100644 index 00000000000..a1cf4bb3d24 --- /dev/null +++ b/example/src/main/java/com/alibaba/nacos/example/ConfigFuzzyWatchExample.java @@ -0,0 +1,99 @@ +/* + * 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.AbstractFuzzyWatchEventWatcher; +import com.alibaba.nacos.api.config.listener.FuzzyWatchEventWatcher; +import com.alibaba.nacos.api.config.listener.ConfigFuzzyWatchChangeEvent; +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 ConfigFuzzyWatchExample { + + 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 + FuzzyWatchEventWatcher listener = new AbstractFuzzyWatchEventWatcher() { + @Override + public void onEvent(ConfigFuzzyWatchChangeEvent event) { + System.out.println("[fuzzy listen config change]" + event.toString()); + } + }; + + // Add the fuzzy listener to monitor configurations starting with "test" + configService.fuzzyWatch("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); + } +} diff --git a/example/src/main/java/com/alibaba/nacos/example/NamingFuzzyWatchExample.java b/example/src/main/java/com/alibaba/nacos/example/NamingFuzzyWatchExample.java new file mode 100644 index 00000000000..c7efac16a9b --- /dev/null +++ b/example/src/main/java/com/alibaba/nacos/example/NamingFuzzyWatchExample.java @@ -0,0 +1,120 @@ +/* + * 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.exception.NacosException; +import com.alibaba.nacos.api.naming.NamingFactory; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.listener.FuzzyWatchChangeEvent; +import com.alibaba.nacos.api.naming.listener.FuzzyWatchEventWatcher; +import com.alibaba.nacos.api.naming.utils.NamingUtils; + +import java.util.Properties; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static com.alibaba.nacos.api.common.Constants.DEFAULT_GROUP; + +/** + * Nacos naming fuzzy watch example. + *

Add the JVM parameter to run the NamingExample:

+ * {@code -DserverAddr=${nacos.server.ip}:${nacos.server.port} -Dnamespace=${namespaceId}} + * + * @author tanyongquan + */ +public class NamingFuzzyWatchExample { + + public static void main(String[] args) throws NacosException, InterruptedException { + + Properties properties = new Properties(); + properties.setProperty("serverAddr", System.getProperty("serverAddr", "localhost")); + properties.setProperty("namespace", System.getProperty("namespace", "public")); + + NamingService naming = NamingFactory.createNamingService(properties); + + int num = 5; + for (int i = 1; i <= num; i++) { + String s = "nacos.test." + i; + naming.registerInstance(s, "11.11.11.11", 8888); + } + + System.out.println(num + " instance have been registered"); + + Executor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), + runnable -> { + Thread thread = new Thread(runnable); + thread.setName("test-thread"); + return thread; + }); + + naming.fuzzyWatch(DEFAULT_GROUP, new FuzzyWatchEventWatcher() { + + //EventListener onEvent is sync to handle, If process too low in onEvent, maybe block other onEvent callback. + //So you can override getExecutor() to async handle event. + @Override + public Executor getExecutor() { + return executor; + } + + @Override + public void onEvent(FuzzyWatchChangeEvent event) { + System.out.println( + "[Fuzzy-Watch-GROUP]changed service name: " + NamingUtils.getServiceKey(event.getNamespace(), + event.getGroupName(), event.getServiceName())); + System.out.println("[Fuzzy-Watch-GROUP]change type: " + event.getChangeType()); + } + }); + + naming.fuzzyWatch("nacos.test.*", DEFAULT_GROUP, new FuzzyWatchEventWatcher() { + + @Override + public Executor getExecutor() { + return executor; + } + + @Override + public void onEvent(FuzzyWatchChangeEvent event) { + System.out.println( + "[Prefix-Fuzzy-Watch]changed service name: " + NamingUtils.getServiceKey(event.getNamespace(), + event.getGroupName(), event.getServiceName())); + System.out.println("[Prefix-Fuzzy-Watch]change type: " + event.getChangeType()); + } + }); + + naming.registerInstance("nacos.test.-1", "11.11.11.11", 8888); + + Thread.sleep(1000); + + naming.registerInstance("nacos.OTHER-PREFIX", "11.11.11.11", 8888); + + Thread.sleep(1000); + + naming.registerInstance("nacos.OTHER-GROUP", "OTHER-GROUP", "11.11.11.11", 8888); + + Thread.sleep(1000); + + for (int i = 1; i <= num; i++) { + String s = "nacos.test." + i; + naming.deregisterInstance(s, "11.11.11.11", 8888); + } + + Thread.sleep(1000); + + } +} diff --git a/naming/src/main/java/com/alibaba/nacos/naming/core/v2/event/client/ClientOperationEvent.java b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/event/client/ClientOperationEvent.java index 93ecd25cdea..b08d2d0c496 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/core/v2/event/client/ClientOperationEvent.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/event/client/ClientOperationEvent.java @@ -20,6 +20,8 @@ import com.alibaba.nacos.naming.core.v2.client.Client; import com.alibaba.nacos.naming.core.v2.pojo.Service; +import java.util.Set; + /** * Operation client event. * @@ -94,10 +96,80 @@ public ClientUnsubscribeServiceEvent(Service service, String clientId) { } } - public static class ClientReleaseEvent extends ClientOperationEvent { + /** + * Client fuzzy watch service event. + */ + public static class ClientFuzzyWatchEvent extends ClientOperationEvent { + + private static final long serialVersionUID = -4518919987813223119L; + + /** + * client watched pattern. + */ + private final String groupKeyPattern; + + /** + * client side received group keys. + */ + private Set clientReceivedServiceKeys; + + /** + * is fuzzy watch initializing. + */ + private boolean isInitializing; + + public ClientFuzzyWatchEvent(String groupKeyPattern, String clientId, Set clientReceivedServiceKeys, + boolean isInitializing) { + super(clientId, null); + this.groupKeyPattern = groupKeyPattern; + this.clientReceivedServiceKeys = clientReceivedServiceKeys; + this.isInitializing = isInitializing; + } + + public String getGroupKeyPattern() { + return groupKeyPattern; + } + + public Set getClientReceivedServiceKeys() { + return clientReceivedServiceKeys; + } + + public void setClientReceivedServiceKeys(Set clientReceivedServiceKeys) { + this.clientReceivedServiceKeys = clientReceivedServiceKeys; + } + + public boolean isInitializing() { + return isInitializing; + } + + public void setInitializing(boolean initializing) { + isInitializing = initializing; + } + } - private static final long serialVersionUID = -281486927726245701L; + /** + * Client cancel fuzzy watch service event. + */ + public static class ClientCancelFuzzyWatchEvent extends ClientOperationEvent { + + private static final long serialVersionUID = -4518919987813223118L; + + private final String pattern; + + public ClientCancelFuzzyWatchEvent(String pattern, String clientId) { + super(clientId, null); + this.pattern = pattern; + } + + public String getPattern() { + return pattern; + } + } + public static class ClientReleaseEvent extends ClientOperationEvent { + + private static final long serialVersionUID = -281486927726245701L; + private final Client client; private final boolean isNative; @@ -107,11 +179,11 @@ public ClientReleaseEvent(Client client, boolean isNative) { this.client = client; this.isNative = isNative; } - + public Client getClient() { return client; } - + public boolean isNative() { return isNative; } diff --git a/naming/src/main/java/com/alibaba/nacos/naming/core/v2/event/service/ServiceEvent.java b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/event/service/ServiceEvent.java index 6d559529880..9530f95b377 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/core/v2/event/service/ServiceEvent.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/event/service/ServiceEvent.java @@ -45,17 +45,25 @@ public static class ServiceChangedEvent extends ServiceEvent { private static final long serialVersionUID = 2123694271992630822L; - public ServiceChangedEvent(Service service) { - this(service, false); + private final String changedType; + + public ServiceChangedEvent(Service service, String changedType) { + this(service, changedType, false); } - public ServiceChangedEvent(Service service, boolean incrementRevision) { + public ServiceChangedEvent(Service service, String changedType, boolean incrementRevision) { super(service); + this.changedType = changedType; service.renewUpdateTime(); if (incrementRevision) { service.incrementRevision(); } } + + public String getChangedType() { + return changedType; + } + } /** @@ -64,14 +72,14 @@ public ServiceChangedEvent(Service service, boolean incrementRevision) { public static class ServiceSubscribedEvent extends ServiceEvent { private static final long serialVersionUID = -2645441445867337345L; - + private final String clientId; public ServiceSubscribedEvent(Service service, String clientId) { super(service); this.clientId = clientId; } - + public String getClientId() { return clientId; } diff --git a/naming/src/main/java/com/alibaba/nacos/naming/core/v2/index/ClientServiceIndexesManager.java b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/index/ClientServiceIndexesManager.java index 48ea5611ae6..2f3bae809c1 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/core/v2/index/ClientServiceIndexesManager.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/index/ClientServiceIndexesManager.java @@ -16,6 +16,7 @@ package com.alibaba.nacos.naming.core.v2.index; +import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.common.notify.Event; import com.alibaba.nacos.common.notify.NotifyCenter; import com.alibaba.nacos.common.notify.listener.SmartSubscriber; @@ -101,15 +102,15 @@ private void handleClientDisconnect(ClientOperationEvent.ClientReleaseEvent even for (Service each : client.getAllSubscribeService()) { removeSubscriberIndexes(each, client.getClientId()); } - DeregisterInstanceReason reason = event.isNative() - ? DeregisterInstanceReason.NATIVE_DISCONNECTED : DeregisterInstanceReason.SYNCED_DISCONNECTED; + DeregisterInstanceReason reason = event.isNative() ? DeregisterInstanceReason.NATIVE_DISCONNECTED + : DeregisterInstanceReason.SYNCED_DISCONNECTED; long currentTimeMillis = System.currentTimeMillis(); for (Service each : client.getAllPublishedService()) { removePublisherIndexes(each, client.getClientId()); InstancePublishInfo instance = client.getInstancePublishInfo(each); - NotifyCenter.publishEvent(new DeregisterInstanceTraceEvent(currentTimeMillis, - "", false, reason, each.getNamespace(), each.getGroup(), each.getName(), - instance.getIp(), instance.getPort())); + NotifyCenter.publishEvent( + new DeregisterInstanceTraceEvent(currentTimeMillis, "", false, reason, each.getNamespace(), + each.getGroup(), each.getName(), instance.getIp(), instance.getPort())); } } @@ -128,14 +129,21 @@ private void handleClientOperation(ClientOperationEvent event) { } private void addPublisherIndexes(Service service, String clientId) { + String serviceChangedType = Constants.ServiceChangedType.INSTANCE_CHANGED; + if (!publisherIndexes.containsKey(service)) { + // The only time the index needs to be updated is when the service is first created + serviceChangedType = Constants.ServiceChangedType.ADD_SERVICE; + } + NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, serviceChangedType, true)); publisherIndexes.computeIfAbsent(service, key -> new ConcurrentHashSet<>()).add(clientId); - NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true)); } private void removePublisherIndexes(Service service, String clientId) { publisherIndexes.computeIfPresent(service, (s, ids) -> { ids.remove(clientId); - NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true)); + String serviceChangedType = ids.isEmpty() ? Constants.ServiceChangedType.DELETE_SERVICE + : Constants.ServiceChangedType.INSTANCE_CHANGED; + NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, serviceChangedType, true)); return ids.isEmpty() ? null : ids; }); } diff --git a/naming/src/main/java/com/alibaba/nacos/naming/core/v2/index/NamingFuzzyWatchContextService.java b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/index/NamingFuzzyWatchContextService.java new file mode 100644 index 00000000000..6f0b0cd5c55 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/index/NamingFuzzyWatchContextService.java @@ -0,0 +1,265 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.naming.core.v2.index; + +import com.alibaba.nacos.api.exception.runtime.NacosRuntimeException; +import com.alibaba.nacos.api.naming.utils.NamingUtils; +import com.alibaba.nacos.common.notify.Event; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.notify.listener.SmartSubscriber; +import com.alibaba.nacos.common.utils.ConcurrentHashSet; +import com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern; +import com.alibaba.nacos.core.utils.GlobalExecutor; +import com.alibaba.nacos.naming.core.v2.ServiceManager; +import com.alibaba.nacos.naming.core.v2.event.client.ClientOperationEvent; +import com.alibaba.nacos.naming.core.v2.pojo.Service; +import com.alibaba.nacos.naming.misc.Loggers; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.alibaba.nacos.api.common.Constants.ServiceChangedType.ADD_SERVICE; +import static com.alibaba.nacos.api.common.Constants.ServiceChangedType.DELETE_SERVICE; +import static com.alibaba.nacos.api.model.v2.ErrorCode.FUZZY_WATCH_PATTERN_MATCH_GROUP_KEY_OVER_LIMIT; +import static com.alibaba.nacos.api.model.v2.ErrorCode.FUZZY_WATCH_PATTERN_OVER_LIMIT; +import static com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern.getNamespaceFromPattern; + +/** + * naming fuzzy watch context service. + * + * @author shiyiyue + */ +@Component +public class NamingFuzzyWatchContextService extends SmartSubscriber { + + /** + * watched client ids of a pattern, {fuzzy watch pattern -> Set[watched clientID]}. + */ + private final ConcurrentMap> watchedClients = new ConcurrentHashMap<>(); + + /** + * The pattern matched service keys for pattern.{fuzzy watch pattern -> Set[matched service keys]}. initialized a + * new entry pattern when a client register a new pattern. destroyed a new entry pattern by task when no clients + * watch pattern in max 30s delay. + */ + private final ConcurrentMap> matchedServiceKeys = new ConcurrentHashMap<>(); + + private static final int FUZZY_WATCH_MAX_PATTERN_COUNT = 50; + + private static final int FUZZY_WATCH_MAX_PATTERN_MATCHED_GROUP_KEY_COUNT = 200; + + public NamingFuzzyWatchContextService() { + GlobalExecutor.scheduleWithFixDelayByCommon(() -> trimFuzzyWatchContext(), 30000); + NotifyCenter.registerSubscriber(this); + } + + /** + * trim fuzzy watch context.
1.remove watchedClients if watched client is empty. 2.remove matchedServiceKeys + * if watchedClients is null. pattern matchedServiceKeys will be removed in second period to avoid frequently + * matchedServiceKeys init. + */ + private void trimFuzzyWatchContext() { + try { + Iterator>> iterator = matchedServiceKeys.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> next = iterator.next(); + Set watchedClients = this.watchedClients.get(next.getKey()); + if (watchedClients == null) { + iterator.remove(); + } else if (watchedClients.isEmpty()) { + this.watchedClients.remove(next.getKey()); + } + } + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + } + + @Override + public List> subscribeTypes() { + List> result = new LinkedList<>(); + result.add(ClientOperationEvent.ClientReleaseEvent.class); + return result; + } + + @Override + public void onEvent(Event event) { + + //handle client disconnected event. + if (event instanceof ClientOperationEvent.ClientReleaseEvent) { + removeFuzzyWatchContext(((ClientOperationEvent.ClientReleaseEvent) event).getClientId()); + } + } + + /** + * get client that fuzzy watch this service. + * + * @param service service to check fuzzy watcher. + * @return client ids. + */ + public Set getFuzzyWatchedClients(Service service) { + Set matchedClients = new HashSet<>(); + Iterator>> iterator = watchedClients.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + if (FuzzyGroupKeyPattern.matchPattern(entry.getKey(), service.getName(), service.getGroup(), + service.getNamespace())) { + matchedClients.addAll(entry.getValue()); + } + } + return matchedClients; + } + + /** + * sync changed service to fuzzy watch context. + * + * @param changedService changed service. + * @param changedType change type. + * @return + */ + public boolean syncServiceContext(Service changedService, String changedType) { + + boolean needNotify = false; + if (!changedType.equals(ADD_SERVICE) && !changedType.equals(DELETE_SERVICE)) { + return false; + } + + String serviceKey = NamingUtils.getServiceKey(changedService.getNamespace(), changedService.getGroup(), + changedService.getName()); + Loggers.SRV_LOG.warn("FUZZY_WATCH: service change matched,service key {},changed type {} ", serviceKey, + changedType); + + Iterator>> iterator = matchedServiceKeys.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry> next = iterator.next(); + if (FuzzyGroupKeyPattern.matchPattern(next.getKey(), changedService.getName(), changedService.getGroup(), + changedService.getNamespace())) { + + Set matchedServiceKeys = next.getValue(); + if (changedType.equals(ADD_SERVICE) && !matchedServiceKeys.contains(serviceKey)) { + if (matchedServiceKeys.size() >= FUZZY_WATCH_MAX_PATTERN_MATCHED_GROUP_KEY_COUNT) { + Loggers.SRV_LOG.warn("FUZZY_WATCH: pattern matched service count is over limit , " + + "current service will be ignore for pattern {} ,current count is {}", next.getKey(), + matchedServiceKeys.size()); + continue; + } + if (matchedServiceKeys.add(serviceKey)) { + Loggers.SRV_LOG.info("FUZZY_WATCH: pattern {} matched service keys count changed to {}", + next.getKey(), matchedServiceKeys.size()); + needNotify = true; + } + + } else if (changedType.equals(DELETE_SERVICE) && matchedServiceKeys.contains(serviceKey)) { + if (matchedServiceKeys.remove(serviceKey)) { + Loggers.SRV_LOG.info("FUZZY_WATCH: pattern {} matched service keys count changed to {}", + next.getKey(), matchedServiceKeys.size()); + needNotify = true; + } + } + } + } + return needNotify; + } + + /** + * sync fuzzy watch context. + * + * @param groupKeyPattern group key pattern. + * @param clientId client id. + * @return + */ + public Set syncFuzzyWatcherContext(String groupKeyPattern, String clientId) { + watchedClients.computeIfAbsent(groupKeyPattern, key -> new ConcurrentHashSet<>()).add(clientId); + Set matchedServiceKeys = initWatchMatchService(groupKeyPattern); + return matchedServiceKeys; + } + + private void removeFuzzyWatchContext(String clientId) { + Iterator>> iterator = watchedClients.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> next = iterator.next(); + next.getValue().remove(clientId); + } + } + + /** + * remove fuzzy watch context for a pattern and client id. + * + * @param groupKeyPattern group key pattern. + * @param clientId client id. + */ + public void removeFuzzyWatchContext(String groupKeyPattern, String clientId) { + if (watchedClients.containsKey(groupKeyPattern)) { + watchedClients.get(groupKeyPattern).remove(clientId); + } + } + + /** + * This method will build/update the fuzzy watch match index for given patterns. + * + * @param completedPattern the completed pattern of watch (with namespace id). + * @return a copy set of matched service keys in Nacos server + */ + public Set initWatchMatchService(String completedPattern) { + + if (!matchedServiceKeys.containsKey(completedPattern)) { + if (matchedServiceKeys.size() >= FUZZY_WATCH_MAX_PATTERN_COUNT) { + Loggers.SRV_LOG.warn( + "FUZZY_WATCH: fuzzy watch pattern count is over limit ,pattern {} init fail,current count is {}", + completedPattern, matchedServiceKeys.size()); + throw new NacosRuntimeException(FUZZY_WATCH_PATTERN_OVER_LIMIT.getCode(), + FUZZY_WATCH_PATTERN_OVER_LIMIT.getMsg()); + } + + long matchBeginTime = System.currentTimeMillis(); + Set namespaceServices = ServiceManager.getInstance() + .getSingletons(getNamespaceFromPattern(completedPattern)); + Set matchedServices = matchedServiceKeys.computeIfAbsent(completedPattern, k -> new HashSet<>()); + + for (Service service : namespaceServices) { + if (FuzzyGroupKeyPattern.matchPattern(completedPattern, service.getName(), service.getGroup(), + service.getNamespace())) { + if (matchedServices.size() >= FUZZY_WATCH_MAX_PATTERN_MATCHED_GROUP_KEY_COUNT) { + + Loggers.SRV_LOG.warn("FUZZY_WATCH: pattern matched service count is over limit , " + + "other services will stop notify for pattern {} ,current count is {}", + completedPattern, matchedServices.size()); + throw new NacosRuntimeException(FUZZY_WATCH_PATTERN_MATCH_GROUP_KEY_OVER_LIMIT.getCode(), + FUZZY_WATCH_PATTERN_MATCH_GROUP_KEY_OVER_LIMIT.getMsg()); + } + matchedServices.add( + NamingUtils.getServiceKey(service.getNamespace(), service.getGroup(), service.getName())); + } + } + matchedServiceKeys.putIfAbsent(completedPattern, matchedServices); + Loggers.SRV_LOG.info("FUZZY_WATCH: pattern {} match {} services, cost {}ms", completedPattern, + matchedServices.size(), System.currentTimeMillis() - matchBeginTime); + + } + + return new HashSet(matchedServiceKeys.get(completedPattern)); + } + +} diff --git a/naming/src/main/java/com/alibaba/nacos/naming/core/v2/metadata/InstanceMetadataProcessor.java b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/metadata/InstanceMetadataProcessor.java index 92cb1ee4f51..d7e6514f287 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/core/v2/metadata/InstanceMetadataProcessor.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/core/v2/metadata/InstanceMetadataProcessor.java @@ -108,7 +108,8 @@ private void updateInstanceMetadata(MetadataOperation op) { Service service = Service.newService(op.getNamespace(), op.getGroup(), op.getServiceName()); service = ServiceManager.getInstance().getSingleton(service); namingMetadataManager.updateInstanceMetadata(service, op.getTag(), op.getMetadata()); - NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true)); + NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, + com.alibaba.nacos.api.common.Constants.ServiceChangedType.INSTANCE_CHANGED, true)); } private void deleteInstanceMetadata(MetadataOperation op) { diff --git a/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/heartbeat/ClientBeatProcessorV2.java b/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/heartbeat/ClientBeatProcessorV2.java index dee58b8c67f..d1125aac39a 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/heartbeat/ClientBeatProcessorV2.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/heartbeat/ClientBeatProcessorV2.java @@ -16,6 +16,7 @@ package com.alibaba.nacos.naming.healthcheck.heartbeat; +import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.api.naming.utils.NamingUtils; import com.alibaba.nacos.common.notify.NotifyCenter; import com.alibaba.nacos.common.trace.event.naming.HealthStateChangeTraceEvent; @@ -67,7 +68,7 @@ public void run() { instance.setHealthy(true); Loggers.EVT_LOG.info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok", rsInfo.getServiceName(), ip, port, rsInfo.getCluster(), UtilsAndCommons.LOCALHOST_SITE); - NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service)); + NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, Constants.ServiceChangedType.HEART_BEAT)); NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(client)); NotifyCenter.publishEvent(new HealthStateChangeTraceEvent(System.currentTimeMillis(), service.getNamespace(), service.getGroup(), service.getName(), instance.getIp(), diff --git a/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/heartbeat/UnhealthyInstanceChecker.java b/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/heartbeat/UnhealthyInstanceChecker.java index 1614a5cf7aa..b3d65847d56 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/heartbeat/UnhealthyInstanceChecker.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/heartbeat/UnhealthyInstanceChecker.java @@ -76,7 +76,7 @@ private void changeHealthyStatus(Client client, Service service, HealthCheckInst .info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client last beat: {}", instance.getIp(), instance.getPort(), instance.getCluster(), service.getName(), UtilsAndCommons.LOCALHOST_SITE, instance.getLastHeartBeatTime()); - NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service)); + NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, Constants.ServiceChangedType.HEART_BEAT)); NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(client)); NotifyCenter.publishEvent(new HealthStateChangeTraceEvent(System.currentTimeMillis(), service.getNamespace(), service.getGroup(), service.getName(), instance.getIp(), instance.getPort(), diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/NamingFuzzyWatchChangeNotifier.java b/naming/src/main/java/com/alibaba/nacos/naming/push/NamingFuzzyWatchChangeNotifier.java new file mode 100644 index 00000000000..28a693f4435 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/NamingFuzzyWatchChangeNotifier.java @@ -0,0 +1,90 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.naming.push; + +import com.alibaba.nacos.api.naming.utils.NamingUtils; +import com.alibaba.nacos.common.notify.Event; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.notify.listener.SmartSubscriber; +import com.alibaba.nacos.naming.core.v2.event.service.ServiceEvent; +import com.alibaba.nacos.naming.core.v2.index.NamingFuzzyWatchContextService; +import com.alibaba.nacos.naming.misc.Loggers; +import com.alibaba.nacos.naming.push.v2.PushConfig; +import com.alibaba.nacos.naming.push.v2.task.FuzzyWatchChangeNotifyTask; +import com.alibaba.nacos.naming.push.v2.task.FuzzyWatchPushDelayTaskEngine; +import org.springframework.stereotype.Service; + +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * service change notify for fuzzy watch. + * + * @author shiyiyue + */ +@Service +public class NamingFuzzyWatchChangeNotifier extends SmartSubscriber { + + private NamingFuzzyWatchContextService namingFuzzyWatchContextService; + + private FuzzyWatchPushDelayTaskEngine fuzzyWatchPushDelayTaskEngine; + + public NamingFuzzyWatchChangeNotifier(NamingFuzzyWatchContextService namingFuzzyWatchContextService, + FuzzyWatchPushDelayTaskEngine fuzzyWatchPushDelayTaskEngine) { + this.fuzzyWatchPushDelayTaskEngine = fuzzyWatchPushDelayTaskEngine; + this.namingFuzzyWatchContextService = namingFuzzyWatchContextService; + NotifyCenter.registerSubscriber(this); + } + + @Override + public List> subscribeTypes() { + List> result = new LinkedList<>(); + result.add(ServiceEvent.ServiceChangedEvent.class); + return result; + } + + @Override + public void onEvent(Event event) { + if (event instanceof ServiceEvent.ServiceChangedEvent) { + ServiceEvent.ServiceChangedEvent serviceChangedEvent = (ServiceEvent.ServiceChangedEvent) event; + if (namingFuzzyWatchContextService.syncServiceContext(serviceChangedEvent.getService(), + serviceChangedEvent.getChangedType())) { + generateFuzzyWatchChangeNotifyTask(serviceChangedEvent.getService(), + serviceChangedEvent.getChangedType()); + } + } + } + + private void generateFuzzyWatchChangeNotifyTask(com.alibaba.nacos.naming.core.v2.pojo.Service service, + String changedType) { + + String serviceKey = NamingUtils.getServiceKey(service.getNamespace(), service.getGroup(), service.getName()); + Set fuzzyWatchedClients = namingFuzzyWatchContextService.getFuzzyWatchedClients(service); + + Loggers.SRV_LOG.info("FUZZY_WATCH:serviceKey {} has {} clients fuzzy watched", serviceKey, + fuzzyWatchedClients == null ? 0 : fuzzyWatchedClients.size()); + // watch notify push task specify by service + for (String clientId : fuzzyWatchedClients) { + FuzzyWatchChangeNotifyTask fuzzyWatchChangeNotifyTask = new FuzzyWatchChangeNotifyTask(serviceKey, + changedType, clientId, PushConfig.getInstance().getPushTaskDelay()); + fuzzyWatchPushDelayTaskEngine.addTask(FuzzyWatchPushDelayTaskEngine.getTaskKey(fuzzyWatchChangeNotifyTask), + fuzzyWatchChangeNotifyTask); + } + } + +} diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/NamingFuzzyWatchSyncNotifier.java b/naming/src/main/java/com/alibaba/nacos/naming/push/NamingFuzzyWatchSyncNotifier.java new file mode 100644 index 00000000000..c9e8f897257 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/NamingFuzzyWatchSyncNotifier.java @@ -0,0 +1,157 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.naming.push; + +import com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchSyncRequest; +import com.alibaba.nacos.common.notify.Event; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.notify.listener.SmartSubscriber; +import com.alibaba.nacos.common.task.BatchTaskCounter; +import com.alibaba.nacos.common.utils.CollectionUtils; +import com.alibaba.nacos.common.utils.FuzzyGroupKeyPattern; +import com.alibaba.nacos.naming.core.v2.event.client.ClientOperationEvent; +import com.alibaba.nacos.naming.core.v2.index.NamingFuzzyWatchContextService; +import com.alibaba.nacos.naming.push.v2.PushConfig; +import com.alibaba.nacos.naming.push.v2.task.FuzzyWatchPushDelayTaskEngine; +import com.alibaba.nacos.naming.push.v2.task.FuzzyWatchSyncNotifyTask; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import static com.alibaba.nacos.api.common.Constants.FINISH_FUZZY_WATCH_INIT_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_DIFF_SYNC_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_INIT_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.ServiceChangedType.ADD_SERVICE; +import static com.alibaba.nacos.api.common.Constants.ServiceChangedType.DELETE_SERVICE; + +/** + * fuzzy watch event for fuzzy watch. + * @author shiyiyue + */ +@Service +public class NamingFuzzyWatchSyncNotifier extends SmartSubscriber { + + private NamingFuzzyWatchContextService namingFuzzyWatchContextService; + + private FuzzyWatchPushDelayTaskEngine fuzzyWatchPushDelayTaskEngine; + + private static final int BATCH_SIZE = 10; + + public NamingFuzzyWatchSyncNotifier(NamingFuzzyWatchContextService namingFuzzyWatchContextService, + FuzzyWatchPushDelayTaskEngine fuzzyWatchPushDelayTaskEngine) { + this.namingFuzzyWatchContextService = namingFuzzyWatchContextService; + this.fuzzyWatchPushDelayTaskEngine = fuzzyWatchPushDelayTaskEngine; + NotifyCenter.registerSubscriber(this); + } + + @Override + public List> subscribeTypes() { + List> result = new LinkedList<>(); + result.add(ClientOperationEvent.ClientFuzzyWatchEvent.class); + result.add(ClientOperationEvent.ClientCancelFuzzyWatchEvent.class); + return result; + } + + @Override + public void onEvent(Event event) { + + if (event instanceof ClientOperationEvent.ClientFuzzyWatchEvent) { + //fuzzy watch event + ClientOperationEvent.ClientFuzzyWatchEvent clientFuzzyWatchEvent = (ClientOperationEvent.ClientFuzzyWatchEvent) event; + handleClientFuzzyWatchEvent(clientFuzzyWatchEvent); + } else if (event instanceof ClientOperationEvent.ClientCancelFuzzyWatchEvent) { + //handle cancel fuzzy watch event for a client cancel a fuzzy pattern + String completedPattern = ((ClientOperationEvent.ClientCancelFuzzyWatchEvent) event).getPattern(); + namingFuzzyWatchContextService.removeFuzzyWatchContext(completedPattern, + ((ClientOperationEvent.ClientCancelFuzzyWatchEvent) event).getClientId()); + } + + } + + private void handleClientFuzzyWatchEvent(ClientOperationEvent.ClientFuzzyWatchEvent clientFuzzyWatchEvent) { + String completedPattern = clientFuzzyWatchEvent.getGroupKeyPattern(); + + //sync fuzzy watch context + Set patternMatchedServiceKeys = namingFuzzyWatchContextService.syncFuzzyWatcherContext(completedPattern, + clientFuzzyWatchEvent.getClientId()); + Set clientReceivedGroupKeys = clientFuzzyWatchEvent.getClientReceivedServiceKeys(); + List groupKeyStates = FuzzyGroupKeyPattern.diffGroupKeys( + patternMatchedServiceKeys, clientReceivedGroupKeys); + Set syncContext = convert(groupKeyStates); + String syncType = + clientFuzzyWatchEvent.isInitializing() ? FUZZY_WATCH_INIT_NOTIFY : FUZZY_WATCH_DIFF_SYNC_NOTIFY; + + if (CollectionUtils.isNotEmpty(groupKeyStates)) { + Set> dividedServices = divideServiceByBatch(syncContext); + BatchTaskCounter batchTaskCounter = new BatchTaskCounter(dividedServices.size()); + int currentBatch = 1; + for (Set batchData : dividedServices) { + FuzzyWatchSyncNotifyTask fuzzyWatchSyncNotifyTask = new FuzzyWatchSyncNotifyTask( + clientFuzzyWatchEvent.getClientId(), completedPattern, syncType, batchData, + PushConfig.getInstance().getPushTaskRetryDelay()); + fuzzyWatchSyncNotifyTask.setBatchTaskCounter(batchTaskCounter); + fuzzyWatchSyncNotifyTask.setTotalBatch(dividedServices.size()); + fuzzyWatchSyncNotifyTask.setCurrentBatch(currentBatch); + fuzzyWatchPushDelayTaskEngine.addTask( + FuzzyWatchPushDelayTaskEngine.getTaskKey(fuzzyWatchSyncNotifyTask), fuzzyWatchSyncNotifyTask); + currentBatch++; + } + } else if (FUZZY_WATCH_INIT_NOTIFY.equals(syncType)) { + FuzzyWatchSyncNotifyTask fuzzyWatchSyncNotifyTask = new FuzzyWatchSyncNotifyTask( + clientFuzzyWatchEvent.getClientId(), completedPattern, FINISH_FUZZY_WATCH_INIT_NOTIFY, null, + PushConfig.getInstance().getPushTaskRetryDelay()); + fuzzyWatchPushDelayTaskEngine.addTask(FuzzyWatchPushDelayTaskEngine.getTaskKey(fuzzyWatchSyncNotifyTask), + fuzzyWatchSyncNotifyTask); + + } + } + + private Set> divideServiceByBatch( + Collection matchedService) { + Set> result = new HashSet<>(); + if (matchedService.isEmpty()) { + return result; + } + Set currentBatch = new HashSet<>(); + for (NamingFuzzyWatchSyncRequest.Context groupedServiceName : matchedService) { + currentBatch.add(groupedServiceName); + if (currentBatch.size() >= this.BATCH_SIZE) { + result.add(currentBatch); + currentBatch = new HashSet<>(); + } + } + if (!currentBatch.isEmpty()) { + result.add(currentBatch); + } + return result; + } + + private Set convert(List diffGroupKeys) { + Set syncContext = new HashSet<>(); + for (FuzzyGroupKeyPattern.GroupKeyState groupKeyState : diffGroupKeys) { + NamingFuzzyWatchSyncRequest.Context context = new NamingFuzzyWatchSyncRequest.Context(); + context.setServiceKey(groupKeyState.getGroupKey()); + context.setChangedType(groupKeyState.isExist() ? ADD_SERVICE : DELETE_SERVICE); + syncContext.add(context); + } + return syncContext; + } +} diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutor.java b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutor.java index 21bcfab0d4b..f081cc32b59 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutor.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutor.java @@ -16,6 +16,8 @@ package com.alibaba.nacos.naming.push.v2.executor; +import com.alibaba.nacos.api.naming.remote.request.AbstractFuzzyWatchNotifyRequest; +import com.alibaba.nacos.api.remote.PushCallBack; import com.alibaba.nacos.naming.pojo.Subscriber; import com.alibaba.nacos.naming.push.v2.PushDataWrapper; import com.alibaba.nacos.naming.push.v2.task.NamingPushCallback; @@ -45,4 +47,14 @@ public interface PushExecutor { * @param callBack callback */ void doPushWithCallback(String clientId, Subscriber subscriber, PushDataWrapper data, NamingPushCallback callBack); + + /** + * Do push to notify fuzzy watcher with call back. + * + * @param clientId client id + * @param fuzzyWatchNotifyRequest request for fuzzy watch notification + * @param callBack callback + */ + void doFuzzyWatchNotifyPushWithCallBack(String clientId, AbstractFuzzyWatchNotifyRequest fuzzyWatchNotifyRequest, PushCallBack callBack); + } diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorDelegate.java b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorDelegate.java index f1fa7288e07..34006f21092 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorDelegate.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorDelegate.java @@ -16,6 +16,8 @@ package com.alibaba.nacos.naming.push.v2.executor; +import com.alibaba.nacos.api.naming.remote.request.AbstractFuzzyWatchNotifyRequest; +import com.alibaba.nacos.api.remote.PushCallBack; import com.alibaba.nacos.naming.core.v2.client.impl.IpPortBasedClient; import com.alibaba.nacos.naming.pojo.Subscriber; import com.alibaba.nacos.naming.push.v2.PushDataWrapper; @@ -53,6 +55,13 @@ public void doPushWithCallback(String clientId, Subscriber subscriber, PushDataW getPushExecuteService(clientId, subscriber).doPushWithCallback(clientId, subscriber, data, callBack); } + @Override + public void doFuzzyWatchNotifyPushWithCallBack(String clientId, AbstractFuzzyWatchNotifyRequest watchNotifyRequest, + PushCallBack callBack) { + // only support fuzzy watch by rpc + rpcPushExecuteService.doFuzzyWatchNotifyPushWithCallBack(clientId, watchNotifyRequest, callBack); + } + private PushExecutor getPushExecuteService(String clientId, Subscriber subscriber) { Optional result = SpiImplPushExecutorHolder.getInstance() .findPushExecutorSpiImpl(clientId, subscriber); diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorRpcImpl.java b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorRpcImpl.java index 143d4715bdf..a2084b5d380 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorRpcImpl.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorRpcImpl.java @@ -17,7 +17,9 @@ package com.alibaba.nacos.naming.push.v2.executor; import com.alibaba.nacos.api.naming.pojo.ServiceInfo; +import com.alibaba.nacos.api.naming.remote.request.AbstractFuzzyWatchNotifyRequest; import com.alibaba.nacos.api.naming.remote.request.NotifySubscriberRequest; +import com.alibaba.nacos.api.remote.PushCallBack; import com.alibaba.nacos.core.remote.RpcPushService; import com.alibaba.nacos.naming.misc.GlobalExecutor; import com.alibaba.nacos.naming.pojo.Subscriber; @@ -60,4 +62,10 @@ private ServiceInfo getServiceInfo(PushDataWrapper data, Subscriber subscriber) .selectInstancesWithHealthyProtection(data.getOriginalData(), data.getServiceMetadata(), false, true, subscriber); } + + @Override + public void doFuzzyWatchNotifyPushWithCallBack(String clientId, AbstractFuzzyWatchNotifyRequest watchNotifyRequest, PushCallBack callBack) { + pushService.pushWithCallback(clientId, watchNotifyRequest, callBack, GlobalExecutor.getCallbackExecutor()); + } + } diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorUdpImpl.java b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorUdpImpl.java index c60478166d9..431ea4f7050 100644 --- a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorUdpImpl.java +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/executor/PushExecutorUdpImpl.java @@ -17,7 +17,9 @@ package com.alibaba.nacos.naming.push.v2.executor; import com.alibaba.nacos.api.naming.pojo.ServiceInfo; +import com.alibaba.nacos.api.naming.remote.request.AbstractFuzzyWatchNotifyRequest; import com.alibaba.nacos.api.naming.utils.NamingUtils; +import com.alibaba.nacos.api.remote.PushCallBack; import com.alibaba.nacos.common.utils.StringUtils; import com.alibaba.nacos.naming.pojo.Subscriber; import com.alibaba.nacos.naming.push.UdpPushService; @@ -93,4 +95,9 @@ private ServiceInfo handleClusterData(ServiceInfo data, Subscriber subscriber) { return StringUtils.isBlank(subscriber.getCluster()) ? data : ServiceUtil.selectInstances(data, subscriber.getCluster()); } + + @Override + public void doFuzzyWatchNotifyPushWithCallBack(String clientId, AbstractFuzzyWatchNotifyRequest watchNotifyRequest, PushCallBack callBack) { + + } } diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchChangeNotifyExecuteTask.java b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchChangeNotifyExecuteTask.java new file mode 100644 index 00000000000..46cabbec2e7 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchChangeNotifyExecuteTask.java @@ -0,0 +1,99 @@ +/* + * 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.naming.push.v2.task; + +import com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchChangeNotifyRequest; +import com.alibaba.nacos.api.remote.PushCallBack; +import com.alibaba.nacos.common.task.AbstractExecuteTask; +import com.alibaba.nacos.naming.misc.Loggers; +import com.alibaba.nacos.naming.push.v2.NoRequiredRetryException; +import com.alibaba.nacos.naming.push.v2.PushConfig; + +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_RESOURCE_CHANGED; + +/** + * Nacos naming fuzzy watch notify service change push delay task. + * + * @author tanyongquan + */ +public class FuzzyWatchChangeNotifyExecuteTask extends AbstractExecuteTask { + + private final String serviceKey; + + private final String changedType; + + private String clientId; + + private final FuzzyWatchPushDelayTaskEngine delayTaskEngine; + + public FuzzyWatchChangeNotifyExecuteTask(FuzzyWatchPushDelayTaskEngine delayTaskEngine, String serviceKey, + String changedType, String targetClient) { + this.serviceKey = serviceKey; + this.changedType = changedType; + this.clientId = targetClient; + this.delayTaskEngine = delayTaskEngine; + } + + @Override + public void run() { + + delayTaskEngine.getPushExecutor().doFuzzyWatchNotifyPushWithCallBack(clientId, + new NamingFuzzyWatchChangeNotifyRequest(serviceKey, changedType, FUZZY_WATCH_RESOURCE_CHANGED), + new FuzzyWatchChangeNotifyCallback(clientId, serviceKey, changedType)); + + } + + private class FuzzyWatchChangeNotifyCallback implements PushCallBack { + + private final String clientId; + + private String serviceKey; + + private String changedType; + + private FuzzyWatchChangeNotifyCallback(String clientId, String serviceKey, String changedType) { + this.clientId = clientId; + this.serviceKey = serviceKey; + this.changedType = changedType; + } + + @Override + public long getTimeout() { + return PushConfig.getInstance().getPushTaskTimeout(); + } + + @Override + public void onSuccess() { + Loggers.PUSH.info("[FUZZY-WATCH] change notify success ,clientId {}, serviceKey {] ,changedType {} ", + clientId, clientId, changedType); + + } + + @Override + public void onFail(Throwable e) { + + Loggers.PUSH.warn("[FUZZY-WATCH] change notify fail ,clientId {}, serviceKey {] ,changedType {} ", clientId, + clientId, changedType, e); + + if (!(e instanceof NoRequiredRetryException)) { + delayTaskEngine.addTask(System.currentTimeMillis(), + new FuzzyWatchChangeNotifyTask(serviceKey, changedType, clientId, + PushConfig.getInstance().getPushTaskRetryDelay())); + } + } + } +} diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchChangeNotifyTask.java b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchChangeNotifyTask.java new file mode 100644 index 00000000000..502cccdd9d8 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchChangeNotifyTask.java @@ -0,0 +1,64 @@ +/* + * 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.naming.push.v2.task; + +import com.alibaba.nacos.common.task.AbstractDelayTask; + +/** + * Nacos naming fuzzy watch notify service change push delay task. + * + * @author tanyongquan + */ +public class FuzzyWatchChangeNotifyTask extends AbstractDelayTask { + + private final String serviceKey; + + private final String changedType; + + private final String clientId; + + private final long delay; + + public FuzzyWatchChangeNotifyTask(String serviceKey, String changedType, String clientId, long delay) { + this.serviceKey = serviceKey; + this.changedType = changedType; + this.delay = delay; + this.clientId = clientId; + + } + + public String getChangedType() { + return changedType; + } + + public String getClientId() { + return clientId; + } + + public long getDelay() { + return delay; + } + + public String getServiceKey() { + return serviceKey; + } + + @Override + public void merge(AbstractDelayTask task) { + + } +} diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchPushDelayTaskEngine.java b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchPushDelayTaskEngine.java new file mode 100644 index 00000000000..94e64d42f43 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchPushDelayTaskEngine.java @@ -0,0 +1,105 @@ +/* + * 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.naming.push.v2.task; + +import com.alibaba.nacos.api.exception.runtime.NacosRuntimeException; +import com.alibaba.nacos.common.task.NacosTask; +import com.alibaba.nacos.common.task.NacosTaskProcessor; +import com.alibaba.nacos.common.task.engine.NacosDelayTaskExecuteEngine; +import com.alibaba.nacos.naming.misc.Loggers; +import com.alibaba.nacos.naming.misc.NamingExecuteTaskDispatcher; +import com.alibaba.nacos.naming.misc.SwitchDomain; +import com.alibaba.nacos.naming.push.v2.executor.PushExecutor; +import com.alibaba.nacos.naming.push.v2.executor.PushExecutorDelegate; +import org.springframework.stereotype.Component; + +/** + * Nacos naming fuzzy watch notify service change push delay task execute engine. + * + * @author tanyongquan + */ +@Component +public class FuzzyWatchPushDelayTaskEngine extends NacosDelayTaskExecuteEngine { + + private final PushExecutorDelegate pushExecutor; + + private final SwitchDomain switchDomain; + + public FuzzyWatchPushDelayTaskEngine(PushExecutorDelegate pushExecutor, SwitchDomain switchDomain) { + super(FuzzyWatchPushDelayTaskEngine.class.getSimpleName(), Loggers.PUSH); + this.pushExecutor = pushExecutor; + this.switchDomain = switchDomain; + setDefaultTaskProcessor(new WatchPushDelayTaskProcessor(this)); + } + + public PushExecutor getPushExecutor() { + return pushExecutor; + } + + @Override + protected void processTasks() { + if (!switchDomain.isPushEnabled()) { + return; + } + super.processTasks(); + } + + private static class WatchPushDelayTaskProcessor implements NacosTaskProcessor { + + private final FuzzyWatchPushDelayTaskEngine fuzzyWatchPushExecuteEngine; + + public WatchPushDelayTaskProcessor(FuzzyWatchPushDelayTaskEngine fuzzyWatchPushExecuteEngine) { + this.fuzzyWatchPushExecuteEngine = fuzzyWatchPushExecuteEngine; + } + + @Override + public boolean process(NacosTask task) { + + if (task instanceof FuzzyWatchChangeNotifyTask) { + //process fuzzy watch change notify when a service changed + FuzzyWatchChangeNotifyTask fuzzyWatchChangeNotifyTask = (FuzzyWatchChangeNotifyTask) task; + NamingExecuteTaskDispatcher.getInstance().dispatchAndExecuteTask(getTaskKey(task), + new FuzzyWatchChangeNotifyExecuteTask(fuzzyWatchPushExecuteEngine, + fuzzyWatchChangeNotifyTask.getServiceKey(), fuzzyWatchChangeNotifyTask.getChangedType(), + fuzzyWatchChangeNotifyTask.getClientId())); + } else if (task instanceof FuzzyWatchSyncNotifyTask) { + //process fuzzy watch sync notify when a new client fuzzy watch a pattern + FuzzyWatchSyncNotifyTask fuzzyWatchSyncNotifyTask = (FuzzyWatchSyncNotifyTask) task; + String pattern = fuzzyWatchSyncNotifyTask.getPattern(); + String clientId = fuzzyWatchSyncNotifyTask.getClientId(); + NamingExecuteTaskDispatcher.getInstance().dispatchAndExecuteTask(getTaskKey(task), + new FuzzyWatchSyncNotifyExecuteTask(clientId, pattern, fuzzyWatchPushExecuteEngine, + fuzzyWatchSyncNotifyTask)); + } + return true; + } + + } + + public static String getTaskKey(NacosTask task) { + if (task instanceof FuzzyWatchChangeNotifyTask) { + return "fwcnT-" + ((FuzzyWatchChangeNotifyTask) task).getClientId() + + ((FuzzyWatchChangeNotifyTask) task).getServiceKey(); + } else if (task instanceof FuzzyWatchSyncNotifyTask) { + return "fwsnT-" + ((FuzzyWatchSyncNotifyTask) task).getSyncType() + "-" + + ((FuzzyWatchSyncNotifyTask) task).getClientId() + ((FuzzyWatchSyncNotifyTask) task).getPattern() + + "-" + ((FuzzyWatchSyncNotifyTask) task).getCurrentBatch(); + } else { + throw new NacosRuntimeException(500, "unknown fuzzy task type"); + } + } +} diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchSyncNotifyExecuteTask.java b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchSyncNotifyExecuteTask.java new file mode 100644 index 00000000000..243cb6336a8 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchSyncNotifyExecuteTask.java @@ -0,0 +1,158 @@ +/* + * 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.naming.push.v2.task; + +import com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchSyncRequest; +import com.alibaba.nacos.api.remote.PushCallBack; +import com.alibaba.nacos.common.task.AbstractExecuteTask; +import com.alibaba.nacos.common.task.BatchTaskCounter; +import com.alibaba.nacos.naming.misc.Loggers; +import com.alibaba.nacos.naming.push.v2.NoRequiredRetryException; +import com.alibaba.nacos.naming.push.v2.PushConfig; + +import static com.alibaba.nacos.api.common.Constants.FINISH_FUZZY_WATCH_INIT_NOTIFY; +import static com.alibaba.nacos.api.common.Constants.FUZZY_WATCH_INIT_NOTIFY; + +/** + * Nacos naming fuzzy watch initial push execute task. + * + * @author tanyongquan + */ +public class FuzzyWatchSyncNotifyExecuteTask extends AbstractExecuteTask { + + private final String clientId; + + private final String pattern; + + private final FuzzyWatchPushDelayTaskEngine fuzzyWatchPushDelayTaskEngine; + + private final FuzzyWatchSyncNotifyTask delayTask; + + /** + * Fuzzy watch origin push matched service size, if there is no failure while executing push, + * {@code originSize == latch}. just use to record log after finish all push. + */ + private final int originSize; + + /** + * TODO set batch size from config. + */ + + public FuzzyWatchSyncNotifyExecuteTask(String clientId, String pattern, + FuzzyWatchPushDelayTaskEngine fuzzyWatchPushDelayTaskEngine, FuzzyWatchSyncNotifyTask delayTask) { + this.clientId = clientId; + this.pattern = pattern; + this.fuzzyWatchPushDelayTaskEngine = fuzzyWatchPushDelayTaskEngine; + this.delayTask = delayTask; + this.originSize = delayTask.getSyncServiceKeys().size(); + } + + @Override + public void run() { + + NamingFuzzyWatchSyncRequest namingFuzzyWatchSyncRequest = NamingFuzzyWatchSyncRequest.buildSyncNotifyRequest( + pattern, delayTask.getSyncType(), delayTask.getSyncServiceKeys(), delayTask.getTotalBatch(), + delayTask.getCurrentBatch()); + fuzzyWatchPushDelayTaskEngine.getPushExecutor() + .doFuzzyWatchNotifyPushWithCallBack(clientId, namingFuzzyWatchSyncRequest, + new FuzzyWatchSyncNotifyExecuteTask.FuzzyWatchSyncNotifyCallback(namingFuzzyWatchSyncRequest, + delayTask.getBatchTaskCounter())); + } + + private class FuzzyWatchSyncNotifyCallback implements PushCallBack { + + private NamingFuzzyWatchSyncRequest namingFuzzyWatchSyncRequest; + + /** + * Record the push task execute start time. + */ + private final long executeStartTime; + + private BatchTaskCounter batchTaskCounter; + + private FuzzyWatchSyncNotifyCallback(NamingFuzzyWatchSyncRequest namingFuzzyWatchSyncRequest, + BatchTaskCounter batchTaskCounter) { + this.namingFuzzyWatchSyncRequest = namingFuzzyWatchSyncRequest; + this.executeStartTime = System.currentTimeMillis(); + this.batchTaskCounter = batchTaskCounter; + } + + @Override + public long getTimeout() { + return PushConfig.getInstance().getPushTaskTimeout(); + } + + @Override + public void onSuccess() { + long pushFinishTime = System.currentTimeMillis(); + long pushCostTimeForNetWork = pushFinishTime - executeStartTime; + long pushCostTimeForAll = pushFinishTime - delayTask.getLastProcessTime(); + + if (isFinishInitTask()) { + Loggers.PUSH.info( + "[FUZZY-WATCH-SYNC-COMPLETE] {}ms, all delay time {}ms for client {} watch init push finish," + + " pattern {}, all push service size {}", pushCostTimeForNetWork, pushCostTimeForAll, + clientId, pattern, originSize); + } else { + Loggers.PUSH.info( + "[FUZZY-WATCH-PUSH-SUCC] {}ms, all delay time {}ms for client {}, pattern {}, syncType={},push size {},currentBatch={}", + pushCostTimeForNetWork, pushCostTimeForAll, clientId, pattern, + namingFuzzyWatchSyncRequest.getSyncType(), namingFuzzyWatchSyncRequest.getContexts().size(), + namingFuzzyWatchSyncRequest.getCurrentBatch()); + // if total batch is success sync to client send + if (isInitNotifyTask()) { + batchTaskCounter.batchSuccess(namingFuzzyWatchSyncRequest.getCurrentBatch()); + if (batchTaskCounter.batchCompleted()) { + fuzzyWatchPushDelayTaskEngine.addTask(System.currentTimeMillis(), + new FuzzyWatchSyncNotifyTask(clientId, pattern, FINISH_FUZZY_WATCH_INIT_NOTIFY, null, + PushConfig.getInstance().getPushTaskDelay())); + } + } + + } + } + + private boolean isFinishInitTask() { + return FINISH_FUZZY_WATCH_INIT_NOTIFY.equals(namingFuzzyWatchSyncRequest.getSyncType()); + } + + private boolean isInitNotifyTask() { + return FUZZY_WATCH_INIT_NOTIFY.equals(namingFuzzyWatchSyncRequest.getSyncType()); + } + + @Override + public void onFail(Throwable e) { + long pushCostTime = System.currentTimeMillis() - executeStartTime; + Loggers.PUSH.error( + "[FUZZY-WATCH-SYNC-PUSH-FAIL] {}ms, pattern {} match {} service: {}, currentBatch={},reason={}, client={}", + pushCostTime, pattern, namingFuzzyWatchSyncRequest.getContexts().size(), + namingFuzzyWatchSyncRequest.getCurrentBatch(), e.getMessage(), clientId); + if (!(e instanceof NoRequiredRetryException)) { + Loggers.PUSH.error("Reason detail: ", e); + //resend request only + FuzzyWatchSyncNotifyTask fuzzyWatchSyncNotifyTask = new FuzzyWatchSyncNotifyTask(clientId, pattern, + delayTask.getSyncType(), namingFuzzyWatchSyncRequest.getContexts(), + PushConfig.getInstance().getPushTaskRetryDelay()); + fuzzyWatchSyncNotifyTask.setBatchTaskCounter(batchTaskCounter); + + fuzzyWatchPushDelayTaskEngine.addTask(System.currentTimeMillis(), fuzzyWatchSyncNotifyTask); + + } + } + } + +} \ No newline at end of file diff --git a/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchSyncNotifyTask.java b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchSyncNotifyTask.java new file mode 100644 index 00000000000..fa4c633f206 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/push/v2/task/FuzzyWatchSyncNotifyTask.java @@ -0,0 +1,116 @@ +/* + * 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.naming.push.v2.task; + +import com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchSyncRequest; +import com.alibaba.nacos.common.task.AbstractDelayTask; +import com.alibaba.nacos.common.task.BatchTaskCounter; +import com.alibaba.nacos.naming.misc.Loggers; + +import java.util.HashSet; +import java.util.Set; + +/** + * Nacos naming fuzzy watch initial push delay task. + * + * @author tanyongquan + */ +public class FuzzyWatchSyncNotifyTask extends AbstractDelayTask { + + private final String clientId; + + private final String pattern; + + private final Set syncServiceKeys; + + private final String syncType; + + private int totalBatch; + + private int currentBatch; + + private BatchTaskCounter batchTaskCounter; + + public FuzzyWatchSyncNotifyTask(String clientId, String pattern, String syncType, + Set syncServiceKeys, long delay) { + this.clientId = clientId; + this.pattern = pattern; + this.syncType = syncType; + if (syncServiceKeys != null) { + this.syncServiceKeys = syncServiceKeys; + } else { + this.syncServiceKeys = new HashSet<>(); + } + setTaskInterval(delay); + setLastProcessTime(System.currentTimeMillis()); + } + + public int getTotalBatch() { + return totalBatch; + } + + public void setTotalBatch(int totalBatch) { + this.totalBatch = totalBatch; + } + + public int getCurrentBatch() { + return currentBatch; + } + + public void setCurrentBatch(int currentBatch) { + this.currentBatch = currentBatch; + } + + @Override + public void merge(AbstractDelayTask task) { + if (!(task instanceof FuzzyWatchSyncNotifyTask)) { + return; + } + FuzzyWatchSyncNotifyTask oldTask = (FuzzyWatchSyncNotifyTask) task; + + if (oldTask.getSyncServiceKeys() != null) { + syncServiceKeys.addAll(oldTask.getSyncServiceKeys()); + } + setLastProcessTime(Math.min(getLastProcessTime(), task.getLastProcessTime())); + Loggers.PUSH.info("[FUZZY-WATCH-INIT-PUSH] Task merge for pattern {}", pattern); + } + + public String getPattern() { + return pattern; + } + + public Set getSyncServiceKeys() { + return syncServiceKeys; + } + + public String getSyncType() { + return syncType; + } + + public String getClientId() { + return clientId; + } + + public BatchTaskCounter getBatchTaskCounter() { + return batchTaskCounter; + } + + public void setBatchTaskCounter(BatchTaskCounter batchTaskCounter) { + this.batchTaskCounter = batchTaskCounter; + } + +} diff --git a/naming/src/main/java/com/alibaba/nacos/naming/remote/rpc/handler/NamingFuzzyWatchRequestHandler.java b/naming/src/main/java/com/alibaba/nacos/naming/remote/rpc/handler/NamingFuzzyWatchRequestHandler.java new file mode 100644 index 00000000000..782038fdac8 --- /dev/null +++ b/naming/src/main/java/com/alibaba/nacos/naming/remote/rpc/handler/NamingFuzzyWatchRequestHandler.java @@ -0,0 +1,67 @@ +/* + * 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.naming.remote.rpc.handler; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.remote.request.NamingFuzzyWatchRequest; +import com.alibaba.nacos.api.naming.remote.response.NamingFuzzyWatchResponse; +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.core.remote.RequestHandler; +import com.alibaba.nacos.naming.core.v2.event.client.ClientOperationEvent; +import com.alibaba.nacos.plugin.auth.constant.ActionTypes; +import org.springframework.stereotype.Component; + +import static com.alibaba.nacos.api.common.Constants.WATCH_TYPE_CANCEL_WATCH; +import static com.alibaba.nacos.api.common.Constants.WATCH_TYPE_WATCH; + +/** + * Fuzzy watch service request handler. + * + * @author tanyongquan + */ +@Component("namingFuzzyWatchRequestHandler") +public class NamingFuzzyWatchRequestHandler extends RequestHandler { + + public NamingFuzzyWatchRequestHandler() { + NotifyCenter.registerToPublisher(ClientOperationEvent.ClientFuzzyWatchEvent.class, 1000); + NotifyCenter.registerToPublisher(ClientOperationEvent.ClientCancelFuzzyWatchEvent.class, 1000); + + } + + @Override + @Secured(action = ActionTypes.READ) + public NamingFuzzyWatchResponse handle(NamingFuzzyWatchRequest request, RequestMeta meta) throws NacosException { + + String groupKeyPattern = request.getGroupKeyPattern(); + switch (request.getWatchType()) { + case WATCH_TYPE_WATCH: + NotifyCenter.publishEvent( + new ClientOperationEvent.ClientFuzzyWatchEvent(groupKeyPattern, meta.getConnectionId(), + request.getReceivedGroupKeys(), request.isInitializing())); + return NamingFuzzyWatchResponse.buildSuccessResponse(); + case WATCH_TYPE_CANCEL_WATCH: + NotifyCenter.publishEvent( + new ClientOperationEvent.ClientCancelFuzzyWatchEvent(groupKeyPattern, meta.getConnectionId())); + return NamingFuzzyWatchResponse.buildSuccessResponse(); + default: + throw new NacosException(NacosException.INVALID_PARAM, + String.format("Unsupported request type %s", request.getWatchType())); + } + } +} diff --git a/naming/src/test/java/com/alibaba/nacos/naming/core/v2/index/ClientServiceIndexesManagerTest.java b/naming/src/test/java/com/alibaba/nacos/naming/core/v2/index/ClientServiceIndexesManagerTest.java index 0c9bc8676a4..c6a3c6c7687 100644 --- a/naming/src/test/java/com/alibaba/nacos/naming/core/v2/index/ClientServiceIndexesManagerTest.java +++ b/naming/src/test/java/com/alibaba/nacos/naming/core/v2/index/ClientServiceIndexesManagerTest.java @@ -120,7 +120,7 @@ void testSubscribeTypes() { List> classes = clientServiceIndexesManager.subscribeTypes(); assertNotNull(classes); - assertEquals(5, classes.size()); + assertEquals(7, classes.size()); } @Test diff --git a/naming/src/test/java/com/alibaba/nacos/naming/push/v2/NamingSubscriberServiceV2ImplTest.java b/naming/src/test/java/com/alibaba/nacos/naming/push/v2/NamingSubscriberServiceV2ImplTest.java index 84db805b5e5..abdce5ad864 100644 --- a/naming/src/test/java/com/alibaba/nacos/naming/push/v2/NamingSubscriberServiceV2ImplTest.java +++ b/naming/src/test/java/com/alibaba/nacos/naming/push/v2/NamingSubscriberServiceV2ImplTest.java @@ -16,10 +16,12 @@ package com.alibaba.nacos.naming.push.v2; +import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.naming.core.v2.client.Client; import com.alibaba.nacos.naming.core.v2.client.manager.ClientManagerDelegate; import com.alibaba.nacos.naming.core.v2.event.service.ServiceEvent; import com.alibaba.nacos.naming.core.v2.index.ClientServiceIndexesManager; +import com.alibaba.nacos.naming.core.v2.index.NamingFuzzyWatchContextService; import com.alibaba.nacos.naming.core.v2.pojo.Service; import com.alibaba.nacos.naming.misc.SwitchDomain; import com.alibaba.nacos.naming.pojo.Subscriber; @@ -61,6 +63,9 @@ class NamingSubscriberServiceV2ImplTest { @Mock private ClientServiceIndexesManager indexesManager; + @Mock + private NamingFuzzyWatchContextService namingFuzzyWatchContextService; + @Mock private PushDelayTaskExecuteEngine delayTaskEngine; @@ -74,10 +79,12 @@ class NamingSubscriberServiceV2ImplTest { @BeforeEach void setUp() throws Exception { - subscriberService = new NamingSubscriberServiceV2Impl(clientManager, indexesManager, null, null, null, switchDomain); + subscriberService = new NamingSubscriberServiceV2Impl(clientManager, indexesManager, null, null, null, + switchDomain); ReflectionTestUtils.setField(subscriberService, "delayTaskEngine", delayTaskEngine); when(indexesManager.getAllClientsSubscribeService(service)).thenReturn(Collections.singletonList(testClientId)); - when(indexesManager.getAllClientsSubscribeService(service1)).thenReturn(Collections.singletonList(testClientId)); + when(indexesManager.getAllClientsSubscribeService(service1)).thenReturn( + Collections.singletonList(testClientId)); Collection services = new LinkedList<>(); services.add(service); services.add(service1); @@ -91,7 +98,8 @@ void setUp() throws Exception { @Test void testGetSubscribersByString() { - Collection actual = subscriberService.getSubscribers(service.getNamespace(), service.getGroupedServiceName()); + Collection actual = subscriberService.getSubscribers(service.getNamespace(), + service.getGroupedServiceName()); assertEquals(1, actual.size()); assertEquals(service.getGroupedServiceName(), actual.iterator().next().getServiceName()); } @@ -105,7 +113,8 @@ void testGetSubscribersByService() { @Test void testGetFuzzySubscribersByString() { - Collection actual = subscriberService.getFuzzySubscribers(service.getNamespace(), service.getGroupedServiceName()); + Collection actual = subscriberService.getFuzzySubscribers(service.getNamespace(), + service.getGroupedServiceName()); assertEquals(2, actual.size()); } @@ -116,8 +125,9 @@ void testGetFuzzySubscribersByService() { } @Test - void onEvent() { - subscriberService.onEvent(new ServiceEvent.ServiceChangedEvent(service)); + public void onEvent() { + subscriberService.onEvent( + new ServiceEvent.ServiceChangedEvent(service, Constants.ServiceChangedType.ADD_SERVICE)); verify(delayTaskEngine).addTask(eq(service), any(PushDelayTask.class)); } } diff --git a/naming/src/test/java/com/alibaba/nacos/naming/push/v2/task/FixturePushExecutor.java b/naming/src/test/java/com/alibaba/nacos/naming/push/v2/task/FixturePushExecutor.java index f26fb516a50..6a9db2583a0 100644 --- a/naming/src/test/java/com/alibaba/nacos/naming/push/v2/task/FixturePushExecutor.java +++ b/naming/src/test/java/com/alibaba/nacos/naming/push/v2/task/FixturePushExecutor.java @@ -16,6 +16,8 @@ package com.alibaba.nacos.naming.push.v2.task; +import com.alibaba.nacos.api.naming.remote.request.AbstractFuzzyWatchNotifyRequest; +import com.alibaba.nacos.api.remote.PushCallBack; import com.alibaba.nacos.naming.pojo.Subscriber; import com.alibaba.nacos.naming.push.v2.PushDataWrapper; import com.alibaba.nacos.naming.push.v2.executor.PushExecutor; @@ -39,6 +41,16 @@ public void doPushWithCallback(String clientId, Subscriber subscriber, PushDataW } } + @Override + public void doFuzzyWatchNotifyPushWithCallBack(String clientId, AbstractFuzzyWatchNotifyRequest watchNotifyRequest, + PushCallBack callBack) { + if (shouldSuccess) { + callBack.onSuccess(); + } else { + callBack.onFail(failedException); + } + } + public void setShouldSuccess(boolean shouldSuccess) { this.shouldSuccess = shouldSuccess; }