package io.cattle.platform.configitem.context.impl; import static io.cattle.platform.core.model.tables.AccountLinkTable.*; import static io.cattle.platform.core.model.tables.AccountTable.*; import static io.cattle.platform.core.model.tables.InstanceHostMapTable.*; import io.cattle.platform.archaius.util.ArchaiusUtil; import io.cattle.platform.configitem.context.dao.MetaDataInfoDao; import io.cattle.platform.configitem.context.data.metadata.common.HostMetaData; import io.cattle.platform.configitem.context.data.metadata.common.MetaHelperInfo; import io.cattle.platform.configitem.model.DefaultItemVersion; import io.cattle.platform.configitem.model.ItemVersion; import io.cattle.platform.configitem.server.model.ConfigItem; import io.cattle.platform.configitem.server.model.Request; import io.cattle.platform.configitem.server.model.impl.ArchiveContext; import io.cattle.platform.core.model.Account; import io.cattle.platform.core.model.AccountLink; import io.cattle.platform.core.model.Agent; import io.cattle.platform.core.model.Instance; import io.cattle.platform.core.model.InstanceHostMap; import io.cattle.platform.lock.definition.LockDefinition; import io.cattle.platform.lock.exception.FailedToAcquireLockException; import io.cattle.platform.servicediscovery.api.dao.ServiceConsumeMapDao; import io.cattle.platform.util.exception.ExceptionUtils; import io.github.ibuildthecloud.gdapi.condition.Condition; import io.github.ibuildthecloud.gdapi.condition.ConditionType; import io.github.ibuildthecloud.gdapi.util.RequestUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import javax.inject.Inject; import javax.inject.Named; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.netflix.config.DynamicBooleanProperty; @Named public class ServiceMetadataInfoFactory extends AbstractAgentBaseContextFactory { private static DynamicBooleanProperty CACHE = ArchaiusUtil.getBoolean("cache.metadata"); private static DynamicBooleanProperty CACHE_LOCK = ArchaiusUtil.getBoolean("cache.metadata.lock"); private static final Logger log = LoggerFactory.getLogger(ServiceMetadataInfoFactory.class); @Inject ServiceConsumeMapDao consumeMapDao; @Inject MetaDataInfoDao metaDataInfoDao; Cache<String, CacheData> cache = CacheBuilder.newBuilder() .expireAfterAccess(30, TimeUnit.SECONDS) .build(); Cache<Long, String> latestVersion = CacheBuilder.newBuilder() .expireAfterAccess(29, TimeUnit.SECONDS) .build(); LoadingCache<Long, ReentrantLock> lockCache = CacheBuilder.newBuilder() .expireAfterWrite(2, TimeUnit.MINUTES) .build(new CacheLoader<Long, ReentrantLock>() { @Override public ReentrantLock load(Long key) throws Exception { return new ReentrantLock(); } }); @Override protected void populateContext(Agent agent, Instance instance, ConfigItem item, ArchiveContext context) { // this method is never being called } public void writeMetadata(final Instance instance, final Callable<String> version, final Request req) throws IOException { if (instance == null) { return; } final InstanceHostMap hostMap = objectManager.findAny(InstanceHostMap.class, INSTANCE_HOST_MAP.INSTANCE_ID, instance.getId()); if (hostMap == null) { return; } try { OutputStream os = req.getOutputStream(); if (CACHE.get()) { req.setContentType("application/octet-stream"); writeCachedGenericData(hostMap.getHostId(), instance, version, getRequestedVersion(req), os); } else { String itemVersion = version.call(); Map<Long, HostMetaData> hostIdToHostMetadata = writeGenericData(instance, os); metaDataInfoDao.fetchSelf(hostIdToHostMetadata.get(hostMap.getHostId()), itemVersion, os); } } catch (ExecutionException e) { ExceptionUtils.rethrowExpectedRuntime(e.getCause()); } catch (IOException e) { throw e; } catch (Exception e) { ExceptionUtils.rethrowExpectedRuntime(e); } finally { } } protected String getRequestedVersion(Request req) { String v = RequestUtils.makeSingularStringIfCan(req.getParams().get("requestedVersion")); return v == null ? "" : v; } protected ReentrantLock doLock(final Instance instance) { if (!CACHE_LOCK.get()) { return null; } ReentrantLock lock = lockCache.getUnchecked(instance.getAccountId()); try { if (lock.tryLock(1, TimeUnit.MINUTES)) { return lock; } throw new FailedToAcquireLockException(new LockDefinition() { @Override public String getLockId() { return "HOST.META." + instance.getAccountId(); } }); } catch (InterruptedException e) { throw new IllegalStateException(e); } } private void writeCachedGenericData(Long hostId, final Instance instance, final Callable<String> versionCallback, String clientRequestedVersion, OutputStream os) throws Exception { final String itemVersion; // Get version before lock String startItemVersion = versionCallback.call(); ReentrantLock lock = doLock(instance); CacheData data = null; try { itemVersion = determineItemVersion(instance.getAccountId(), startItemVersion, clientRequestedVersion, versionCallback); data = cache.get(instance.getAccountId() + "/" + itemVersion, new Callable<CacheData>() { @Override public CacheData call() throws Exception { long start = System.currentTimeMillis(); CacheData data = new CacheData(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); DeflaterOutputStream gz = new DeflaterOutputStream(baos, new Deflater(Deflater.DEFAULT_COMPRESSION, true), true); data.hostIdToHostMetadata = writeGenericData(instance, gz); gz.flush(); data.bytes = baos.toByteArray(); log.debug("Generated [{}] in {}ms", itemVersion, System.currentTimeMillis()-start); return data; } }); latestVersion.put(instance.getAccountId(), itemVersion); } finally { if (lock != null) { lock.unlock(); } } IOUtils.write(data.bytes, os); try (DeflaterOutputStream gz = new DeflaterOutputStream(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true), true)) { metaDataInfoDao.fetchSelf(data.hostIdToHostMetadata.get(hostId), itemVersion, gz); } } protected String determineItemVersion(Long accountId, String requestedItemVersion, String clientRequestedVersion, Callable<String> versionCallback) throws Exception { String cachedItemVersion = latestVersion.getIfPresent(accountId); if (cachedItemVersion == null || requestedItemVersion == null) { String result = versionCallback.call(); log.debug("Latest version doesn't exist for [{}] using [{}]", accountId, result); return result; } ItemVersion cached = DefaultItemVersion.fromString(cachedItemVersion); DefaultItemVersion requested = DefaultItemVersion.fromString(requestedItemVersion); try { if (clientRequestedVersion != null) { requested.setRevision(Long.parseLong(clientRequestedVersion)); } } catch (NumberFormatException nfe) { } if (!cached.getSourceRevision().equals(requested.getSourceRevision())) { String result = versionCallback.call(); log.debug("Source versions don't match for [{}] using [{}]", accountId, result); return versionCallback.call(); } if (cached.getRevision() >= requested.getRevision()) { log.debug("Using cached version [{}] instead of requested [{}]", cachedItemVersion, requestedItemVersion); return cachedItemVersion; } String result = versionCallback.call(); log.debug("Using latest from DB for version [{}] instead of requested [{}]", result, requestedItemVersion); return result; } private Map<Long, HostMetaData> writeGenericData(Instance instance, OutputStream os) { MetaHelperInfo helperInfo = fetchHelperData(objectManager.loadResource(Account.class, instance.getAccountId())); // Metadata visible to the user metaDataInfoDao.fetchContainers(helperInfo, os); metaDataInfoDao.fetchServices(helperInfo, os); metaDataInfoDao.fetchStacks(helperInfo, os); metaDataInfoDao.fetchHosts(helperInfo, os); metaDataInfoDao.fetchNetworks(helperInfo, os); // Helper metadata metaDataInfoDao.fetchServiceContainerLinks(helperInfo, os); metaDataInfoDao.fetchServiceLinks(helperInfo, os); metaDataInfoDao.fetchContainerLinks(helperInfo, os); return helperInfo.getHostIdToHostMetadata(); } private MetaHelperInfo fetchHelperData(Account account) { Map<Long, Account> accounts = new HashMap<>(); Set<Long> linkedServicesIds = new HashSet<>(); Set<Long> linkedStackIds = new HashSet<>(); List<? extends Account> allAccounts = objectManager.find(Account.class, ACCOUNT.REMOVED, new Condition( ConditionType.NULL)); Map<Long, Account> allAccountsMap = new HashMap<>(); for (Account a : allAccounts) { allAccountsMap.put(a.getId(), a); } // fetch accounts/services that are linked TO your account accounts.put(account.getId(), account); List<? extends AccountLink> accountLinks = objectManager.find(AccountLink.class, ACCOUNT_LINK.ACCOUNT_ID, account.getId(), ACCOUNT_LINK.REMOVED, null); for (AccountLink accountLink : accountLinks) { Long accountId = accountLink.getLinkedAccountId(); accounts.put(accountId, allAccountsMap.get(accountId)); } // fetch accounts/services that your account is linked TO accountLinks = objectManager.find(AccountLink.class, ACCOUNT_LINK.LINKED_ACCOUNT_ID, account.getId(), ACCOUNT_LINK.REMOVED, null); for (AccountLink accountLink : accountLinks) { Long accountId = accountLink.getAccountId(); accounts.put(accountId, allAccountsMap.get(accountId)); } // fetch services linked both ways Map<Long, Long> map1 = consumeMapDao.findConsumedServicesIdsToStackIdsFromOtherAccounts(account.getId()); Map<Long, Long> map2 = consumeMapDao.findConsumedByServicesIdsToStackIdsFromOtherAccounts(account.getId()); linkedServicesIds.addAll(map1.keySet()); linkedStackIds.addAll(map1.values()); linkedServicesIds.addAll(map2.keySet()); linkedStackIds.addAll(map2.values()); return new MetaHelperInfo(account, accounts, linkedServicesIds, linkedStackIds, metaDataInfoDao); } private static class CacheData { Map<Long, HostMetaData> hostIdToHostMetadata; byte[] bytes; } }