/* This file is part of VoltDB.
* Copyright (C) 2008-2014 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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 VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.planner;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.voltdb.jni.ExecutionEngine;
import org.voltdb.jni.Sha1Wrapper;
public abstract class ActivePlanRepository {
/// A plan fragment entry in the cache.
private static class FragInfo {
final Sha1Wrapper hash;
final long fragId;
final byte[] plan;
int refCount;
/// The ticker value current when this fragment was last (dis)used.
/// A new FragInfo or any other not in the LRU map because it is being referenced has value 0.
/// A non-zero value is either the fragment's current key in the LRU map OR its intended/future
/// key, if it has been lazily updated after the fragment was reused.
long lastUse;
/// The statement text for this fragment. For ad hoc queries this may be null, since
/// there is no single statement text---ad hoc queries that differ only by their constants
/// reuse the same plan.
String stmtText;
FragInfo(Sha1Wrapper key, byte[] plan, long nextId, String stmtText)
{
this.hash = key;
this.plan = plan;
this.fragId = nextId;
this.refCount = 0;
this.lastUse = 0;
this.stmtText = stmtText;
}
}
private static HashMap<Sha1Wrapper, FragInfo> m_plansByHash = new HashMap<Sha1Wrapper, FragInfo>();
private static HashMap<Long, FragInfo> m_plansById = new HashMap<Long, FragInfo>();
private static TreeMap<Long, FragInfo> m_plansLRU = new TreeMap<Long, FragInfo>();
/// A ticker that provides temporary ids for all cached fragments, for communicating with the EE.
private static final long INITIAL_FRAG_ID = 5000;
private static long m_nextFragId = INITIAL_FRAG_ID;
/// A ticker that allows the sequencing of all fragment uses, providing a key to the LRU map.
private static long m_nextFragUse = 1;
/**
* Get the site-local fragment id for a given plan identified by 20-byte sha-1 hash
*/
public static long getFragmentIdForPlanHash(byte[] planHash) {
Sha1Wrapper key = new Sha1Wrapper(planHash);
FragInfo frag = null;
synchronized (FragInfo.class) {
frag = m_plansByHash.get(key);
}
assert(frag != null);
return frag.fragId;
}
/**
* Get the statement text for the fragment identified by its hash
*/
public static String getStmtTextForPlanHash(byte[] planHash) {
Sha1Wrapper key = new Sha1Wrapper(planHash);
FragInfo frag = null;
synchronized (FragInfo.class) {
frag = m_plansByHash.get(key);
}
assert(frag != null);
assert(frag.stmtText != null);
return frag.stmtText;
}
/**
* Get the site-local fragment id for a given plan identified by 20-byte sha-1 hash
* If the plan isn't known to this SPC, load it up. Otherwise addref it.
*/
public static long loadOrAddRefPlanFragment(byte[] planHash, byte[] plan, String stmtText) {
Sha1Wrapper key = new Sha1Wrapper(planHash);
synchronized (FragInfo.class) {
FragInfo frag = m_plansByHash.get(key);
if (frag == null) {
frag = new FragInfo(key, plan, m_nextFragId++, stmtText);
m_plansByHash.put(frag.hash, frag);
m_plansById.put(frag.fragId, frag);
if (m_plansById.size() > ExecutionEngine.EE_PLAN_CACHE_SIZE) {
evictLRUfragment();
}
}
// The fragment MAY be in the LRU map.
// An incremented refCount is a lazy way to keep it safe from eviction
// without having to update the map.
// This optimizes for popular fragments in a small or stable cache that may be reused
// many times before the eviction process needs to take any notice.
frag.refCount++;
return frag.fragId;
}
}
private static void evictLRUfragment() {
/// Evict the least recently used fragment (if any are currently unused).
/// Along the way, update any obsolete entries that were left
/// by the laziness of the fragment state changes (fragment reuse).
/// In the rare case of a cache bloated beyond its usual limit,
/// keep evicting as needed and as entries are available until the bloat is gone.
while ( ! m_plansLRU.isEmpty()) {
// Remove the earliest entry.
Entry<Long, FragInfo> lru = m_plansLRU.pollFirstEntry();
FragInfo frag = lru.getValue();
if (frag.refCount > 0) {
// The fragment is being re-used, it is no longer an eviction candidate.
// It is only in the map due to the laziness in loadOrAddRefPlanFragment.
// It will be re-considered (at a later key) once it is no longer referenced.
// Resetting its lastUse to 0, here, restores it to a state identical to that
// of a new fragment.
// It eventually causes decrefPlanFragmentById to put it back in the map
// at its then up-to-date key.
// This makes it safe to keep out of the LRU map for now.
// See the comment in decrefPlanFragmentById and the one in the next code block.
frag.lastUse = 0;
}
else if (lru.getKey() != frag.lastUse) {
// The fragment is not in use but has been re-used more recently than the key reflects.
// This is a result of the laziness in decrefPlanFragmentById.
// Correct the entry's key in the LRU map to reflect its last use.
// This may STILL be the least recently used entry.
// If so, it will be picked off in a later iteration of this loop;
// its key will now match its lastUse value.
m_plansLRU.put(frag.lastUse, frag);
}
else {
// Found and removed the actual up-to-date least recently used entry from the LRU map.
// Remove the entry from the other collections.
m_plansById.remove(frag.fragId);
m_plansByHash.remove(frag.hash);
// Normally, one eviction for each new fragment is enough to restore order.
// BUT, if a prior call ever failed to find an unused fragment in the cache,
// the cache may have grown beyond its normal size. In that rare case,
// one eviction is not enough to reduce the cache to the desired size,
// so take another bite at the apple.
// Otherwise, trading exactly one evicted fragment for each new fragment
// would never reduce the cache.
if (m_plansById.size() > ExecutionEngine.EE_PLAN_CACHE_SIZE) {
continue;
}
return;
}
}
// Strange. All FragInfo entries appear to be in use. There's nothing to evict.
// Let the cache bloat a little and try again later after the next new fragment.
}
/**
* Decref the plan associated with this site-local fragment id. If the refcount
* goes to 0, the plan may be removed (depending on caching policy).
*/
public static void decrefPlanFragmentById(long fragmentId) {
// skip dummy/invalid fragment ids
if (fragmentId <= 0) return;
FragInfo frag = null;
synchronized (FragInfo.class) {
frag = m_plansById.get(fragmentId);
// The assert that used to be here would fail in TestAdHocQueries when it
// re-initialized the RealVoltDB, clearing the m_plansById before
// all SQLStmts were finalized. Maybe that's just a "test bug" that would be
// better fixed with some kind of test-only cleanup hook?
// OR It's possible that this early return is covering for a minor bug.
// Maybe SQLStmt.finalize is calling this method when it shouldn't?
// Maybe that's because the SQLStmt site member should be null in more cases?
//assert(frag != null);
if (frag == null) {
return;
}
if (--frag.refCount == 0) {
// The disused fragment belongs in the LRU map at the end -- at the current "ticker".
// If its lastUse value is 0 like a new entry's, it is not currently in the map.
// Put into the map in its proper position.
// If it is already in the LRU map (at a "too early" entry), just set its lastUse value
// as a cheap way to notify evictLRUfragment that it is not ready for eviction but
// should instead be re-ordered further forward in the map.
// This re-ordering only needs to happen when the eviction process considers the entry.
// For a popular fragment in a small or stable cache, that may be after MANY
// re-uses like this.
// This prevents thrashing of the LRU map, repositioning recent entries.
boolean notInLRUmap = (frag.lastUse == 0); // check this BEFORE updating lastUse
frag.lastUse = ++m_nextFragUse;
if (notInLRUmap) {
m_plansLRU.put(frag.lastUse, frag);
}
}
}
}
/**
* Get the full JSON plan associated with a given site-local fragment id.
* Called by the EE
*/
public static byte[] planForFragmentId(long fragmentId) {
assert(fragmentId > 0);
FragInfo frag = null;
synchronized (FragInfo.class) {
frag = m_plansById.get(fragmentId);
}
assert(frag != null);
return frag.plan;
}
@Deprecated
public static void addFragmentForTest(long fragmentId, byte[] plan, String stmtText) {
Sha1Wrapper key = new Sha1Wrapper(new byte[20]);
synchronized (FragInfo.class) {
FragInfo frag = new FragInfo(key, plan, fragmentId, stmtText);
m_plansById.put(frag.fragId, frag);
frag.refCount++;
}
}
public static void clear() {
synchronized (FragInfo.class) {
m_plansById.clear();
m_plansByHash.clear();
m_plansLRU.clear();
m_nextFragId = INITIAL_FRAG_ID;
m_nextFragUse = 1;
}
}
}