package com.ctrip.framework.apollo.configservice.service; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; 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.ctrip.framework.apollo.biz.config.BizConfig; import com.ctrip.framework.apollo.biz.repository.AppNamespaceRepository; import com.ctrip.framework.apollo.common.entity.AppNamespace; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import com.ctrip.framework.apollo.tracer.spi.Transaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class AppNamespaceServiceWithCache implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(AppNamespaceServiceWithCache.class); private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR) .skipNulls(); @Autowired private AppNamespaceRepository appNamespaceRepository; @Autowired private BizConfig bizConfig; private int scanInterval; private TimeUnit scanIntervalTimeUnit; private int rebuildInterval; private TimeUnit rebuildIntervalTimeUnit; private ScheduledExecutorService scheduledExecutorService; private long maxIdScanned; //store namespaceName -> AppNamespace private Map<String, AppNamespace> publicAppNamespaceCache; //store appId+namespaceName -> AppNamespace private Map<String, AppNamespace> appNamespaceCache; //store id -> AppNamespace private Map<Long, AppNamespace> appNamespaceIdCache; public AppNamespaceServiceWithCache() { maxIdScanned = 0; publicAppNamespaceCache = Maps.newConcurrentMap(); appNamespaceCache = Maps.newConcurrentMap(); appNamespaceIdCache = Maps.newConcurrentMap(); scheduledExecutorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory .create("AppNamespaceServiceWithCache", true)); } public List<AppNamespace> findByAppIdAndNamespaces(String appId, Set<String> namespaceNames) { Preconditions.checkArgument(!Strings.isNullOrEmpty(appId), "appId must not be null"); if (namespaceNames == null || namespaceNames.isEmpty()) { return Collections.emptyList(); } // return appNamespaceRepository.findByAppIdAndNameIn(appId, namespaceNames); List<AppNamespace> result = Lists.newArrayList(); for (String namespaceName : namespaceNames) { AppNamespace appNamespace = appNamespaceCache.get(STRING_JOINER.join(appId, namespaceName)); if (appNamespace != null) { result.add(appNamespace); } } return result; } public List<AppNamespace> findPublicNamespacesByNames(Set<String> namespaceNames) { if (namespaceNames == null || namespaceNames.isEmpty()) { return Collections.emptyList(); } // return appNamespaceRepository.findByNameInAndIsPublicTrue(namespaceNames); List<AppNamespace> result = Lists.newArrayList(); for (String namespaceName : namespaceNames) { AppNamespace appNamespace = publicAppNamespaceCache.get(namespaceName); if (appNamespace != null) { result.add(appNamespace); } } return result; } @Override public void afterPropertiesSet() throws Exception { populateDataBaseInterval(); scanNewAppNamespaces(); //block the startup process until load finished scheduledExecutorService.scheduleAtFixedRate(() -> { Transaction transaction = Tracer.newTransaction("Apollo.AppNamespaceServiceWithCache", "rebuildCache"); try { this.updateAndDeleteCache(); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Rebuild cache failed", ex); } finally { transaction.complete(); } }, rebuildInterval, rebuildInterval, rebuildIntervalTimeUnit); scheduledExecutorService.scheduleWithFixedDelay(this::scanNewAppNamespaces, scanInterval, scanInterval, scanIntervalTimeUnit); } private void scanNewAppNamespaces() { Transaction transaction = Tracer.newTransaction("Apollo.AppNamespaceServiceWithCache", "scanNewAppNamespaces"); try { this.loadNewAppNamespaces(); transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { transaction.setStatus(ex); logger.error("Load new app namespaces failed", ex); } finally { transaction.complete(); } } //for those new app namespaces private void loadNewAppNamespaces() { boolean hasMore = true; while (hasMore && !Thread.currentThread().isInterrupted()) { //current batch is 500 List<AppNamespace> appNamespaces = appNamespaceRepository .findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned); if (CollectionUtils.isEmpty(appNamespaces)) { break; } mergeAppNamespaces(appNamespaces); int scanned = appNamespaces.size(); maxIdScanned = appNamespaces.get(scanned - 1).getId(); hasMore = scanned == 500; logger.info("Loaded {} new app namespaces with startId {}", scanned, maxIdScanned); } } private void mergeAppNamespaces(List<AppNamespace> appNamespaces) { for (AppNamespace appNamespace : appNamespaces) { appNamespaceCache.put(assembleAppNamespaceKey(appNamespace), appNamespace); appNamespaceIdCache.put(appNamespace.getId(), appNamespace); if (appNamespace.isPublic()) { publicAppNamespaceCache.put(appNamespace.getName(), appNamespace); } } } //for those updated or deleted app namespaces private void updateAndDeleteCache() { List<Long> ids = Lists.newArrayList(appNamespaceIdCache.keySet()); if (CollectionUtils.isEmpty(ids)) { return; } List<List<Long>> partitionIds = Lists.partition(ids, 500); for (List<Long> toRebuild : partitionIds) { Iterable<AppNamespace> appNamespaces = appNamespaceRepository.findAll(toRebuild); if (appNamespaces == null) { continue; } //handle updated Set<Long> foundIds = handleUpdatedAppNamespaces(appNamespaces); //handle deleted handleDeletedAppNamespaces(Sets.difference(Sets.newHashSet(toRebuild), foundIds)); } } //for those updated app namespaces private Set<Long> handleUpdatedAppNamespaces(Iterable<AppNamespace> appNamespaces) { Set<Long> foundIds = Sets.newHashSet(); for (AppNamespace appNamespace : appNamespaces) { foundIds.add(appNamespace.getId()); AppNamespace thatInCache = appNamespaceIdCache.get(appNamespace.getId()); if (thatInCache != null && appNamespace.getDataChangeLastModifiedTime().after(thatInCache .getDataChangeLastModifiedTime())) { appNamespaceIdCache.put(appNamespace.getId(), appNamespace); String oldKey = assembleAppNamespaceKey(thatInCache); String newKey = assembleAppNamespaceKey(appNamespace); appNamespaceCache.put(newKey, appNamespace); //in case appId or namespaceName changes if (!newKey.equals(oldKey)) { appNamespaceCache.remove(oldKey); } if (appNamespace.isPublic()) { publicAppNamespaceCache.put(appNamespace.getName(), appNamespace); //in case namespaceName changes if (!appNamespace.getName().equals(thatInCache.getName()) && thatInCache.isPublic()) { publicAppNamespaceCache.remove(thatInCache.getName()); } } else if (thatInCache.isPublic()) { //just in case isPublic changes publicAppNamespaceCache.remove(thatInCache.getName()); } logger.info("Found AppNamespace changes, old: {}, new: {}", thatInCache, appNamespace); } } return foundIds; } //for those deleted app namespaces private void handleDeletedAppNamespaces(Set<Long> deletedIds) { if (CollectionUtils.isEmpty(deletedIds)) { return; } for (Long deletedId : deletedIds) { AppNamespace deleted = appNamespaceIdCache.remove(deletedId); if (deleted == null) { continue; } appNamespaceCache.remove(assembleAppNamespaceKey(deleted)); if (deleted.isPublic()) { publicAppNamespaceCache.remove(deleted.getName()); } logger.info("Found AppNamespace deleted, {}", deleted); } } private String assembleAppNamespaceKey(AppNamespace appNamespace) { return STRING_JOINER.join(appNamespace.getAppId(), appNamespace.getName()); } private void populateDataBaseInterval() { scanInterval = bizConfig.appNamespaceCacheScanInterval(); scanIntervalTimeUnit = bizConfig.appNamespaceCacheScanIntervalTimeUnit(); rebuildInterval = bizConfig.appNamespaceCacheRebuildInterval(); rebuildIntervalTimeUnit = bizConfig.appNamespaceCacheRebuildIntervalTimeUnit(); } }