package com.tesora.dve.sql.schema.cache; /* * #%L * Tesora Inc. * Database Virtualization Engine * %% * Copyright (C) 2011 - 2014 Tesora Inc. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import java.lang.reflect.Array; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheStats; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.tesora.dve.common.PEConstants; import com.tesora.dve.exceptions.PEException; import com.tesora.dve.exceptions.PENotFoundException; import com.tesora.dve.sql.SchemaException; import com.tesora.dve.sql.ParserException.Pass; import com.tesora.dve.sql.schema.Persistable; import com.tesora.dve.sql.schema.SchemaContext; import com.tesora.dve.sql.util.ListSet; import com.tesora.dve.sql.util.Pair; import com.tesora.dve.sql.util.UnaryProcedure; // this is the immutable global cache public class SchemaCache implements SchemaSource, RemovalListener<SchemaCacheKey<?>, Object> { private GlobalPlanCache planCache; private final Cache<SchemaCacheKey<?>,Object>[] tloaded; private final CacheStats[] tloaded_baseline; private final PreparedStatementPlanCache pstmtCache; private final long version; @SuppressWarnings("unchecked") public SchemaCache(long versionAtCreation, CacheLimits limits, PreparedStatementPlanCache thePstmtCache) { CacheSegment[] values = CacheSegment.values(); tloaded = (Cache<SchemaCacheKey<?>, Object>[]) Array.newInstance(Cache/*<SchemaCacheKey<?>,Object>*/.class, values.length); tloaded_baseline = new CacheStats[values.length]; for(CacheSegment cs : values) { if (cs == CacheSegment.PLAN) continue; tloaded[cs.ordinal()] = buildCache(limits.getLimit(cs)); tloaded_baseline[cs.ordinal()] = null; } planCache = new GlobalPlanCache(limits); pstmtCache = thePstmtCache; version = versionAtCreation; } private Cache<SchemaCacheKey<?>, Object> buildCache(int size) { CacheBuilder<SchemaCacheKey<?>, Object> boo = CacheBuilder.newBuilder().removalListener(this).recordStats(); if (size > -1) boo = boo.maximumSize(size); return boo.build(); } private static Cache<PlanCacheKey, RegularCachedPlan> buildPlanCache(int size) { return CacheBuilder.newBuilder().maximumSize(size).recordStats().build(); } private static class ContextLoader<T> implements Callable<T> { private final SchemaContext cntxt; private final SchemaCacheKey<T> key; public ContextLoader(SchemaContext csc, SchemaCacheKey<T> k) { cntxt = csc; key = k; } @Override public T call() throws Exception { cntxt.addCacheLoading(key); T candidate = key.load(cntxt); cntxt.removeCacheLoading(key); if (candidate == null) throw new PENotFoundException("unable to find " + key); return candidate; } } private static class ConstantLoader<T> implements Callable<T> { private final T target; public ConstantLoader(T t) { target = t; } @Override public T call() throws Exception { return target; } } public long getCacheSize(CacheSegment cs) { if (cs == CacheSegment.PLAN) return planCache.getSize(); return getSubCache(cs).size(); } public CacheStats getCacheStats(CacheSegment cs) { if (cs == CacheSegment.PLAN) return planCache.getStats(); return getSubCacheStats(cs); } public void resetCacheStats(CacheSegment cs) { if (cs == CacheSegment.PLAN) planCache.resetStats(); else tloaded_baseline[cs.ordinal()] = getSubCache(cs).stats(); } private Cache<SchemaCacheKey<?>, Object> getSubCache(CacheSegment cs) { return tloaded[cs.ordinal()]; } private CacheStats getSubCacheStats(CacheSegment cs) { if(tloaded_baseline[cs.ordinal()] != null) return tloaded[cs.ordinal()].stats().minus(tloaded_baseline[cs.ordinal()]); return tloaded[cs.ordinal()].stats(); } @SuppressWarnings({ "unchecked", "rawtypes" }) private void loadingGet(SchemaCacheKey<?> key, Object value) { try { getSubCache(key.getCacheSegment()).get(key, new ConstantLoader(value)); } catch (ExecutionException ee) { } } @SuppressWarnings("unchecked") @Override public <T> T find(SchemaContext cntxt, SchemaCacheKey<T> ck) { if (ck == null) return null; try { return (T)getSubCache(ck.getCacheSegment()).get(ck, new ContextLoader<T>(cntxt,ck)); } catch (ExecutionException ee) { if (ee.getCause() instanceof PENotFoundException) return null; throw new SchemaException(Pass.SECOND,ee.getCause()); } } @Override public void setLoaded(final Persistable<?, ?> p, SchemaCacheKey<?> sck) { if (sck != null) { if (p == null) { getSubCache(sck.getCacheSegment()).invalidate(sck); } else { loadingGet(sck,p); } } } @Override public Persistable<?,?> getLoaded(SchemaCacheKey<?> sck) { if (sck == null) return null; return (Persistable<?, ?>) getSubCache(sck.getCacheSegment()).getIfPresent(sck); } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public <T> SchemaEdge<T> buildEdge(T p) { Cacheable c = (Cacheable)p; SchemaEdge<T> e = (p == null ? new LoadedEdge() : new LoadedEdge(c.getCacheKey())); return e; } @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public <T> SchemaEdge<T> buildTransientEdge(T p) { return new TransientEdge(p); } @Override public <T> SchemaEdge<T> buildEdgeFromKey(SchemaCacheKey<T> sck) { return new LoadedEdge<T>(sck); } @Override public CacheType getType() { return CacheType.GLOBAL; } @Override public long getVersion() { return version; } // used for debugging only public String describePlanCache() { StringBuffer buf = new StringBuffer(); for(Map.Entry<PlanCacheKey, RegularCachedPlan> me : planCache.getROMap().entrySet()) { buf.append(me.getKey().toString()).append("==>").append(me.getValue().getClass().getSimpleName()).append(PEConstants.LINE_SEPARATOR); } return buf.toString(); } // dump plan cache public void applyOnPlans(UnaryProcedure<RegularCachedPlan> f) { for(RegularCachedPlan rcp : planCache.getROMap().values()) f.execute(rcp); } @Override public boolean isPlanCacheEmpty() { return planCache.isEmpty(); } @Override public boolean canCachePlans(SchemaContext sc) { return SchemaSourceFactory.getCacheLimits(sc).getLimit(CacheSegment.PLAN) > 0; } @Override public void putCachedPlan(RegularCachedPlan cp) { planCache.put(cp); } @Override public void clearCachedPlan(RegularCachedPlan cp) { planCache.invalidate(cp.getKey()); } @Override public RegularCachedPlan getCachedPlan(SchemaContext sc, PlanCacheKey pck) { return planCache.get(sc, pck); } @Override public void onCacheLimitUpdate(CacheSegment cs, CacheLimits limits) { if (cs == CacheSegment.PREPARED) return; if (cs == CacheSegment.PLAN || cs == CacheSegment.RAWPLAN) { planCache = new GlobalPlanCache(limits); if (cs == CacheSegment.RAWPLAN) { tloaded[cs.ordinal()] = buildCache(limits.getLimit(cs)); tloaded_baseline[cs.ordinal()] = null; } } else { tloaded[cs.ordinal()] = buildCache(limits.getLimit(cs)); tloaded_baseline[cs.ordinal()] = null; if (cs == CacheSegment.TABLE || cs == CacheSegment.TENANT) planCache = new GlobalPlanCache(limits); } } @Override public void onRemoval( RemovalNotification<SchemaCacheKey<?>, Object> paramRemovalNotification) { SchemaCacheKey<?> sck = paramRemovalNotification.getKey(); if (sck.getCacheSegment() != CacheSegment.UNCATEGORIZED) { HashSet<PlanCacheKey> current = new HashSet<PlanCacheKey>(planCache.getROMap().keySet()); for(PlanCacheKey pck : current) { CachedPlan cp = planCache.get(null,pck); if (cp == null) continue; if (cp.invalidate(sck)) planCache.invalidate(pck); } pstmtCache.invalidate(sck); } } // return true to invalidate the entire cache public boolean onModification(CacheInvalidationRecord cir) { if (cir.getGlobalToken() != null) return true; // if the key is null, it's something we don't bother caching if (cir.getInvalidateActions().isEmpty()) return false; // build the set of all stuff to invalidate HashSet<SchemaCacheKey<?>> visited = new HashSet<SchemaCacheKey<?>>(); LinkedList<SchemaCacheKey<?>> toVisit = new LinkedList<SchemaCacheKey<?>>(); ListSet<SchemaCacheKey<?>> toInvalidate = new ListSet<SchemaCacheKey<?>>(); boolean haveRawPlanEvent = false; for(Pair<SchemaCacheKey<?>, InvalidationScope> p : cir.getInvalidateActions()) { if (p.getSecond() == InvalidationScope.GLOBAL) return true; if (p.getFirst().getCacheSegment() == CacheSegment.RAWPLAN) haveRawPlanEvent = true; else if (p.getSecond() == InvalidationScope.CASCADE) { toVisit.add(p.getFirst()); } else toInvalidate.add(p.getFirst()); } while(!toVisit.isEmpty()) { SchemaCacheKey<?> first = toVisit.removeFirst(); if (first == null || !visited.add(first)) continue; toInvalidate.add(first); Cache<SchemaCacheKey<?>, Object> sub = getSubCache(first.getCacheSegment()); Object present = sub.getIfPresent(first); if (present != null) toVisit.addAll(first.getCascades(present)); } // if we're removing a scope or a table - both are now contained in the cache aware lookups - // so just remove the entry for the associated object - and let the usual invalidation code handle // the plan cache. for(SchemaCacheKey<?> sck : toInvalidate) { if (sck.getCacheSegment() != CacheSegment.UNCATEGORIZED) { // make sure we invalidate both the forward and reverse lookups invalidate(sck); } else { return true; } } if (haveRawPlanEvent) planCache.onRawPlanEvent(); return false; } @Override public void invalidate(SchemaCacheKey<?> sck) { Cache<SchemaCacheKey<?>, Object> c = getSubCache(sck.getCacheSegment()); c.invalidate(sck); } @Override public CachedPreparedStatement getPreparedStatement(PlanCacheKey pck) { return pstmtCache.getPreparedStatement(pck); } @Override public CachedPreparedStatement getPreparedStatement(SchemaContext sc, int connID, String stmtID) throws PEException { return pstmtCache.getPreparedStatement(sc, connID, stmtID); } @Override public void putPreparedStatement(CachedPreparedStatement cps, int connID, String stmtID, String rawSQL, boolean reregister) throws PEException { pstmtCache.putPreparedStatement(cps, connID, stmtID, rawSQL, reregister); } @Override public void clearPreparedStatement(int connID, String stmtID) { pstmtCache.clearPreparedStatement(connID, stmtID); } private static class GlobalPlanCache extends PlanCache { private final Cache<PlanCacheKey, RegularCachedPlan> planCache; private CacheStats baselineStats = null; public GlobalPlanCache(CacheLimits limits) { super(limits.getLimit(CacheSegment.RAWPLAN)); planCache = buildPlanCache(limits.getLimit(CacheSegment.PLAN)); } @Override public boolean isMainEmpty() { return planCache.size() == 0; } @Override public void invalidateMain(PlanCacheKey pck) { planCache.invalidate(pck); } @Override public RegularCachedPlan getMain(PlanCacheKey pck) { return planCache.getIfPresent(pck); } @Override public void putMain(RegularCachedPlan rcp) { planCache.put(rcp.getKey(),rcp); } @Override public long getMainSize() { return planCache.size(); } @Override public CacheStats getMainStats() { if(baselineStats != null) return planCache.stats().minus(baselineStats); return planCache.stats(); } @Override public void resetMainStats() { baselineStats = planCache.stats(); } @Override public Map<PlanCacheKey, RegularCachedPlan> getMainMap() { return planCache.asMap(); } } }