package com.ctrip.framework.apollo.biz.grayReleaseRule;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.ctrip.framework.apollo.biz.config.BizConfig;
import com.ctrip.framework.apollo.biz.entity.GrayReleaseRule;
import com.ctrip.framework.apollo.biz.entity.ReleaseMessage;
import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener;
import com.ctrip.framework.apollo.biz.message.Topics;
import com.ctrip.framework.apollo.biz.repository.GrayReleaseRuleRepository;
import com.ctrip.framework.apollo.common.constants.NamespaceBranchStatus;
import com.ctrip.framework.apollo.common.dto.GrayReleaseRuleItemDTO;
import com.ctrip.framework.apollo.common.utils.GrayReleaseRuleItemTransformer;
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.util.CollectionUtils;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class GrayReleaseRulesHolder implements ReleaseMessageListener, InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(GrayReleaseRulesHolder.class);
private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);
private static final Splitter STRING_SPLITTER =
Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings();
@Autowired
private GrayReleaseRuleRepository grayReleaseRuleRepository;
@Autowired
private BizConfig bizConfig;
private int databaseScanInterval;
private ScheduledExecutorService executorService;
//store configAppId+configCluster+configNamespace -> GrayReleaseRuleCache map
private Multimap<String, GrayReleaseRuleCache> grayReleaseRuleCache;
//store clientAppId+clientNamespace+ip -> ruleId map
private Multimap<String, Long> reversedGrayReleaseRuleCache;
//an auto increment version to indicate the age of rules
private AtomicLong loadVersion;
public GrayReleaseRulesHolder() {
loadVersion = new AtomicLong();
grayReleaseRuleCache = Multimaps.synchronizedSetMultimap(HashMultimap.create());
reversedGrayReleaseRuleCache = Multimaps.synchronizedSetMultimap(HashMultimap.create());
executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory
.create("GrayReleaseRulesHolder", true));
}
@Override
public void afterPropertiesSet() throws Exception {
populateDataBaseInterval();
//force sync load for the first time
periodicScanRules();
executorService.scheduleWithFixedDelay(this::periodicScanRules,
getDatabaseScanIntervalSecond(), getDatabaseScanIntervalSecond(), getDatabaseScanTimeUnit()
);
}
@Override
public void handleMessage(ReleaseMessage message, String channel) {
logger.info("message received - channel: {}, message: {}", channel, message);
String releaseMessage = message.getMessage();
if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(releaseMessage)) {
return;
}
List<String> keys = STRING_SPLITTER.splitToList(releaseMessage);
//message should be appId+cluster+namespace
if (keys.size() != 3) {
logger.error("message format invalid - {}", releaseMessage);
return;
}
String appId = keys.get(0);
String cluster = keys.get(1);
String namespace = keys.get(2);
List<GrayReleaseRule> rules = grayReleaseRuleRepository
.findByAppIdAndClusterNameAndNamespaceName(appId, cluster, namespace);
mergeGrayReleaseRules(rules);
}
private void periodicScanRules() {
Transaction transaction = Tracer.newTransaction("Apollo.GrayReleaseRulesScanner",
"scanGrayReleaseRules");
try {
loadVersion.incrementAndGet();
scanGrayReleaseRules();
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
logger.error("Scan gray release rule failed", ex);
} finally {
transaction.complete();
}
}
public Long findReleaseIdFromGrayReleaseRule(String clientAppId, String clientIp, String
configAppId, String configCluster, String configNamespaceName) {
String key = assembleGrayReleaseRuleKey(configAppId, configCluster, configNamespaceName);
if (!grayReleaseRuleCache.containsKey(key)) {
return null;
}
//create a new list to avoid ConcurrentModificationException
List<GrayReleaseRuleCache> rules = Lists.newArrayList(grayReleaseRuleCache.get(key));
for (GrayReleaseRuleCache rule : rules) {
//check branch status
if (rule.getBranchStatus() != NamespaceBranchStatus.ACTIVE) {
continue;
}
if (rule.matches(clientAppId, clientIp)) {
return rule.getReleaseId();
}
}
return null;
}
/**
* Check whether there are gray release rules for the clientAppId, clientIp, namespace
* combination. Please note that even there are gray release rules, it doesn't mean it will always
* load gray releases. Because gray release rules actually apply to one more dimension - cluster.
*/
public boolean hasGrayReleaseRule(String clientAppId, String clientIp, String namespaceName) {
return reversedGrayReleaseRuleCache.containsKey(assembleReversedGrayReleaseRuleKey(clientAppId,
namespaceName, clientIp)) || reversedGrayReleaseRuleCache.containsKey
(assembleReversedGrayReleaseRuleKey(clientAppId, namespaceName, GrayReleaseRuleItemDTO
.ALL_IP));
}
private void scanGrayReleaseRules() {
long maxIdScanned = 0;
boolean hasMore = true;
while (hasMore && !Thread.currentThread().isInterrupted()) {
List<GrayReleaseRule> grayReleaseRules = grayReleaseRuleRepository
.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
if (CollectionUtils.isEmpty(grayReleaseRules)) {
break;
}
mergeGrayReleaseRules(grayReleaseRules);
int rulesScanned = grayReleaseRules.size();
maxIdScanned = grayReleaseRules.get(rulesScanned - 1).getId();
//batch is 500
hasMore = rulesScanned == 500;
}
}
private void mergeGrayReleaseRules(List<GrayReleaseRule> grayReleaseRules) {
if (CollectionUtils.isEmpty(grayReleaseRules)) {
return;
}
for (GrayReleaseRule grayReleaseRule : grayReleaseRules) {
if (grayReleaseRule.getReleaseId() == null || grayReleaseRule.getReleaseId() == 0) {
//filter rules with no release id, i.e. never released
continue;
}
String key = assembleGrayReleaseRuleKey(grayReleaseRule.getAppId(), grayReleaseRule
.getClusterName(), grayReleaseRule.getNamespaceName());
//create a new list to avoid ConcurrentModificationException
List<GrayReleaseRuleCache> rules = Lists.newArrayList(grayReleaseRuleCache.get(key));
GrayReleaseRuleCache oldRule = null;
for (GrayReleaseRuleCache ruleCache : rules) {
if (ruleCache.getBranchName().equals(grayReleaseRule.getBranchName())) {
oldRule = ruleCache;
break;
}
}
//if old rule is null and new rule's branch status is not active, ignore
if (oldRule == null && grayReleaseRule.getBranchStatus() != NamespaceBranchStatus.ACTIVE) {
continue;
}
//use id comparison to avoid synchronization
if (oldRule == null || grayReleaseRule.getId() > oldRule.getRuleId()) {
addCache(key, transformRuleToRuleCache(grayReleaseRule));
if (oldRule != null) {
removeCache(key, oldRule);
}
} else {
if (oldRule.getBranchStatus() == NamespaceBranchStatus.ACTIVE) {
//update load version
oldRule.setLoadVersion(loadVersion.get());
} else if ((loadVersion.get() - oldRule.getLoadVersion()) > 1) {
//remove outdated inactive branch rule after 2 update cycles
removeCache(key, oldRule);
}
}
}
}
private void addCache(String key, GrayReleaseRuleCache ruleCache) {
if (ruleCache.getBranchStatus() == NamespaceBranchStatus.ACTIVE) {
for (GrayReleaseRuleItemDTO ruleItemDTO : ruleCache.getRuleItems()) {
for (String clientIp : ruleItemDTO.getClientIpList()) {
reversedGrayReleaseRuleCache.put(assembleReversedGrayReleaseRuleKey(ruleItemDTO
.getClientAppId(), ruleCache.getNamespaceName(), clientIp), ruleCache.getRuleId());
}
}
}
grayReleaseRuleCache.put(key, ruleCache);
}
private void removeCache(String key, GrayReleaseRuleCache ruleCache) {
grayReleaseRuleCache.remove(key, ruleCache);
for (GrayReleaseRuleItemDTO ruleItemDTO : ruleCache.getRuleItems()) {
for (String clientIp : ruleItemDTO.getClientIpList()) {
reversedGrayReleaseRuleCache.remove(assembleReversedGrayReleaseRuleKey(ruleItemDTO
.getClientAppId(), ruleCache.getNamespaceName(), clientIp), ruleCache.getRuleId());
}
}
}
private GrayReleaseRuleCache transformRuleToRuleCache(GrayReleaseRule grayReleaseRule) {
Set<GrayReleaseRuleItemDTO> ruleItems;
try {
ruleItems = GrayReleaseRuleItemTransformer.batchTransformFromJSON(grayReleaseRule.getRules());
} catch (Throwable ex) {
ruleItems = Sets.newHashSet();
Tracer.logError(ex);
logger.error("parse rule for gray release rule {} failed", grayReleaseRule.getId(), ex);
}
GrayReleaseRuleCache ruleCache = new GrayReleaseRuleCache(grayReleaseRule.getId(),
grayReleaseRule.getBranchName(), grayReleaseRule.getNamespaceName(), grayReleaseRule
.getReleaseId(), grayReleaseRule.getBranchStatus(), loadVersion.get(), ruleItems);
return ruleCache;
}
private void populateDataBaseInterval() {
databaseScanInterval = bizConfig.grayReleaseRuleScanInterval();
}
private int getDatabaseScanIntervalSecond() {
return databaseScanInterval;
}
private TimeUnit getDatabaseScanTimeUnit() {
return TimeUnit.SECONDS;
}
private String assembleGrayReleaseRuleKey(String configAppId, String configCluster, String
configNamespaceName) {
return STRING_JOINER.join(configAppId, configCluster, configNamespaceName);
}
private String assembleReversedGrayReleaseRuleKey(String clientAppId, String
clientNamespaceName, String clientIp) {
return STRING_JOINER.join(clientAppId, clientNamespaceName, clientIp);
}
}