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.util.concurrent.ConcurrentHashMap; import com.tesora.dve.exceptions.PEException; import com.tesora.dve.sql.parser.InvokeParser; import com.tesora.dve.sql.parser.PreparePlanningResult; import com.tesora.dve.sql.schema.SchemaContext; // prepared stmts are logically cached per connection, but for dml caching we cache per jvm and maintain // a mapping from the connection specific id to a global id. public class PreparedStatementPlanCache implements PreparedPlanCache { private final PStmtCache cache; private ConcurrentHashMap<String, PStmtMappingEntry> clientMapping; // once the client mapping becomes this big, we have to stop private int maxSize; public PreparedStatementPlanCache(CacheLimits cl) { maxSize = cl.getLimit(CacheSegment.PREPARED); cache = new PStmtCache(); clientMapping = new ConcurrentHashMap<String, PStmtMappingEntry>(); } @Override public void onCacheLimitUpdate(CacheSegment cs, CacheLimits limits) { if (cs != CacheSegment.PREPARED) return; int newSize = limits.getLimit(CacheSegment.PREPARED); if (newSize == 0) { // this means disable, so we can clear everything now maxSize = 0; clientMapping.clear(); cache.clear(); } else { // if newSize is smaller - everything in flight will continue but no new stmts can be allocated; // if larger - then we just grow upwards maxSize = newSize; } } @Override public CachedPreparedStatement getPreparedStatement(PlanCacheKey pck) { return cache.lookup(pck); } @Override public CachedPreparedStatement getPreparedStatement(SchemaContext sc, int connID, String stmtID) throws PEException { String ckey = buildConnectionStmtKey(connID,stmtID); PStmtMappingEntry psme = clientMapping.get(ckey); if (psme == null) throw new PEException("Internal error - no prepared statement found for " + ckey); if (psme.get() == null) { // entry invalidated, replan PreparePlanningResult ppr = InvokeParser.reprepareStatement(sc, psme.getPrepareSQL(), stmtID); psme.setCachedPreparedStatement(ppr.getCachedPlan()); } return psme.get(); } @Override public void putPreparedStatement(CachedPreparedStatement cps, int connID, String stmtID, String rawSQL, boolean reregister) throws PEException { // if not reregistering, do the size check first if (!reregister && (clientMapping.size() + 1) >= maxSize) throw new PEException("Max number of prepared stmts exceeded."); String key = buildConnectionStmtKey(connID,stmtID); PStmtMappingEntry psme = cache.put(cps, rawSQL); clientMapping.put(key,psme); if (psme.get() == null) psme.setCachedPreparedStatement(cps); } @Override public void clearPreparedStatement(int connID, String stmtID) { String connKey = buildConnectionStmtKey(connID, stmtID); PStmtMappingEntry psme = clientMapping.remove(connKey); if (psme == null) return; cache.remove(psme.getKey()); } @Override public void invalidate(SchemaCacheKey<?> sck) { cache.invalidate(sck); } private static class PStmtMappingEntry { private PlanCacheKey pck; private CachedPreparedStatement stmt; private final String originalSQL; public PStmtMappingEntry(CachedPreparedStatement stmt, String origSQL) { this.pck = stmt.getKey(); this.stmt = stmt; this.originalSQL = origSQL; } public PlanCacheKey getKey() { return pck; } public void invalidate(SchemaCacheKey<?> sck) { if (stmt == null) return; if (stmt.invalidate(sck)) stmt = null; } public CachedPreparedStatement get() { return stmt; } public String getPrepareSQL() { return originalSQL; } // for reprepare void setCachedPreparedStatement(CachedPreparedStatement stmt) { this.stmt = stmt; } } private static String buildConnectionStmtKey(int connid, String stmtID) { StringBuilder buf = new StringBuilder(); buf.append(connid).append("/").append(stmtID); return buf.toString(); } private static class MappingEntry { private int refcount; private PStmtMappingEntry entry; public MappingEntry(PStmtMappingEntry psme) { entry = psme; refcount = 0; } public PStmtMappingEntry getEntry(boolean counting) { if (counting) synchronized(this) { refcount++; } return entry; } public synchronized void decrement() { refcount--; } public synchronized boolean isUnreferenced() { return refcount <= 0; } } private static class PStmtCache { private ConcurrentHashMap<PlanCacheKey,MappingEntry> cache; public PStmtCache() { clear(); } public void clear() { cache = new ConcurrentHashMap<PlanCacheKey,MappingEntry>(); } public CachedPreparedStatement lookup(PlanCacheKey pck) { MappingEntry me = cache.get(pck); if (me == null) return null; return me.getEntry(false).get(); } public void invalidate(SchemaCacheKey<?> sck) { for(MappingEntry psme : cache.values()) psme.getEntry(false).invalidate(sck); } public PStmtMappingEntry put(CachedPreparedStatement cps, String rawSQL) { MappingEntry me = cache.get(cps.getKey()); if (me == null) { MappingEntry nme = new MappingEntry(new PStmtMappingEntry(cps,rawSQL)); synchronized(cache) { me = cache.get(cps.getKey()); if (me == null) { cache.put(cps.getKey(),nme); me = nme; } } } return me.getEntry(true); } public void remove(PlanCacheKey pck) { MappingEntry me = cache.get(pck); if (me == null) return; me.decrement(); if (me.isUnreferenced()) { synchronized(cache) { if (me.isUnreferenced()) { cache.remove(pck); } } } } } }