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