diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java index 437b9223046..c349980d0fc 100644 --- a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java @@ -240,6 +240,11 @@ public boolean isConfigServiceCacheKeyIgnoreCase() { return getBooleanProperty("config-service.cache.key.ignore-case", false); } + public boolean isConfigServiceChangeCacheEnabled() { + return getBooleanProperty("config-service.incremental.change.enabled", false); + } + + int checkInt(int value, int min, int max, int defaultValue) { if (value >= min && value <= max) { return value; diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseRepository.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseRepository.java index 992c8c93283..70664c40bae 100644 --- a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseRepository.java +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ReleaseRepository.java @@ -37,13 +37,15 @@ Release findFirstByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderBy Release findByIdAndIsAbandonedFalse(long id); + Release findByReleaseKey(String releaseKey); + List findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(String appId, String clusterName, String namespaceName, Pageable page); List findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(String appId, String clusterName, String namespaceName, Pageable page); List findByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseAndIdBetweenOrderByIdDesc(String appId, String clusterName, String namespaceName, long fromId, long toId); - List findByReleaseKeyIn(Set releaseKey); + List findByReleaseKeyIn(Set releaseKeys); List findByIdIn(Set releaseIds); diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java index 95b099e8fc4..689ef3a1fde 100644 --- a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ReleaseService.java @@ -119,6 +119,10 @@ public List findByReleaseKeys(Set releaseKeys) { return releaseRepository.findByReleaseKeyIn(releaseKeys); } + public Release findByReleaseKey(String releaseKey) { + return releaseRepository.findByReleaseKey(releaseKey); + } + public Release findLatestActiveRelease(Namespace namespace) { return findLatestActiveRelease(namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName()); diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java index 76e90709244..258901d8421 100644 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java @@ -30,6 +30,7 @@ import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache; import com.ctrip.framework.apollo.configservice.service.config.ConfigService; import com.ctrip.framework.apollo.configservice.service.config.ConfigServiceWithCache; +import com.ctrip.framework.apollo.configservice.service.config.ConfigServiceWithChangeCache; import com.ctrip.framework.apollo.configservice.service.config.DefaultConfigService; import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil; import io.micrometer.core.instrument.MeterRegistry; @@ -73,9 +74,12 @@ public ConfigService configService() { return new ConfigServiceWithCache(releaseService, releaseMessageService, grayReleaseRulesHolder(), bizConfig, meterRegistry); } + if(bizConfig.isConfigServiceChangeCacheEnabled()){ + return new ConfigServiceWithChangeCache(releaseService, releaseMessageService, + grayReleaseRulesHolder(), bizConfig, meterRegistry); + } return new DefaultConfigService(releaseService, grayReleaseRulesHolder()); } - @Bean public static NoOpPasswordEncoder passwordEncoder() { return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java index 001e014ea02..5270848fa9a 100755 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigController.java @@ -16,6 +16,7 @@ */ package com.ctrip.framework.apollo.configservice.controller; +import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.common.utils.WebUtils; @@ -26,10 +27,13 @@ import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfig; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; +import com.ctrip.framework.apollo.core.dto.ConfigurationChange; +import com.ctrip.framework.apollo.core.enums.ConfigSyncType; import com.ctrip.framework.apollo.tracer.Tracer; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import org.springframework.web.bind.annotation.GetMapping; @@ -42,6 +46,9 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -59,21 +66,25 @@ public class ConfigController { private final NamespaceUtil namespaceUtil; private final InstanceConfigAuditUtil instanceConfigAuditUtil; private final Gson gson; + private final BizConfig bizConfig; + private static final Type configurationTypeReference = new TypeToken>() { - }.getType(); + }.getType(); public ConfigController( final ConfigService configService, final AppNamespaceServiceWithCache appNamespaceService, final NamespaceUtil namespaceUtil, final InstanceConfigAuditUtil instanceConfigAuditUtil, - final Gson gson) { + final Gson gson, + final BizConfig bizConfig) { this.configService = configService; this.appNamespaceService = appNamespaceService; this.namespaceUtil = namespaceUtil; this.instanceConfigAuditUtil = instanceConfigAuditUtil; this.gson = gson; + this.bizConfig = bizConfig; } @GetMapping(value = "/{appId}/{clusterName}/{namespace:.+}") @@ -132,10 +143,10 @@ public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String auditReleases(appId, clusterName, dataCenter, clientIp, releases); - String mergedReleaseKey = releases.stream().map(Release::getReleaseKey) - .collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR)); + String latestMergedReleaseKey = releases.stream().map(Release::getReleaseKey) + .collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR)); - if (mergedReleaseKey.equals(clientSideReleaseKey)) { + if (latestMergedReleaseKey.equals(clientSideReleaseKey)) { // Client side configuration is the same with server side, return 304 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); Tracer.logEvent("Apollo.Config.NotModified", @@ -144,8 +155,42 @@ public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String } ApolloConfig apolloConfig = new ApolloConfig(appId, appClusterNameLoaded, originalNamespace, - mergedReleaseKey); - apolloConfig.setConfigurations(mergeReleaseConfigurations(releases)); + latestMergedReleaseKey); + + Map latestConfigurations = mergeReleaseConfigurations(releases); + + if (bizConfig.isConfigServiceChangeCacheEnabled()) { + LinkedHashSet clientSideReleaseKeys = Sets.newLinkedHashSet( + Arrays.stream(clientSideReleaseKey.split("\\+")).collect(Collectors.toList())); + + Map historyReleases = configService.findReleasesByReleaseKeys( + clientSideReleaseKeys); + //find history releases + if (historyReleases != null) { + //order by clientSideReleaseKeys + List historyReleasesWithOrder = new ArrayList<>(); + for (String item : clientSideReleaseKeys) { + Release release = historyReleases.get(item); + if (release != null) { + historyReleasesWithOrder.add(release); + } + } + + Map historyConfigurations = mergeReleaseConfigurations + (historyReleasesWithOrder); + + List configurationChanges = configService.calcConfigurationChanges( + latestConfigurations, historyConfigurations); + + apolloConfig.setConfigurationChanges(configurationChanges); + + apolloConfig.setConfigSyncType(ConfigSyncType.INCREMENTALSYNC.getValue()); + return apolloConfig; + } + + } + + apolloConfig.setConfigurations(latestConfigurations); Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter)); diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java index f25479a59e5..5a4e26d00d5 100644 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/AbstractConfigService.java @@ -21,9 +21,16 @@ import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; +import com.ctrip.framework.apollo.core.dto.ConfigurationChange; import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) @@ -93,6 +100,52 @@ private Release findRelease(String clientAppId, String clientIp, String clientLa return release; } + public List calcConfigurationChanges( + Map latestReleaseConfigurations, Map historyConfigurations) { + if (latestReleaseConfigurations == null) { + latestReleaseConfigurations = new HashMap<>(); + } + + if (historyConfigurations == null) { + historyConfigurations = new HashMap<>(); + } + + Set previousKeys = historyConfigurations.keySet(); + Set currentKeys = latestReleaseConfigurations.keySet(); + + Set commonKeys = Sets.intersection(previousKeys, currentKeys); + Set newKeys = Sets.difference(currentKeys, commonKeys); + Set removedKeys = Sets.difference(previousKeys, commonKeys); + + List changes = Lists.newArrayList(); + + for (String newKey : newKeys) { + changes.add(new ConfigurationChange(newKey, latestReleaseConfigurations.get(newKey), + "ADDED")); + } + + for (String removedKey : removedKeys) { + changes.add(new ConfigurationChange(removedKey, null, "DELETED")); + } + + for (String commonKey : commonKeys) { + String previousValue = historyConfigurations.get(commonKey); + String currentValue = latestReleaseConfigurations.get(commonKey); + if (com.google.common.base.Objects.equal(previousValue, currentValue)) { + continue; + } + changes.add( + new ConfigurationChange(commonKey, currentValue, "MODIFIED")); + } + + return changes; + } + + @Override + public Map findReleasesByReleaseKeys(Set releaseKeys) { + return null; + } + /** * Find active release by id */ diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java index a7f14606364..ceb0851a6b9 100644 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigService.java @@ -23,7 +23,7 @@ /** * @author Jason Song(song_s@ctrip.com) */ -public interface ConfigService extends ReleaseMessageListener { +public interface ConfigService extends ReleaseMessageListener, IncrementalSyncConfigService { /** * Load config diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java index 00ca866b0ba..3744027f4c5 100644 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithCache.java @@ -62,9 +62,9 @@ public class ConfigServiceWithCache extends AbstractConfigService { private static final String TRACER_EVENT_CACHE_GET = "ConfigCache.Get"; private static final String TRACER_EVENT_CACHE_GET_ID = "ConfigCache.GetById"; - private final ReleaseService releaseService; + protected final ReleaseService releaseService; private final ReleaseMessageService releaseMessageService; - private final BizConfig bizConfig; + protected final BizConfig bizConfig; private final MeterRegistry meterRegistry; private LoadingCache configCache; diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCache.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCache.java new file mode 100644 index 00000000000..bb3cbc77cad --- /dev/null +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCache.java @@ -0,0 +1,155 @@ +/* + * Copyright 2024 Apollo Authors + * + * 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.ctrip.framework.apollo.configservice.service.config; + +import com.ctrip.framework.apollo.biz.config.BizConfig; +import com.ctrip.framework.apollo.biz.entity.Release; +import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; +import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; +import com.ctrip.framework.apollo.biz.message.Topics; +import com.ctrip.framework.apollo.biz.service.ReleaseMessageService; +import com.ctrip.framework.apollo.biz.service.ReleaseService; +import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; +import com.ctrip.framework.apollo.tracer.Tracer; +import com.ctrip.framework.apollo.tracer.spi.Transaction; +import com.google.common.base.Strings; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMap; +import io.micrometer.core.instrument.MeterRegistry; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * config service with change cache + * + * @author jason + */ +public class ConfigServiceWithChangeCache extends ConfigServiceWithCache { + + private static final Logger logger = LoggerFactory.getLogger(ConfigServiceWithChangeCache.class); + + + private static final long DEFAULT_EXPIRED_AFTER_ACCESS_IN_SENCONDS = 10; + + private static final String TRACER_EVENT_CHANGE_CACHE_LOAD_KEY = "ConfigChangeCache.LoadFromDBbyKey"; + + private static final String TRACER_EVENT_CHANGE_CACHE_LOAD = "ConfigChangeCache.LoadFromDB"; + + + public LoadingCache> releasesCache; + + + public ConfigServiceWithChangeCache(final ReleaseService releaseService, + final ReleaseMessageService releaseMessageService, + final GrayReleaseRulesHolder grayReleaseRulesHolder, + final BizConfig bizConfig, + final MeterRegistry meterRegistry) { + + super(releaseService, releaseMessageService, grayReleaseRulesHolder, bizConfig, meterRegistry); + + } + + @PostConstruct + public void initialize() { + super.initialize(); + buildReleaseCache(); + } + + private void buildReleaseCache() { + + CacheBuilder releasesCacheBuilder = CacheBuilder.newBuilder() + .expireAfterAccess(DEFAULT_EXPIRED_AFTER_ACCESS_IN_SENCONDS, TimeUnit.SECONDS); + + releasesCache = releasesCacheBuilder.build(new CacheLoader>() { + @Override + public Optional load(String key) { + Transaction transaction = Tracer.newTransaction(TRACER_EVENT_CHANGE_CACHE_LOAD_KEY, key); + try { + Release release = releaseService.findByReleaseKey(key); + + transaction.setStatus(Transaction.SUCCESS); + + return Optional.ofNullable(release); + } catch (Throwable ex) { + transaction.setStatus(ex); + throw ex; + } finally { + transaction.complete(); + } + + } + }); + } + + @Override + public void handleMessage(ReleaseMessage message, String channel) { + logger.info("message received - channel: {}, message: {}", channel, message); + if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty( + message.getMessage())) { + return; + } + String messageKey = message.getMessage(); + if (bizConfig.isConfigServiceCacheKeyIgnoreCase()) { + messageKey = messageKey.toLowerCase(); + } + Transaction transaction = Tracer.newTransaction(TRACER_EVENT_CHANGE_CACHE_LOAD, messageKey); + try { + List namespaceInfo = ReleaseMessageKeyGenerator.messageToList(messageKey); + Release latestRelease = releaseService.findLatestActiveRelease(namespaceInfo.get(0), + namespaceInfo.get(1), namespaceInfo.get(2)); + + releasesCache.put(latestRelease.getReleaseKey(), Optional.ofNullable(latestRelease)); + + transaction.setStatus(Transaction.SUCCESS); + } catch (Throwable ex) { + transaction.setStatus(ex); + //ignore + } finally { + transaction.complete(); + } + } + + @Override + public Map findReleasesByReleaseKeys(Set releaseKeys) { + try { + + ImmutableMap> releases = releasesCache.getAll(releaseKeys); + + Map filterReleases = releases.entrySet().stream() + .filter(entry -> entry.getValue().isPresent()) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get())); + //find all keys + if (releaseKeys.size() == filterReleases.size()) { + return filterReleases; + } + } catch (ExecutionException e) { + //ignore + } + return null; + } +} diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/IncrementalSyncConfigService.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/IncrementalSyncConfigService.java new file mode 100644 index 00000000000..f201ea8850a --- /dev/null +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/config/IncrementalSyncConfigService.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Apollo Authors + * + * 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.ctrip.framework.apollo.configservice.service.config; + +import com.ctrip.framework.apollo.biz.entity.Release; +import com.ctrip.framework.apollo.core.dto.ConfigurationChange; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author jason + */ +public interface IncrementalSyncConfigService { + + /** + * @param latestReleaseConfigurations + * @param historyConfigurations + * @return the ConfigurationChanges + */ + List calcConfigurationChanges( + Map latestReleaseConfigurations, Map historyConfigurations); + + /** + * @param releaseKeys + * @return the ReleaseMap + */ + Map findReleasesByReleaseKeys(Set releaseKeys); + +} diff --git a/apollo-configservice/src/main/resources/jpa/configdb.init.h2.sql b/apollo-configservice/src/main/resources/jpa/configdb.init.h2.sql index a4f7ae6aae2..7d55d1c1bb4 100644 --- a/apollo-configservice/src/main/resources/jpa/configdb.init.h2.sql +++ b/apollo-configservice/src/main/resources/jpa/configdb.init.h2.sql @@ -19,5 +19,6 @@ VALUES ('namespace.lock.switch', 'default', 'false', '一次发布只能有一个人修改开关', 'default', '1970-01-01 00:00:00'), ('item.key.length.limit', 'default', '128', 'item key 最大长度限制', 'default', '1970-01-01 00:00:00'), ('item.value.length.limit', 'default', '20000', 'item value最大长度限制', 'default', '1970-01-01 00:00:00'), - ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!', 'default', '1970-01-01 00:00:00'); + ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!', 'default', '1970-01-01 00:00:00'), + ('config-service.incremental.change.enabled', 'default', 'false', 'ConfigService是否开启客户端同步增量配置,开启后能提高性能,但是会增大内存消耗!', 'default', '1970-01-01 00:00:00'); CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR "com.ctrip.framework.apollo.common.jpa.H2Function.unixTimestamp"; diff --git a/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java b/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java index db74066f45d..1173241b32e 100644 --- a/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java +++ b/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigControllerTest.java @@ -16,6 +16,7 @@ */ package com.ctrip.framework.apollo.configservice.controller; +import com.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.entity.Release; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache; @@ -25,11 +26,16 @@ import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfig; import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages; +import com.ctrip.framework.apollo.core.dto.ConfigurationChange; +import com.ctrip.framework.apollo.core.enums.ConfigSyncType; +import com.ctrip.framework.apollo.core.enums.ConfigurationChangeType; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,6 +44,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; @@ -49,11 +58,14 @@ */ @RunWith(MockitoJUnitRunner.class) public class ConfigControllerTest { + private ConfigController configController; @Mock private ConfigService configService; @Mock private AppNamespaceServiceWithCache appNamespaceService; + @Mock + private BizConfig bizConfig; private String someAppId; private String someClusterName; private String defaultClusterName; @@ -69,6 +81,12 @@ public class ConfigControllerTest { private Release someRelease; @Mock private Release somePublicRelease; + + @Mock + private Release anotherPublicRelease; + + @Mock + private Release anotherRelease; @Mock private NamespaceUtil namespaceUtil; @Mock @@ -80,7 +98,7 @@ public class ConfigControllerTest { @Before public void setUp() throws Exception { configController = spy(new ConfigController( - configService, appNamespaceService, namespaceUtil, instanceConfigAuditUtil, gson + configService, appNamespaceService, namespaceUtil, instanceConfigAuditUtil, gson, bizConfig )); someAppId = "1"; @@ -99,12 +117,16 @@ public void setUp() throws Exception { when(someRelease.getConfigurations()).thenReturn(someValidConfiguration); when(somePublicRelease.getConfigurations()).thenReturn(somePublicConfiguration); when(namespaceUtil.filterNamespaceName(defaultNamespaceName)).thenReturn(defaultNamespaceName); - when(namespaceUtil.filterNamespaceName(somePublicNamespaceName)).thenReturn(somePublicNamespaceName); - when(namespaceUtil.normalizeNamespace(someAppId, defaultNamespaceName)).thenReturn(defaultNamespaceName); - when(namespaceUtil.normalizeNamespace(someAppId, somePublicNamespaceName)).thenReturn(somePublicNamespaceName); + when(namespaceUtil.filterNamespaceName(somePublicNamespaceName)).thenReturn( + somePublicNamespaceName); + when(namespaceUtil.normalizeNamespace(someAppId, defaultNamespaceName)).thenReturn( + defaultNamespaceName); + when(namespaceUtil.normalizeNamespace(someAppId, somePublicNamespaceName)).thenReturn( + somePublicNamespaceName); someMessagesAsString = "someValidJson"; - when(configController.transformMessages(someMessagesAsString)).thenReturn(someNotificationMessages); + when(configController.transformMessages(someMessagesAsString)).thenReturn( + someNotificationMessages); } @Test @@ -113,7 +135,8 @@ public void testQueryConfig() throws Exception { String someServerSideNewReleaseKey = "2"; HttpServletResponse someResponse = mock(HttpServletResponse.class); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someServerSideNewReleaseKey); when(someRelease.getNamespaceName()).thenReturn(defaultNamespaceName); @@ -122,14 +145,16 @@ public void testQueryConfig() throws Exception { defaultNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); - verify(configService, times(1)).loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, + verify(configService, times(1)).loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); assertEquals(defaultNamespaceName, result.getNamespaceName()); assertEquals(someServerSideNewReleaseKey, result.getReleaseKey()); verify(instanceConfigAuditUtil, times(1)).audit(someAppId, someClusterName, someDataCenter, - someClientIp, someAppId, someClusterName, defaultNamespaceName, someServerSideNewReleaseKey); + someClientIp, someAppId, someClusterName, defaultNamespaceName, + someServerSideNewReleaseKey); } @Test @@ -139,17 +164,20 @@ public void testQueryConfigFile() throws Exception { HttpServletResponse someResponse = mock(HttpServletResponse.class); String someNamespaceName = String.format("%s.%s", defaultClusterName, "properties"); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someServerSideNewReleaseKey); when(namespaceUtil.filterNamespaceName(someNamespaceName)).thenReturn(defaultNamespaceName); - when(namespaceUtil.normalizeNamespace(someAppId, defaultNamespaceName)).thenReturn(defaultNamespaceName); + when(namespaceUtil.normalizeNamespace(someAppId, defaultNamespaceName)).thenReturn( + defaultNamespaceName); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, someNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); - verify(configService, times(1)).loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, + verify(configService, times(1)).loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); assertEquals(someAppId, result.getAppId()); assertEquals(someClusterName, result.getCluster()); @@ -166,11 +194,14 @@ public void testQueryConfigFileWithPrivateNamespace() throws Exception { String somePrivateNamespaceName = String.format("%s.%s", somePrivateNamespace, "xml"); AppNamespace appNamespace = mock(AppNamespace.class); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, somePrivateNamespace, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, somePrivateNamespace, someDataCenter, someNotificationMessages)).thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someServerSideNewReleaseKey); - when(namespaceUtil.filterNamespaceName(somePrivateNamespaceName)).thenReturn(somePrivateNamespace); - when(namespaceUtil.normalizeNamespace(someAppId, somePrivateNamespace)).thenReturn(somePrivateNamespace); + when(namespaceUtil.filterNamespaceName(somePrivateNamespaceName)).thenReturn( + somePrivateNamespace); + when(namespaceUtil.normalizeNamespace(someAppId, somePrivateNamespace)).thenReturn( + somePrivateNamespace); when(appNamespaceService.findByAppIdAndNamespace(someAppId, somePrivateNamespace)) .thenReturn(appNamespace); @@ -189,7 +220,8 @@ public void testQueryConfigWithReleaseNotFound() throws Exception { String someClientSideReleaseKey = "1"; HttpServletResponse someResponse = mock(HttpServletResponse.class); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(null); ApolloConfig result = configController.queryConfig(someAppId, someClusterName, @@ -206,13 +238,16 @@ public void testQueryConfigWithApolloConfigNotModified() throws Exception { String someServerSideReleaseKey = someClientSideReleaseKey; HttpServletResponse someResponse = mock(HttpServletResponse.class); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, defaultNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); ApolloConfig result = - configController.queryConfig(someAppId, someClusterName, defaultNamespaceName, someDataCenter, - someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); + configController.queryConfig(someAppId, someClusterName, defaultNamespaceName, + someDataCenter, + someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, + someRequest, someResponse); assertNull(result); verify(someResponse, times(1)).setStatus(HttpServletResponse.SC_NOT_MODIFIED); @@ -227,18 +262,22 @@ public void testQueryConfigWithAppOwnNamespace() throws Exception { AppNamespace someAppOwnNamespace = assemblePublicAppNamespace(someAppId, someAppOwnNamespaceName); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, someAppOwnNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, someAppOwnNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(someRelease); when(appNamespaceService.findPublicNamespaceByName(someAppOwnNamespaceName)) .thenReturn(someAppOwnNamespace); when(someRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); - when(namespaceUtil.filterNamespaceName(someAppOwnNamespaceName)).thenReturn(someAppOwnNamespaceName); - when(namespaceUtil.normalizeNamespace(someAppId, someAppOwnNamespaceName)).thenReturn(someAppOwnNamespaceName); + when(namespaceUtil.filterNamespaceName(someAppOwnNamespaceName)).thenReturn( + someAppOwnNamespaceName); + when(namespaceUtil.normalizeNamespace(someAppId, someAppOwnNamespaceName)).thenReturn( + someAppOwnNamespaceName); ApolloConfig result = configController .queryConfig(someAppId, someClusterName, someAppOwnNamespaceName, someDataCenter, - someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); + someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, + someRequest, someResponse); assertEquals(someServerSideReleaseKey, result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); @@ -257,11 +296,13 @@ public void testQueryConfigWithPubicNamespaceAndNoAppOverride() throws Exception AppNamespace somePublicAppNamespace = assemblePublicAppNamespace(somePublicAppId, somePublicNamespaceName); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, somePublicNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(null); when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) .thenReturn(somePublicAppNamespace); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, someClusterName, somePublicNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, + someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(somePublicRelease); when(somePublicRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); when(somePublicRelease.getAppId()).thenReturn(somePublicAppId); @@ -270,7 +311,8 @@ public void testQueryConfigWithPubicNamespaceAndNoAppOverride() throws Exception ApolloConfig result = configController .queryConfig(someAppId, someClusterName, somePublicNamespaceName, someDataCenter, - someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); + someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, + someRequest, someResponse); assertEquals(someServerSideReleaseKey, result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); @@ -278,7 +320,8 @@ public void testQueryConfigWithPubicNamespaceAndNoAppOverride() throws Exception assertEquals(somePublicNamespaceName, result.getNamespaceName()); assertEquals("foo", result.getConfigurations().get("apollo.public.bar")); verify(instanceConfigAuditUtil, times(1)).audit(someAppId, someClusterName, someDataCenter, - someClientIp, somePublicAppId, somePublicClusterName, somePublicNamespaceName, someServerSideReleaseKey); + someClientIp, somePublicAppId, somePublicClusterName, somePublicNamespaceName, + someServerSideReleaseKey); } @Test @@ -291,20 +334,26 @@ public void testQueryConfigFileWithPublicNamespaceAndNoAppOverride() throws Exce AppNamespace somePublicAppNamespace = assemblePublicAppNamespace(somePublicAppId, somePublicNamespaceName); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, somePublicNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(null); when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) .thenReturn(somePublicAppNamespace); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, someClusterName, somePublicNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, + someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(somePublicRelease); when(somePublicRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); when(namespaceUtil.filterNamespaceName(someNamespace)).thenReturn(somePublicNamespaceName); - when(namespaceUtil.normalizeNamespace(someAppId, somePublicNamespaceName)).thenReturn(somePublicNamespaceName); - when(appNamespaceService.findByAppIdAndNamespace(someAppId, somePublicNamespaceName)).thenReturn(null); + when(namespaceUtil.normalizeNamespace(someAppId, somePublicNamespaceName)).thenReturn( + somePublicNamespaceName); + when( + appNamespaceService.findByAppIdAndNamespace(someAppId, somePublicNamespaceName)).thenReturn( + null); ApolloConfig result = configController .queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, - someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); + someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, + someRequest, someResponse); assertEquals(someServerSideReleaseKey, result.getReleaseKey()); assertEquals(someAppId, result.getAppId()); @@ -327,13 +376,15 @@ public void testQueryConfigWithPublicNamespaceAndAppOverride() throws Exception when(somePublicRelease.getConfigurations()) .thenReturn("{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}"); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, someClusterName, somePublicNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(someRelease); when(someRelease.getReleaseKey()).thenReturn(someAppSideReleaseKey); when(someRelease.getNamespaceName()).thenReturn(somePublicNamespaceName); when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) .thenReturn(somePublicAppNamespace); - when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, someClusterName, somePublicNamespaceName, + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, + someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(somePublicRelease); when(somePublicRelease.getReleaseKey()).thenReturn(somePublicAppSideReleaseKey); when(somePublicRelease.getAppId()).thenReturn(somePublicAppId); @@ -343,7 +394,8 @@ public void testQueryConfigWithPublicNamespaceAndAppOverride() throws Exception ApolloConfig result = configController .queryConfig(someAppId, someClusterName, somePublicNamespaceName, someDataCenter, - someAppSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); + someAppSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, + someRequest, someResponse); assertEquals(Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR) .join(someAppSideReleaseKey, somePublicAppSideReleaseKey), @@ -356,7 +408,8 @@ public void testQueryConfigWithPublicNamespaceAndAppOverride() throws Exception verify(instanceConfigAuditUtil, times(1)).audit(someAppId, someClusterName, someDataCenter, someClientIp, someAppId, someClusterName, somePublicNamespaceName, someAppSideReleaseKey); verify(instanceConfigAuditUtil, times(1)).audit(someAppId, someClusterName, someDataCenter, - someClientIp, somePublicAppId, someDataCenter, somePublicNamespaceName, somePublicAppSideReleaseKey); + someClientIp, somePublicAppId, someDataCenter, somePublicNamespaceName, + somePublicAppSideReleaseKey); } @Test @@ -406,7 +459,8 @@ public void testQueryConfigForNoAppIdPlaceHolder() throws Exception { defaultNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); - verify(configService, never()).loadConfig(appId, someClientIp, someAppId, someClientLabel, someClusterName, defaultNamespaceName, + verify(configService, never()).loadConfig(appId, someClientIp, someAppId, someClientLabel, + someClusterName, defaultNamespaceName, someDataCenter, someNotificationMessages); verify(appNamespaceService, never()).findPublicNamespaceByName(defaultNamespaceName); assertNull(result); @@ -425,16 +479,19 @@ public void testQueryConfigForNoAppIdPlaceHolderWithPublicNamespace() throws Exc when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) .thenReturn(somePublicAppNamespace); - when(configService.loadConfig(appId, someClientIp, someClientLabel, somePublicAppId, someClusterName, somePublicNamespaceName, + when(configService.loadConfig(appId, someClientIp, someClientLabel, somePublicAppId, + someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages)).thenReturn(somePublicRelease); when(somePublicRelease.getReleaseKey()).thenReturn(someServerSideReleaseKey); - when(namespaceUtil.normalizeNamespace(appId, somePublicNamespaceName)).thenReturn(somePublicNamespaceName); + when(namespaceUtil.normalizeNamespace(appId, somePublicNamespaceName)).thenReturn( + somePublicNamespaceName); ApolloConfig result = configController.queryConfig(appId, someClusterName, somePublicNamespaceName, someDataCenter, someClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); - verify(configService, never()).loadConfig(appId, someClientIp, someClientLabel, appId, someClusterName, + verify(configService, never()).loadConfig(appId, someClientIp, someClientLabel, appId, + someClusterName, somePublicNamespaceName, someDataCenter, someNotificationMessages); assertEquals(someServerSideReleaseKey, result.getReleaseKey()); assertEquals(appId, result.getAppId()); @@ -478,4 +535,142 @@ private AppNamespace assembleAppNamespace(String appId, String namespace, boolea appNamespace.setPublic(isPublic); return appNamespace; } + + @Test + public void testQueryConfigWithIncrementalSync() throws Exception { + when(bizConfig.isConfigServiceChangeCacheEnabled()) + .thenReturn(true); + String clientSideReleaseKey = "1"; + String someConfigurations = "{\"apollo.public.foo\": \"foo\"}"; + HttpServletResponse someResponse = mock(HttpServletResponse.class); + Map someReleaseMap = mock(Map.class); + + String anotherConfigurations = "{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}"; + + when(configService.findReleasesByReleaseKeys(Sets.newHashSet(clientSideReleaseKey))).thenReturn( + someReleaseMap); + when(someReleaseMap.get(clientSideReleaseKey)).thenReturn(someRelease); + when(someRelease.getConfigurations()).thenReturn(someConfigurations); + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, defaultNamespaceName, + someDataCenter, someNotificationMessages)).thenReturn(anotherRelease); + when(anotherRelease.getNamespaceName()).thenReturn(defaultNamespaceName); + when(anotherRelease.getConfigurations()).thenReturn(anotherConfigurations); + + List configurationChanges = new ArrayList<>(); + configurationChanges.add(new ConfigurationChange("apollo.public.bar", "bar", "ADDED")); + when(configService.calcConfigurationChanges( + gson.fromJson(anotherConfigurations, configurationTypeReference), + gson.fromJson(someConfigurations, configurationTypeReference))) + .thenReturn(configurationChanges); + + ApolloConfig anotherResult = configController.queryConfig(someAppId, someClusterName, + defaultNamespaceName, someDataCenter, clientSideReleaseKey, + someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); + assertEquals(ConfigSyncType.INCREMENTALSYNC.getValue(), anotherResult.getConfigSyncType()); + assertEquals(configurationChanges, anotherResult.getConfigurationChanges()); + + } + + @Test + public void testQueryConfigWithIncrementalSyncNotFound() throws Exception { + when(bizConfig.isConfigServiceChangeCacheEnabled()) + .thenReturn(true); + + String someClientSideReleaseKey = "1"; + String someServerSideNewReleaseKey = "2"; + HttpServletResponse someResponse = mock(HttpServletResponse.class); + + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, defaultNamespaceName, + someDataCenter, someNotificationMessages)).thenReturn(someRelease); + when(configService.findReleasesByReleaseKeys( + Sets.newHashSet(someClientSideReleaseKey))).thenReturn(null); + + when(someRelease.getReleaseKey()).thenReturn(someServerSideNewReleaseKey); + when(someRelease.getNamespaceName()).thenReturn(defaultNamespaceName); + String configurations = "{\"apollo.public.foo\": \"foo\"}"; + when(someRelease.getConfigurations()).thenReturn(configurations); + + ApolloConfig result = configController.queryConfig(someAppId, someClusterName, + defaultNamespaceName, someDataCenter, someClientSideReleaseKey, + someClientIp, someClientLabel, someMessagesAsString, someRequest, someResponse); + assertEquals(1, result.getConfigurations().size()); + assertEquals("foo", result.getConfigurations().get("apollo.public.foo")); + } + + @Test + public void testQueryConfigWithIncrementalSyncPublicNamespaceAndAppOverride() throws Exception { + when(bizConfig.isConfigServiceChangeCacheEnabled()) + .thenReturn(true); + String someAppClientSideReleaseKey = "1"; + String somePublicAppClientSideReleaseKey = "2"; + String someConfigurations = "{\"apollo.public.foo.client\": \"foo.override\"}"; + String somePublicConfigurations = "{\"apollo.public.foo.client\": \"foo\"}"; + Map someReleaseMap = mock(Map.class); + Release somePublicRelease = mock(Release.class); + + when(configService.findReleasesByReleaseKeys(Sets.newHashSet(someAppClientSideReleaseKey, + somePublicAppClientSideReleaseKey))).thenReturn(someReleaseMap); + when(someReleaseMap.get(someAppClientSideReleaseKey)).thenReturn(someRelease); + when(someReleaseMap.get(somePublicAppClientSideReleaseKey)).thenReturn(somePublicRelease); + when(someRelease.getConfigurations()).thenReturn(someConfigurations); + when(somePublicRelease.getConfigurations()).thenReturn(somePublicConfigurations); + + String someAppServerSideReleaseKey = "3"; + String somePublicAppSideReleaseKey = "4"; + + HttpServletResponse someResponse = mock(HttpServletResponse.class); + String somePublicAppId = "somePublicAppId"; + AppNamespace somePublicAppNamespace = + assemblePublicAppNamespace(somePublicAppId, somePublicNamespaceName); + + when(anotherRelease.getConfigurations()).thenReturn( + "{\"apollo.public.foo\": \"foo-override\"}"); + when(anotherPublicRelease.getConfigurations()) + .thenReturn("{\"apollo.public.foo\": \"foo\", \"apollo.public.bar\": \"bar\"}"); + + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, someAppId, + someClusterName, somePublicNamespaceName, + someDataCenter, someNotificationMessages)).thenReturn(anotherRelease); + when(anotherRelease.getReleaseKey()).thenReturn(someAppServerSideReleaseKey); + when(anotherRelease.getNamespaceName()).thenReturn(somePublicNamespaceName); + when(appNamespaceService.findPublicNamespaceByName(somePublicNamespaceName)) + .thenReturn(somePublicAppNamespace); + when(configService.loadConfig(someAppId, someClientIp, someClientLabel, somePublicAppId, + someClusterName, somePublicNamespaceName, + someDataCenter, someNotificationMessages)).thenReturn(anotherPublicRelease); + when(anotherPublicRelease.getReleaseKey()).thenReturn(somePublicAppSideReleaseKey); + when(anotherPublicRelease.getAppId()).thenReturn(somePublicAppId); + when(anotherPublicRelease.getClusterName()).thenReturn(someDataCenter); + when(anotherPublicRelease.getNamespaceName()).thenReturn(somePublicNamespaceName); + + String mergeServerSideConfigurations = "{\"apollo.public.bar\": \"bar\",\"apollo.public.foo\": \"foo-override\"}"; + String mergeClientSideConfigurations = "{\"apollo.public.foo.client\": \"foo.override\"}"; + List configurationChanges = new ArrayList<>(); + configurationChanges.add(new ConfigurationChange("apollo.public.bar", "bar", "ADDED")); + configurationChanges.add(new ConfigurationChange("apollo.public.foo", "foo-override", "ADDED")); + configurationChanges.add(new ConfigurationChange("apollo.public.foo.client", null, "DELETED")); + when(configService.calcConfigurationChanges( + gson.fromJson(mergeServerSideConfigurations, configurationTypeReference), + gson.fromJson(mergeClientSideConfigurations, configurationTypeReference))) + .thenReturn(configurationChanges); + + String mergeClientSideReleaseKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR) + .join(someAppClientSideReleaseKey, somePublicAppClientSideReleaseKey); + ApolloConfig result = configController.queryConfig(someAppId, someClusterName, + somePublicNamespaceName, someDataCenter, + mergeClientSideReleaseKey, someClientIp, someClientLabel, someMessagesAsString, someRequest, + someResponse); + + assertEquals(Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR) + .join(someAppServerSideReleaseKey, somePublicAppSideReleaseKey), + result.getReleaseKey()); + assertEquals(ConfigSyncType.INCREMENTALSYNC.getValue(), result.getConfigSyncType()); + assertEquals(configurationChanges, result.getConfigurationChanges()); + } + + + private static final Type configurationTypeReference = new TypeToken>() { + }.getType(); } diff --git a/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCacheTest.java b/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCacheTest.java new file mode 100644 index 00000000000..68db36dd5a1 --- /dev/null +++ b/apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/config/ConfigServiceWithChangeCacheTest.java @@ -0,0 +1,246 @@ +/* + * Copyright 2024 Apollo Authors + * + * 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.ctrip.framework.apollo.configservice.service.config; + +import com.ctrip.framework.apollo.biz.config.BizConfig; +import com.ctrip.framework.apollo.biz.entity.Release; +import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; +import com.ctrip.framework.apollo.biz.grayReleaseRule.GrayReleaseRulesHolder; +import com.ctrip.framework.apollo.biz.message.Topics; +import com.ctrip.framework.apollo.biz.service.ReleaseMessageService; +import com.ctrip.framework.apollo.biz.service.ReleaseService; +import com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator; +import com.ctrip.framework.apollo.core.dto.ConfigurationChange; +import com.ctrip.framework.apollo.core.enums.ConfigurationChangeType; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.*; + +/** + * @author jason + */ +@RunWith(MockitoJUnitRunner.class) +public class ConfigServiceWithChangeCacheTest { + + private ConfigServiceWithChangeCache configServiceWithChangeCache; + + @Mock + private ReleaseService releaseService; + + @Mock + private ReleaseMessageService releaseMessageService; + @Mock + private Release someRelease; + @Mock + private BizConfig bizConfig; + @Mock + private MeterRegistry meterRegistry; + @Mock + private GrayReleaseRulesHolder grayReleaseRulesHolder; + + private String someKey; + + private String someReleaseKey; + + private String someAppId; + private String someClusterName; + private String someNamespaceName; + + + @Before + public void setUp() throws Exception { + configServiceWithChangeCache = new ConfigServiceWithChangeCache(releaseService, + releaseMessageService, + grayReleaseRulesHolder, bizConfig, meterRegistry); + + configServiceWithChangeCache.initialize(); + + someReleaseKey = "someReleaseKey"; + someAppId = "someAppId"; + someClusterName = "someClusterName"; + someNamespaceName = "someNamespaceName"; + + someKey = ReleaseMessageKeyGenerator.generate(someAppId, someClusterName, someNamespaceName); + + } + + @Test + public void testChangeConfigurationsWithAdd() { + String key1 = "key1"; + String value1 = "value1"; + + String key2 = "key2"; + String value2 = "value2"; + + Map latestConfig = ImmutableMap.of(key1, value1, key2, value2); + Map historyConfig = ImmutableMap.of(key1, value1); + + List result = + configServiceWithChangeCache.calcConfigurationChanges(latestConfig, historyConfig); + + assertEquals(1, result.size()); + assertEquals(key2, result.get(0).getKey()); + assertEquals(value2, result.get(0).getNewValue()); + assertEquals("ADDED", result.get(0).getConfigurationChangeType()); + } + + @Test + public void testChangeConfigurationsWithLatestConfigIsNULL() { + String key1 = "key1"; + String value1 = "value1"; + + Map historyConfig = ImmutableMap.of(key1, value1); + + List result = + configServiceWithChangeCache.calcConfigurationChanges(null, historyConfig); + + assertEquals(1, result.size()); + assertEquals(key1, result.get(0).getKey()); + assertEquals(null, result.get(0).getNewValue()); + assertEquals("DELETED", result.get(0).getConfigurationChangeType()); + } + + @Test + public void testChangeConfigurationsWithHistoryConfigIsNULL() { + String key1 = "key1"; + String value1 = "value1"; + + Map latestConfig = ImmutableMap.of(key1, value1); + + List result = + configServiceWithChangeCache.calcConfigurationChanges(latestConfig, null); + + assertEquals(1, result.size()); + assertEquals(key1, result.get(0).getKey()); + assertEquals(value1, result.get(0).getNewValue()); + assertEquals("ADDED", result.get(0).getConfigurationChangeType()); + } + + @Test + public void testChangeConfigurationsWithUpdate() { + String key1 = "key1"; + String value1 = "value1"; + + String anotherValue1 = "anotherValue1"; + + Map latestConfig = ImmutableMap.of(key1, anotherValue1); + Map historyConfig = ImmutableMap.of(key1, value1); + + List result = + configServiceWithChangeCache.calcConfigurationChanges(latestConfig, historyConfig); + + assertEquals(1, result.size()); + assertEquals(key1, result.get(0).getKey()); + assertEquals(anotherValue1, result.get(0).getNewValue()); + assertEquals("MODIFIED", result.get(0).getConfigurationChangeType()); + } + + @Test + public void testChangeConfigurationsWithDelete() { + String key1 = "key1"; + String value1 = "value1"; + + Map latestConfig = ImmutableMap.of(); + Map historyConfig = ImmutableMap.of(key1, value1); + + List result = + configServiceWithChangeCache.calcConfigurationChanges(latestConfig, historyConfig); + + assertEquals(1, result.size()); + assertEquals(key1, result.get(0).getKey()); + assertEquals(null, result.get(0).getNewValue()); + assertEquals("DELETED", result.get(0).getConfigurationChangeType()); + } + + @Test + public void testFindReleasesByReleaseKeys() { + when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn + (someRelease); + + Map someReleaseMap = configServiceWithChangeCache.findReleasesByReleaseKeys( + Sets.newHashSet(someReleaseKey)); + Map anotherReleaseMap = configServiceWithChangeCache.findReleasesByReleaseKeys( + Sets.newHashSet(someReleaseKey)); + + int retryTimes = 100; + + for (int i = 0; i < retryTimes; i++) { + configServiceWithChangeCache.findReleasesByReleaseKeys(Sets.newHashSet(someReleaseKey)); + } + + assertEquals(someRelease, someReleaseMap.get(someReleaseKey)); + assertEquals(someRelease, anotherReleaseMap.get(someReleaseKey)); + + verify(releaseService, times(1)).findByReleaseKey(someReleaseKey); + } + + @Test + public void testFindReleasesByReleaseKeysWithReleaseNotFound() { + when(releaseService.findByReleaseKey(someReleaseKey)).thenReturn + (null); + + Map someReleaseMap = configServiceWithChangeCache.findReleasesByReleaseKeys( + Sets.newHashSet(someReleaseKey)); + Map anotherReleaseMap = configServiceWithChangeCache.findReleasesByReleaseKeys( + Sets.newHashSet(someReleaseKey)); + + int retryTimes = 100; + + for (int i = 0; i < retryTimes; i++) { + configServiceWithChangeCache.findReleasesByReleaseKeys(Sets.newHashSet(someReleaseKey)); + } + + assertNull(someReleaseMap); + assertNull(anotherReleaseMap); + + verify(releaseService, times(1)).findByReleaseKey(someReleaseKey); + } + + @Test + public void testFindReleasesByReleaseKeysWithReleaseMessageNotification() { + ReleaseMessage someReleaseMessage = mock(ReleaseMessage.class); + + when(releaseService.findLatestActiveRelease(someAppId, someClusterName, + someNamespaceName)).thenReturn(someRelease); + when(someReleaseMessage.getMessage()).thenReturn(someKey); + when(someRelease.getReleaseKey()).thenReturn(someReleaseKey); + + configServiceWithChangeCache.handleMessage(someReleaseMessage, Topics.APOLLO_RELEASE_TOPIC); + Map someReleaseMap = configServiceWithChangeCache.findReleasesByReleaseKeys( + Sets.newHashSet(someReleaseKey)); + Map anotherReleaseMap = configServiceWithChangeCache.findReleasesByReleaseKeys( + Sets.newHashSet(someReleaseKey)); + + assertEquals(someRelease, someReleaseMap.get(someReleaseKey)); + assertEquals(someRelease, anotherReleaseMap.get(someReleaseKey)); + + verify(releaseService, times(0)).findByReleaseKey(someKey); + } + +} diff --git a/changes/changes-2.4.0.md b/changes/changes-2.4.0.md index 3ebe2e98381..80afb80a4be 100644 --- a/changes/changes-2.4.0.md +++ b/changes/changes-2.4.0.md @@ -9,6 +9,8 @@ Apollo 2.4.0 * [Feature support portal restTemplate Client connection pool config](https://github.com/apolloconfig/apollo/pull/5200) * [Feature added the ability for administrators to globally search for Value](https://github.com/apolloconfig/apollo/pull/5182) * [Feature support the observe status access-key for pre-check and logging only](https://github.com/apolloconfig/apollo/pull/5236) +* [Feature support incremental configuration synchronization client](https://github.com/apolloconfig/apollo/pull/5288) + ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1) diff --git a/docs/en/deployment/distributed-deployment-guide.md b/docs/en/deployment/distributed-deployment-guide.md index 91afbe7a4fb..50c00a8c294 100644 --- a/docs/en/deployment/distributed-deployment-guide.md +++ b/docs/en/deployment/distributed-deployment-guide.md @@ -1637,3 +1637,20 @@ json } ``` The above configuration specifies that the retention size for release history of appId=kl, clusterName=bj, namespaceName=namespace1, and branchName=bj is 10, and the retention size for release history of appId=kl, clusterName=bj, namespaceName=namespace2, and branchName=bj is 20. In general, branchName equals clusterName. It is only different during gray release, where the branchName needs to be confirmed by querying the ReleaseHistory table in the database. + +### 3.2.14 config-service.incremental.change.enabled - whether to enable incremental configuration synchronization client + +> for server versions 2.4.0 and above && client versions 2.3.0 and above + +This is a function switch, if configured to true,config Service will cache previously loaded +configuration information and send incremental updates to the client, reducing network pressure on +the server + +The default is false. Please evaluate the total configuration size and adjust the config service +memory configuration before turning it on. + +> Ensure that the `app.id`、`apollo.cluster` of the configuration in the application is in the correct case when caching is enabled, otherwise it will not fetch the correct configuration, You can also refer to the `config-service.cache.key.ignore-case` configuration for compatibility processing. + +> `config-service.cache.enabled` configuration adjustment requires a restart of the config service to take effect + + diff --git a/docs/zh/deployment/distributed-deployment-guide.md b/docs/zh/deployment/distributed-deployment-guide.md index 6e34a9f664e..b6f74a1cd77 100644 --- a/docs/zh/deployment/distributed-deployment-guide.md +++ b/docs/zh/deployment/distributed-deployment-guide.md @@ -1573,3 +1573,16 @@ json } ``` 以上配置指定了 appId=kl、clusterName=bj、namespaceName=namespace1、branchName=bj 的发布历史保留数量为 10,appId=kl、clusterName=bj、namespaceName=namespace2、branchName=bj 的发布历史保留数量为 20,branchName 一般等于 clusterName,只有灰度发布时才会不同,灰度发布的 branchName 需要查询数据库 ReleaseHistory 表确认。 + +### 3.2.14 config-service.incremental.change.enabled - 是否开启增量配置同步客户端 + +> 适用于服务端2.4.0及以上版本 && 客户端2.3.0及以上版本 + +这是一个功能开关,如果配置为true的话,config service会缓存加载过的配置信息,发送给客户端增量配置,减少客户端对服务端的网络压力。 + +默认为false,开启前请先评估总配置大小并调整config service内存配置。 + +> 开启缓存后必须确保应用中配置的`app.id`、`apollo.cluster` +> 大小写正确,否则将获取不到正确的配置,另可参考`config-service.cache.key.ignore-case`配置做兼容处理。 + +> `config-service.incremental.change.enabled` 配置调整必须重启 config service 才能生效 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 93db6c18277..bbc6f98b0bd 100644 --- a/pom.xml +++ b/pom.xml @@ -200,11 +200,11 @@ commons-lang3 ${common-lang3.version} - + com.thoughtworks.xstream xstream - 1.4.21 + 1.4.20 @@ -649,4 +649,4 @@ - + \ No newline at end of file diff --git a/scripts/sql/profiles/h2-default/apolloconfigdb.sql b/scripts/sql/profiles/h2-default/apolloconfigdb.sql index 4fa4f92fa4e..dbf4c041ca1 100644 --- a/scripts/sql/profiles/h2-default/apolloconfigdb.sql +++ b/scripts/sql/profiles/h2-default/apolloconfigdb.sql @@ -483,8 +483,8 @@ VALUES ('namespace.lock.switch', 'default', 'false', '一次发布只能有一个人修改开关'), ('item.key.length.limit', 'default', '128', 'item key 最大长度限制'), ('item.value.length.limit', 'default', '20000', 'item value最大长度限制'), - ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!'); - + ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!'), + ('config-service.incremental.change.enabled', 'default', 'false', 'ConfigService是否开启增量配置同步客户端,开启后能提高性能,但是会增大内存消耗!'); -- -- =============================================================================== -- == == diff --git a/scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql b/scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql index be5d8604d00..ed2be3caac9 100644 --- a/scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql +++ b/scripts/sql/profiles/mysql-database-not-specified/apolloconfigdb.sql @@ -497,7 +497,8 @@ VALUES ('namespace.lock.switch', 'default', 'false', '一次发布只能有一个人修改开关'), ('item.key.length.limit', 'default', '128', 'item key 最大长度限制'), ('item.value.length.limit', 'default', '20000', 'item value最大长度限制'), - ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!'); + ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!'), + ('config-service.incremental.change.enabled', 'default', 'false', 'ConfigService是否开启客户端同步增量配置,开启后能提高性能,但是会增大内存消耗!'); -- -- =============================================================================== diff --git a/scripts/sql/profiles/mysql-default/apolloconfigdb.sql b/scripts/sql/profiles/mysql-default/apolloconfigdb.sql index 4da37d8010a..c7a797e6e3b 100644 --- a/scripts/sql/profiles/mysql-default/apolloconfigdb.sql +++ b/scripts/sql/profiles/mysql-default/apolloconfigdb.sql @@ -502,7 +502,8 @@ VALUES ('namespace.lock.switch', 'default', 'false', '一次发布只能有一个人修改开关'), ('item.key.length.limit', 'default', '128', 'item key 最大长度限制'), ('item.value.length.limit', 'default', '20000', 'item value最大长度限制'), - ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!'); + ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!'), + ('config-service.incremental.change.enabled', 'default', 'false', 'ConfigService是否开启增量配置同步客户端,开启后能提高性能,但是会增大内存消耗!'); -- -- =============================================================================== diff --git a/scripts/sql/src/apolloconfigdb.sql b/scripts/sql/src/apolloconfigdb.sql index f6c847a77ce..1353cc711fb 100644 --- a/scripts/sql/src/apolloconfigdb.sql +++ b/scripts/sql/src/apolloconfigdb.sql @@ -490,8 +490,8 @@ VALUES ('namespace.lock.switch', 'default', 'false', '一次发布只能有一个人修改开关'), ('item.key.length.limit', 'default', '128', 'item key 最大长度限制'), ('item.value.length.limit', 'default', '20000', 'item value最大长度限制'), - ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!'); - + ('config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!'), + ('config-service.incremental.change.enabled', 'default', 'false', 'ConfigService是否开启增量配置同步客户端,开启后能提高性能,但是会增大内存消耗!'); -- ${gists.autoGeneratedDeclaration} /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;