package com.ctrip.framework.apollo.configservice.util; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; import com.google.common.collect.Queues; import com.ctrip.framework.apollo.biz.entity.Instance; import com.ctrip.framework.apollo.biz.entity.InstanceConfig; import com.ctrip.framework.apollo.biz.service.InstanceService; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; import com.ctrip.framework.apollo.tracer.Tracer; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * @author Jason Song(song_s@ctrip.com) */ @Service public class InstanceConfigAuditUtil implements InitializingBean { private static final int INSTANCE_CONFIG_AUDIT_MAX_SIZE = 2000; private static final int INSTANCE_CACHE_MAX_SIZE = 10000; private static final int INSTANCE_CONFIG_CACHE_MAX_SIZE = 10000; private static final long OFFER_TIME_LAST_MODIFIED_TIME_THRESHOLD_IN_MILLI = TimeUnit.MINUTES.toMillis(10);//10 minutes private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR); private final ExecutorService auditExecutorService; private final AtomicBoolean auditStopped; private BlockingQueue<InstanceConfigAuditModel> audits = Queues.newLinkedBlockingQueue (INSTANCE_CONFIG_AUDIT_MAX_SIZE); private Cache<String, Long> instanceCache; private Cache<String, String> instanceConfigReleaseKeyCache; @Autowired private InstanceService instanceService; public InstanceConfigAuditUtil() { auditExecutorService = Executors.newSingleThreadExecutor( ApolloThreadFactory.create("InstanceConfigAuditUtil", true)); auditStopped = new AtomicBoolean(false); instanceCache = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.HOURS) .maximumSize(INSTANCE_CACHE_MAX_SIZE).build(); instanceConfigReleaseKeyCache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS) .maximumSize(INSTANCE_CONFIG_CACHE_MAX_SIZE).build(); } public boolean audit(String appId, String clusterName, String dataCenter, String ip, String configAppId, String configClusterName, String configNamespace, String releaseKey) { return this.audits.offer(new InstanceConfigAuditModel(appId, clusterName, dataCenter, ip, configAppId, configClusterName, configNamespace, releaseKey)); } void doAudit(InstanceConfigAuditModel auditModel) { String instanceCacheKey = assembleInstanceKey(auditModel.getAppId(), auditModel .getClusterName(), auditModel.getIp(), auditModel.getDataCenter()); Long instanceId = instanceCache.getIfPresent(instanceCacheKey); if (instanceId == null) { instanceId = prepareInstanceId(auditModel); instanceCache.put(instanceCacheKey, instanceId); } //load instance config release key from cache, and check if release key is the same String instanceConfigCacheKey = assembleInstanceConfigKey(instanceId, auditModel .getConfigAppId(), auditModel.getConfigNamespace()); String cacheReleaseKey = instanceConfigReleaseKeyCache.getIfPresent(instanceConfigCacheKey); //if release key is the same, then skip audit if (cacheReleaseKey != null && Objects.equals(cacheReleaseKey, auditModel.getReleaseKey())) { return; } instanceConfigReleaseKeyCache.put(instanceConfigCacheKey, auditModel.getReleaseKey()); //if release key is not the same or cannot find in cache, then do audit InstanceConfig instanceConfig = instanceService.findInstanceConfig(instanceId, auditModel .getConfigAppId(), auditModel.getConfigNamespace()); if (instanceConfig != null) { if (!Objects.equals(instanceConfig.getReleaseKey(), auditModel.getReleaseKey())) { instanceConfig.setConfigClusterName(auditModel.getConfigClusterName()); instanceConfig.setReleaseKey(auditModel.getReleaseKey()); instanceConfig.setReleaseDeliveryTime(auditModel.getOfferTime()); } else if (offerTimeAndLastModifiedTimeCloseEnough(auditModel.getOfferTime(), instanceConfig.getDataChangeLastModifiedTime())) { //when releaseKey is the same, optimize to reduce writes if the record was updated not long ago return; } //we need to update no matter the release key is the same or not, to ensure the //last modified time is updated each day instanceConfig.setDataChangeLastModifiedTime(auditModel.getOfferTime()); instanceService.updateInstanceConfig(instanceConfig); return; } instanceConfig = new InstanceConfig(); instanceConfig.setInstanceId(instanceId); instanceConfig.setConfigAppId(auditModel.getConfigAppId()); instanceConfig.setConfigClusterName(auditModel.getConfigClusterName()); instanceConfig.setConfigNamespaceName(auditModel.getConfigNamespace()); instanceConfig.setReleaseKey(auditModel.getReleaseKey()); instanceConfig.setReleaseDeliveryTime(auditModel.getOfferTime()); instanceConfig.setDataChangeCreatedTime(auditModel.getOfferTime()); try { instanceService.createInstanceConfig(instanceConfig); } catch (DataIntegrityViolationException ex) { //concurrent insertion, safe to ignore } } private boolean offerTimeAndLastModifiedTimeCloseEnough(Date offerTime, Date lastModifiedTime) { return (offerTime.getTime() - lastModifiedTime.getTime()) < OFFER_TIME_LAST_MODIFIED_TIME_THRESHOLD_IN_MILLI; } private long prepareInstanceId(InstanceConfigAuditModel auditModel) { Instance instance = instanceService.findInstance(auditModel.getAppId(), auditModel .getClusterName(), auditModel.getDataCenter(), auditModel.getIp()); if (instance != null) { return instance.getId(); } instance = new Instance(); instance.setAppId(auditModel.getAppId()); instance.setClusterName(auditModel.getClusterName()); instance.setDataCenter(auditModel.getDataCenter()); instance.setIp(auditModel.getIp()); try { return instanceService.createInstance(instance).getId(); } catch (DataIntegrityViolationException ex) { //return the one exists return instanceService.findInstance(instance.getAppId(), instance.getClusterName(), instance.getDataCenter(), instance.getIp()).getId(); } } @Override public void afterPropertiesSet() throws Exception { auditExecutorService.submit(() -> { while (!auditStopped.get() && !Thread.currentThread().isInterrupted()) { try { InstanceConfigAuditModel model = audits.poll(); if (model == null) { TimeUnit.SECONDS.sleep(1); continue; } doAudit(model); } catch (Throwable ex) { Tracer.logError(ex); } } }); } private String assembleInstanceKey(String appId, String cluster, String ip, String datacenter) { List<String> keyParts = Lists.newArrayList(appId, cluster, ip); if (!Strings.isNullOrEmpty(datacenter)) { keyParts.add(datacenter); } return STRING_JOINER.join(keyParts); } private String assembleInstanceConfigKey(long instanceId, String configAppId, String configNamespace) { return STRING_JOINER.join(instanceId, configAppId, configNamespace); } public static class InstanceConfigAuditModel { private String appId; private String clusterName; private String dataCenter; private String ip; private String configAppId; private String configClusterName; private String configNamespace; private String releaseKey; private Date offerTime; public InstanceConfigAuditModel(String appId, String clusterName, String dataCenter, String clientIp, String configAppId, String configClusterName, String configNamespace, String releaseKey) { this.offerTime = new Date(); this.appId = appId; this.clusterName = clusterName; this.dataCenter = Strings.isNullOrEmpty(dataCenter) ? "" : dataCenter; this.ip = clientIp; this.configAppId = configAppId; this.configClusterName = configClusterName; this.configNamespace = configNamespace; this.releaseKey = releaseKey; } public String getAppId() { return appId; } public String getClusterName() { return clusterName; } public String getDataCenter() { return dataCenter; } public String getIp() { return ip; } public String getConfigAppId() { return configAppId; } public String getConfigNamespace() { return configNamespace; } public String getReleaseKey() { return releaseKey; } public String getConfigClusterName() { return configClusterName; } public Date getOfferTime() { return offerTime; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; InstanceConfigAuditModel model = (InstanceConfigAuditModel) o; return Objects.equals(appId, model.appId) && Objects.equals(clusterName, model.clusterName) && Objects.equals(dataCenter, model.dataCenter) && Objects.equals(ip, model.ip) && Objects.equals(configAppId, model.configAppId) && Objects.equals(configClusterName, model.configClusterName) && Objects.equals(configNamespace, model.configNamespace) && Objects.equals(releaseKey, model.releaseKey); } @Override public int hashCode() { return Objects.hash(appId, clusterName, dataCenter, ip, configAppId, configClusterName, configNamespace, releaseKey); } } }