/* * JBoss, Home of Professional Open Source. * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. Some portions may be licensed * to Red Hat, Inc. under one or more contributor license agreements. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. */ package org.teiid.common.buffer.impl; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import org.teiid.client.BatchSerializer; import org.teiid.client.ResizingArrayList; import org.teiid.client.util.ExceptionUtil; import org.teiid.common.buffer.*; import org.teiid.common.buffer.AutoCleanupUtil.Removable; import org.teiid.common.buffer.LobManager.ReferenceMode; import org.teiid.core.TeiidComponentException; import org.teiid.core.TeiidRuntimeException; import org.teiid.core.types.DataTypeManager; import org.teiid.core.types.DataTypeManager.WeakReferenceHashedValueCache; import org.teiid.core.types.Streamable; import org.teiid.core.util.Assertion; import org.teiid.dqp.internal.process.DQPConfiguration; import org.teiid.dqp.internal.process.RequestWorkItem; import org.teiid.logging.LogConstants; import org.teiid.logging.LogManager; import org.teiid.logging.MessageLevel; import org.teiid.query.QueryPlugin; import org.teiid.query.ReplicatedObject; import org.teiid.query.processor.relational.ListNestedSortComparator; import org.teiid.query.sql.symbol.ElementSymbol; import org.teiid.query.sql.symbol.Expression; import org.teiid.query.util.CommandContext; import org.teiid.query.util.Options; /** * <p>Default implementation of BufferManager.</p> * Responsible for creating/tracking TupleBuffers and providing access to the StorageManager. * </p> * * TODO: add detection of pinned batches to prevent unnecessary purging of non-persistent batches * - this is not necessary for already persistent batches, since we hold a weak reference * * TODO: add a pre-fetch for tuplebuffers or some built-in correlation logic with the queue. */ public class BufferManagerImpl implements BufferManager, ReplicatedObject<String> { private static final int SYSTEM_OVERHEAD_MEGS = 150; /** * Asynch cleaner attempts to age out old entries and to reduce the memory size when * little is reserved. */ private static final int MAX_READ_AGE = 1<<19; private static final class Cleaner extends TimerTask { WeakReference<BufferManagerImpl> bufferRef; public Cleaner(BufferManagerImpl bufferManagerImpl) { this.bufferRef = new WeakReference<BufferManagerImpl>(bufferManagerImpl); } @Override public void run() { while (true) { BufferManagerImpl impl = this.bufferRef.get(); if (impl == null) { this.cancel(); return; } AutoCleanupUtil.doCleanup(false); impl.cleaning.set(true); try { long evicted = impl.doEvictions(impl.maxProcessingBytes, true, impl.initialEvictionQueue); if (evicted != 0 && LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.TRACE)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, "Asynch eviction run", evicted, impl.reserveBatchBytes.get(), impl.maxReserveBytes, impl.activeBatchBytes.get()); //$NON-NLS-1$ } if (evicted < impl.maxProcessingBytes) { long secondEvicted = impl.doEvictions(impl.maxProcessingBytes/2, true, impl.evictionQueue); if (secondEvicted != 0 && LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.TRACE)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, "Asynch eviction run", secondEvicted, impl.reserveBatchBytes.get(), impl.maxReserveBytes, impl.activeBatchBytes.get()); //$NON-NLS-1$ } } } catch (Throwable t) { LogManager.logDetail(LogConstants.CTX_BUFFER_MGR, t, "Exception during cleaning run"); //$NON-NLS-1$ } synchronized (this) { impl.cleaning.set(false); try { this.wait(100); } catch (InterruptedException e) { break; } } } } } private final class Remover implements Removable { private Long id; private AtomicBoolean prefersMemory; public Remover(Long id, AtomicBoolean prefersMemory) { this.id = id; this.prefersMemory = prefersMemory; } @Override public void remove() { removeCacheGroup(id, prefersMemory.get()); } } /** * This estimate is based upon adding the value to 2/3 maps and having CacheEntry/PhysicalInfo keys */ private static final long BATCH_OVERHEAD = 128; final class BatchManagerImpl implements BatchManager, Serializer<List<? extends List<?>>> { final Long id; SizeUtility sizeUtility; private WeakReference<BatchManagerImpl> ref = new WeakReference<BatchManagerImpl>(this); private PhantomReference<Object> cleanup; AtomicBoolean prefersMemory = new AtomicBoolean(); String[] types; private LobManager lobManager; private long totalSize; private long currentSize; private long rowsSampled; private BatchManagerImpl(Long newID, Class<?>[] types) { this.id = newID; this.sizeUtility = new SizeUtility(types); this.types = new String[types.length]; for (int i = 0; i < types.length; i++) { this.types[i] = DataTypeManager.getDataTypeName(types[i]); } } @Override public Long getId() { return id; } public void setLobManager(LobManager lobManager) { this.lobManager = lobManager; } @Override public String[] getTypes() { return types; } @Override public boolean prefersMemory() { return prefersMemory.get(); } @Override public void setPrefersMemory(boolean prefers) { //TODO: it's only expected to move from not preferring to preferring this.prefersMemory.set(prefers); } @Override public boolean useSoftCache() { return prefersMemory.get(); } @Override public Reference<? extends BatchManager> getBatchManagerReference() { return ref; } @Override public Long createManagedBatch(List<? extends List<?>> batch, Long previous, boolean removeOld) throws TeiidComponentException { if (cleanup == null) { cache.createCacheGroup(id); cleanup = AutoCleanupUtil.setCleanupReference(this, new Remover(id, prefersMemory)); } CacheEntry old = null; if (previous != null) { old = fastGet(previous, prefersMemory.get(), true); //check to see if we can reuse the existing entry if (removeOld) { if (old != null) { synchronized (old) { int oldRowCount = ((List)old.getObject()).size(); if (!old.isPersistent() && (batch.size() > (oldRowCount>>2) && batch.size() < (oldRowCount<<1))) { old.setObject(batch); return previous; } } } remove(previous); } } int sizeEstimate = getSizeEstimate(batch); updateEstimates(sizeEstimate, false); totalSize += sizeEstimate; rowsSampled += batch.size(); Long oid = batchAdded.getAndIncrement(); CacheKey key = new CacheKey(oid, readAttempts.get(), old!=null?old.getKey().getOrderingValue():0); CacheEntry ce = new CacheEntry(key, sizeEstimate, batch, this.ref, false); if (!cache.addToCacheGroup(id, ce.getId())) { this.remove(); throw new TeiidComponentException(QueryPlugin.Event.TEIID31138, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31138, id)); } overheadBytes.addAndGet(BATCH_OVERHEAD); if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.TRACE)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, "Add batch to BufferManager", this.id, ce.getId(), "with size estimate", ce.getSizeEstimate()); //$NON-NLS-1$ //$NON-NLS-2$ } addMemoryEntry(ce); return oid; } private void updateEstimates(long sizeEstimate, boolean remove) throws TeiidComponentException { if (remove) { sizeEstimate = -sizeEstimate; } currentSize += sizeEstimate; if (!remove && currentSize > maxBatchManagerSizeEstimate) { this.remove(); throw new TeiidComponentException(QueryPlugin.Event.TEIID31261, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31261, maxBatchManagerSizeEstimate, id)); } CommandContext threadLocalContext = CommandContext.getThreadLocalContext(); if (threadLocalContext != null) { long bytesUsed = threadLocalContext.getSession().addAndGetBytesUsed(sizeEstimate); if (!remove && bytesUsed > maxSessionBatchManagerSizeEstimate) { //TODO: kill the session? this.remove(); throw new TeiidComponentException(QueryPlugin.Event.TEIID31261, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31262, maxSessionBatchManagerSizeEstimate, id)); } } } @Override public List<? extends List<?>> deserialize(ObjectInput ois) throws IOException, ClassNotFoundException { List<? extends List<?>> batch = BatchSerializer.readBatch(ois, types); if (lobManager != null) { for (int i = batch.size() - 1; i >= 0; i--) { try { lobManager.updateReferences(batch.get(i), ReferenceMode.ATTACH); } catch (TeiidComponentException e) { throw new TeiidRuntimeException(QueryPlugin.Event.TEIID30052, e); } } } return batch; } @Override public void serialize(List<? extends List<?>> obj, ObjectOutput oos) throws IOException { ResizingArrayList<?> list = null; if (obj instanceof ResizingArrayList<?>) { list = (ResizingArrayList<?>)obj; } try { //it's expected that the containing structure has updated the lob manager BatchSerializer.writeBatch(oos, types, obj); } catch (RuntimeException e) { if (ExceptionUtil.getExceptionOfType(e, ClassCastException.class) != null) { throw e; } //there is a chance of a concurrent persist while modifying //in which case we want to swallow this exception if (list == null) { throw e; } LogManager.logDetail(LogConstants.CTX_BUFFER_MGR, e, "Possible Concurrent Modification", id); //$NON-NLS-1$ } } public int getSizeEstimate(List<? extends List<?>> obj) { return (int) Math.max(1, sizeUtility.getBatchSize(DataTypeManager.isValueCacheEnabled(), obj)); } @SuppressWarnings("unchecked") @Override public List<List<?>> getBatch(Long batch, boolean retain) throws TeiidComponentException { cleanSoftReferences(); long reads = readAttempts.incrementAndGet(); if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.TRACE)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, id, "getting batch", batch, "total reads", reads, "reference hits", referenceHit.get()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } CacheEntry ce = fastGet(batch, prefersMemory.get(), retain); if (ce != null) { if (!retain) { updateEstimates(ce.getSizeEstimate(), true); } return (List<List<?>>)(!retain?ce.nullOut():ce.getObject()); } //obtain a granular lock to prevent double memory loading Object o = cache.lockForLoad(batch, this); try { ce = fastGet(batch, prefersMemory.get(), retain); if (ce != null) { if (!retain) { updateEstimates(ce.getSizeEstimate(), true); } return (List<List<?>>)(!retain?ce.nullOut():ce.getObject()); } long count = readCount.incrementAndGet(); if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.DETAIL)) { LogManager.logDetail(LogConstants.CTX_BUFFER_MGR, id, "reading batch", batch, "from storage, total reads:", count); //$NON-NLS-1$ //$NON-NLS-2$ } ce = cache.get(o, batch, this.ref); if (ce == null) { throw new AssertionError("Batch not found in storage " + batch); //$NON-NLS-1$ } if (!retain) { updateEstimates(ce.getSizeEstimate(), true); removeFromCache(this.id, batch); persistBatchReferences(ce.getSizeEstimate()); } else { addMemoryEntry(ce); } } finally { cache.unlockForLoad(o); } return (List<List<?>>)ce.getObject(); } @Override public void remove(Long batch) { Integer sizeEstimate = BufferManagerImpl.this.remove(id, batch, prefersMemory.get()); if (sizeEstimate != null) { try { updateEstimates(sizeEstimate, true); } catch (TeiidComponentException e) { } } } @Override public void remove() { if (cleanup != null) { try { updateEstimates(currentSize, true); } catch (TeiidComponentException e) { } removeCacheGroup(id, prefersMemory.get()); AutoCleanupUtil.removeCleanupReference(cleanup); cleanup = null; } } @Override public String toString() { return id.toString(); } @Override public int getRowSizeEstimate() { if (rowsSampled == 0) { return 0; } return (int)(totalSize/rowsSampled); } @Override public String describe(List<? extends List<?>> obj) { return "Batch of " + obj.size() + " rows of " + types; //$NON-NLS-1$ //$NON-NLS-2$ } } private static class BatchSoftReference extends SoftReference<CacheEntry> { private int sizeEstimate; private Long key; public BatchSoftReference(CacheEntry referent, ReferenceQueue<? super CacheEntry> q, int sizeEstimate) { super(referent, q); this.sizeEstimate = sizeEstimate; this.key = referent.getId(); } } static final int CONCURRENCY_LEVEL = 32; //TODO: make this configurable since it is roughly the same as max active plans private static final int TARGET_BYTES_PER_ROW = 1 << 11; //2k bytes per row private static ReferenceQueue<CacheEntry> SOFT_QUEUE = new ReferenceQueue<CacheEntry>(); // Configuration private int processorBatchSize = BufferManager.DEFAULT_PROCESSOR_BATCH_SIZE; //set to acceptable defaults for testing private int maxProcessingBytes = 1 << 21; private Integer maxProcessingBytesOrig; long maxReserveBytes = 1 << 28; AtomicLong reserveBatchBytes = new AtomicLong(); AtomicLong overheadBytes = new AtomicLong(); private int maxActivePlans = DQPConfiguration.DEFAULT_MAX_ACTIVE_PLANS; //used as a hint to set the reserveBatchKB private boolean useWeakReferences = true; private boolean inlineLobs = true; private int targetBytesPerRow = TARGET_BYTES_PER_ROW; private int maxSoftReferences; private int nominalProcessingMemoryMax = maxProcessingBytes; private ReentrantLock lock = new ReentrantLock(); private Condition batchesFreed = lock.newCondition(); AtomicLong activeBatchBytes = new AtomicLong(); private AtomicLong readAttempts = new AtomicLong(); //TODO: consider the size estimate in the weighting function LrfuEvictionQueue<CacheEntry> evictionQueue = new LrfuEvictionQueue<CacheEntry>(readAttempts); LrfuEvictionQueue<CacheEntry> initialEvictionQueue = new LrfuEvictionQueue<CacheEntry>(readAttempts); ConcurrentHashMap<Long, CacheEntry> memoryEntries = new ConcurrentHashMap<Long, CacheEntry>(16, .75f, CONCURRENCY_LEVEL); //limited size reference caches based upon the memory settings private WeakReferenceHashedValueCache<CacheEntry> weakReferenceCache; private Map<Long, BatchSoftReference> softCache = Collections.synchronizedMap(new LinkedHashMap<Long, BatchSoftReference>(16, .75f, false) { private static final long serialVersionUID = 1L; protected boolean removeEldestEntry(Map.Entry<Long,BatchSoftReference> eldest) { if (size() > maxSoftReferences) { BatchSoftReference bsr = eldest.getValue(); clearSoftReference(bsr); return true; } return false; } }); private Cache cache; private StorageManager storageManager; private Map<String, TupleReference> tupleBufferMap = new ConcurrentHashMap<String, TupleReference>(); private ReferenceQueue<TupleBuffer> tupleBufferQueue = new ReferenceQueue<TupleBuffer>(); private AtomicLong tsId = new AtomicLong(); private AtomicLong batchAdded = new AtomicLong(); private AtomicLong readCount = new AtomicLong(); private AtomicLong writeCount = new AtomicLong(); private AtomicLong referenceHit = new AtomicLong(); //TODO: this does not scale well with multiple embedded instances private static final Timer timer = new Timer("BufferManager Cleaner", true); //$NON-NLS-1$ private Cleaner cleaner; private AtomicBoolean cleaning = new AtomicBoolean(); private long maxFileStoreLength = Long.MAX_VALUE; private long maxBatchManagerSizeEstimate = Long.MAX_VALUE; private long maxSessionBatchManagerSizeEstimate = Long.MAX_VALUE; public BufferManagerImpl() { this.cleaner = new Cleaner(this); timer.schedule(cleaner, 100); } void clearSoftReference(BatchSoftReference bsr) { synchronized (bsr) { overheadBytes.addAndGet(-bsr.sizeEstimate); bsr.sizeEstimate = 0; } bsr.clear(); } private Integer removeFromCache(Long gid, Long batch) { Integer result = cache.remove(gid, batch); if (result != null) { overheadBytes.addAndGet(-BATCH_OVERHEAD); } return result; } public long getBatchesAdded() { return batchAdded.get(); } public long getReadCount() { return readCount.get(); } public long getWriteCount() { return writeCount.get(); } public long getReadAttempts() { return readAttempts.get(); } @Override public int getMaxProcessingSize() { return maxProcessingBytes; } public long getReserveBatchBytes() { return reserveBatchBytes.get(); } /** * Get processor batch size * @return Number of rows in a processor batch */ @Override public int getProcessorBatchSize() { return this.processorBatchSize; } public void setTargetBytesPerRow(int targetBytesPerRow) { this.targetBytesPerRow = targetBytesPerRow; } public void setProcessorBatchSize(int processorBatchSize) { this.processorBatchSize = processorBatchSize; } @Override public TupleBuffer createTupleBuffer(final List elements, String groupName, TupleSourceType tupleSourceType) { final Long newID = this.tsId.getAndIncrement(); int[] lobIndexes = LobManager.getLobIndexes(elements); Class<?>[] types = getTypeClasses(elements); BatchManagerImpl batchManager = createBatchManager(newID, types); LobManager lobManager = null; if (lobIndexes != null) { FileStore lobStore = createFileStore(newID + "_lobs"); //$NON-NLS-1$ lobManager = new LobManager(lobIndexes, lobStore); batchManager.setLobManager(lobManager); } TupleBuffer tupleBuffer = new TupleBuffer(batchManager, String.valueOf(newID), elements, lobManager, getProcessorBatchSize(elements)); if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.DETAIL)) { LogManager.logDetail(LogConstants.CTX_BUFFER_MGR, "Creating TupleBuffer:", newID, elements, Arrays.toString(types), "batch size", tupleBuffer.getBatchSize(), "of type", tupleSourceType); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } tupleBuffer.setInlineLobs(inlineLobs); return tupleBuffer; } public STree createSTree(final List<? extends Expression> elements, String groupName, int keyLength) { Long newID = this.tsId.getAndIncrement(); int[] lobIndexes = LobManager.getLobIndexes(elements); Class<?>[] types = getTypeClasses(elements); BatchManagerImpl bm = createBatchManager(newID, types); LobManager lobManager = null; if (lobIndexes != null) { lobManager = new LobManager(lobIndexes, null); //persistence is not expected yet - later we might utilize storage for out-of-line lob values bm.setLobManager(lobManager); } BatchManager keyManager = createBatchManager(this.tsId.getAndIncrement(), Arrays.copyOf(types, keyLength)); int[] compareIndexes = new int[keyLength]; for (int i = 1; i < compareIndexes.length; i++) { compareIndexes[i] = i; } if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.DETAIL)) { LogManager.logDetail(LogConstants.CTX_BUFFER_MGR, "Creating STree:", newID, keyLength, elements); //$NON-NLS-1$ } return new STree(keyManager, bm, new ListNestedSortComparator(compareIndexes).defaultNullOrder(getOptions().getDefaultNullOrder()), getProcessorBatchSize(elements.subList(0, keyLength)), getProcessorBatchSize(elements), keyLength, lobManager); } private static Class<?>[] getTypeClasses(final List<? extends Expression> elements) { Class<?>[] types = new Class[elements.size()]; for (ListIterator<? extends Expression> i = elements.listIterator(); i.hasNext();) { Expression expr = i.next(); Class<?> type = expr.getType(); Assertion.isNotNull(type); types[i.previousIndex()] = type; } return types; } private BatchManagerImpl createBatchManager(final Long newID, Class<?>[] types) { return new BatchManagerImpl(newID, types); } @Override public FileStore createFileStore(String name) { if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.TRACE)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, "Creating FileStore:", name); //$NON-NLS-1$ } FileStore result = this.storageManager.createFileStore(name); result.setMaxLength(this.maxFileStoreLength); return result; } @Override public long getMaxStorageSpace() { return this.storageManager.getMaxStorageSpace(); } public Cache getCache() { return cache; } public void setMaxActivePlans(int maxActivePlans) { this.maxActivePlans = maxActivePlans; } public void setMaxProcessingKB(int maxProcessingKB) { if (maxProcessingKB > -1) { this.maxProcessingBytes = maxProcessingKB<<10; } else { this.maxProcessingBytes = -1; } } public void setMaxReserveKB(int maxReserveBatchKB) { if (maxReserveBatchKB > -1) { long maxReserve = ((long)maxReserveBatchKB)<<10; this.maxReserveBytes = maxReserve; this.reserveBatchBytes.set(maxReserve); } else { this.maxReserveBytes = -1; } } @Override public void initialize() throws TeiidComponentException { long maxMemory = Runtime.getRuntime().maxMemory(); maxMemory = Math.max(0, maxMemory - (SYSTEM_OVERHEAD_MEGS << 20)); //assume an overhead for the AS/system stuff if (getMaxReserveKB() < 0) { this.maxReserveBytes = 0; int one_gig = 1 << 30; if (maxMemory > one_gig) { //assume 70% of the memory over the first gig this.maxReserveBytes = (long)Math.max(0, (maxMemory - one_gig) * .5); } this.maxReserveBytes += Math.max(0, Math.min(one_gig, maxMemory) * .4); } this.reserveBatchBytes.set(maxReserveBytes); if (this.maxProcessingBytesOrig == null) { //store the config value so that we can be reinitialized (this is not a clean approach) this.maxProcessingBytesOrig = this.maxProcessingBytes; } if (this.maxProcessingBytesOrig < 0) { this.maxProcessingBytes = (int)Math.min(Math.max(processorBatchSize * targetBytesPerRow * 16l, (.07 * maxMemory)/Math.pow(maxActivePlans, .8)), Integer.MAX_VALUE); } if (this.storageManager != null) { long max = this.storageManager.getMaxStorageSpace(); this.maxFileStoreLength = (max/maxActivePlans)>>2; this.maxBatchManagerSizeEstimate = (long)(.8*((((long)this.getMaxReserveKB())<<10) + max + cache.getMemoryBufferSpace())/Math.sqrt(maxActivePlans)); if (this.options != null) { this.maxSessionBatchManagerSizeEstimate = this.options.getMaxSessionBufferSizeEstimate(); } this.maxBatchManagerSizeEstimate = Math.min(maxBatchManagerSizeEstimate, maxSessionBatchManagerSizeEstimate); } //make a guess at the max number of batches long memoryBatches = maxMemory / (processorBatchSize * targetBytesPerRow); //memoryBatches represents a full batch, so assume that most will be smaller int logSize = 67 - Long.numberOfLeadingZeros(memoryBatches); if (useWeakReferences) { weakReferenceCache = new WeakReferenceHashedValueCache<CacheEntry>(Math.min(30, logSize)); } this.maxSoftReferences = 1 << Math.min(30, logSize); this.nominalProcessingMemoryMax = (int)Math.max(Math.min(this.maxReserveBytes, 2*this.maxProcessingBytes), Math.min(Integer.MAX_VALUE, 2*this.maxReserveBytes/maxActivePlans)); } void setNominalProcessingMemoryMax(int nominalProcessingMemoryMax) { this.nominalProcessingMemoryMax = nominalProcessingMemoryMax; } @Override public void releaseOrphanedBuffers(long count) { releaseBuffers(count, false); } @Override public void releaseBuffers(int count) { releaseBuffers(count, true); } private void releaseBuffers(long count, boolean updateContext) { if (count < 1) { return; } if (updateContext) { if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.TRACE)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, "Releasing buffer space", count); //$NON-NLS-1$ } CommandContext context = CommandContext.getThreadLocalContext(); if (context != null) { context.addAndGetReservedBuffers((int)-count); } } else { if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.INFO)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, "Releasing orphaned buffer space", count); //$NON-NLS-1$ } } lock.lock(); try { this.reserveBatchBytes.addAndGet(count); batchesFreed.signalAll(); } finally { lock.unlock(); } } @Override public int reserveBuffers(int count, BufferReserveMode mode) { if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.TRACE)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, "Reserving buffer space", count, mode); //$NON-NLS-1$ } CommandContext context = CommandContext.getThreadLocalContext(); int existing = 0; if (context != null) { existing = (int)Math.min(Integer.MAX_VALUE, context.addAndGetReservedBuffers(0)); } int result = count; if (mode == BufferReserveMode.FORCE) { reserve(count, context); } else { lock.lock(); try { count = Math.min(count, nominalProcessingMemoryMax - existing); result = noWaitReserve(count, false, context); } finally { lock.unlock(); } } persistBatchReferences(result); return result; } private void reserve(int count, CommandContext context) { this.reserveBatchBytes.addAndGet(-count); if (context != null) { context.addAndGetReservedBuffers(count); } } @Override public int reserveBuffersBlocking(int count, long[] val, boolean force) throws BlockedException { if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.TRACE)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, "Reserving buffer space", count, force); //$NON-NLS-1$ } assert count >= 0; if (count == 0) { return 0; } int result = 0; int count_orig = count; CommandContext context = CommandContext.getThreadLocalContext(); long reserved = 0; if (context != null) { reserved = context.addAndGetReservedBuffers(0); //TODO: in theory we have to check the whole stack as we could be //issuing embedded queries back to ourselves } count = Math.min(count, (int)Math.min(Integer.MAX_VALUE, nominalProcessingMemoryMax - reserved)); if (count_orig != count && !force) { return 0; //is not possible to reserve the desired amount } result = noWaitReserve(count, true, context); if (result == 0) { if (val[0]++ == 0) { val[1] = System.currentTimeMillis(); } if (val[1] > 1) { long last = val[1]; val[1] = System.currentTimeMillis(); try { lock.lock(); if (val[1] - last < 10) { //if the time difference is too close, then wait to prevent tight spins //but we can't wait too long as we don't want to thread starve the system batchesFreed.await(20, TimeUnit.MILLISECONDS); } if ((val[0] << (force?16:18)) > count) { //aging out //TOOD: ideally we should be using a priority queue and better scheduling if (!force) { return 0; } reserve(count_orig, context); result = count_orig; } else { int min = 0; if (force) { min = 2*count/3; } else { min = 4*count/5; } //if a sample looks good proceed if (reserveBatchBytes.get() > min){ reserve(count_orig, context); result = count_orig; } } } catch (InterruptedException e) { throw new TeiidRuntimeException(e); } finally { lock.unlock(); } } if (result == 0) { if (context != null) { RequestWorkItem workItem = context.getWorkItem(); if (workItem != null) { //if we have a workitem (non-test scenario) then before //throwing blocked on memory to indicate there's more work workItem.moreWork(); } } throw BlockedException.BLOCKED_ON_MEMORY_EXCEPTION; } } if (force && result < count_orig) { reserve(count_orig - result, context); result = count_orig; } val[0] = 0; persistBatchReferences(result); return result; } private int noWaitReserve(int count, boolean allOrNothing, CommandContext context) { boolean success = false; for (int i = 0; !success && i < 2; i++) { long reserveBatch = this.reserveBatchBytes.get(); long overhead = this.overheadBytes.get(); long current = reserveBatch - overhead; if (allOrNothing) { if (count > current) { return 0; } } else if (count > current) { count = (int)Math.max(0, current); } if (count == 0) { return 0; } if (this.reserveBatchBytes.compareAndSet(reserveBatch, reserveBatch - count)) { success = true; } } //the value is changing rapidly, but we've already potentially adjusted the value twice, so just proceed if (!success) { this.reserveBatchBytes.addAndGet(-count); } if (context != null) { context.addAndGetReservedBuffers(count); } return count; } void persistBatchReferences(int max) { if (max <= 0) { return; } long activeBatch = activeBatchBytes.get() + overheadBytes.get(); long reserveBatch = reserveBatchBytes.get(); long memoryCount = activeBatch + maxReserveBytes - reserveBatch; if (memoryCount <= maxReserveBytes) { if (DataTypeManager.USE_VALUE_CACHE && DataTypeManager.isValueCacheEnabled() && memoryCount < maxReserveBytes / 8) { DataTypeManager.setValueCacheEnabled(false); } return; } else if (DataTypeManager.USE_VALUE_CACHE) { DataTypeManager.setValueCacheEnabled(true); } if (cleaning.compareAndSet(false, true)) { synchronized (cleaner) { cleaner.notify(); } } //we delay work here as there should be excess vm space, we are using an overestimate, and we want the cleaner to do the work if possible //TODO: track sizes held by each queue independently long maxToFree = Math.min(max, memoryCount - maxReserveBytes); LrfuEvictionQueue<CacheEntry> first = initialEvictionQueue; LrfuEvictionQueue<CacheEntry> second = evictionQueue; if (evictionQueue.getSize() > 2*initialEvictionQueue.getSize()) { //attempt to evict from the non-initial queue first as these should essentially be cost "free" and hopefully the reference cache can mitigate //the cost of rereading first = evictionQueue; second = initialEvictionQueue; } maxToFree -= doEvictions(maxToFree, false, first); if (maxToFree > 0) { maxToFree = Math.min(maxToFree, activeBatchBytes.get() + overheadBytes.get() - reserveBatchBytes.get()); if (maxToFree > 0) { doEvictions(maxToFree, false, second); } } } long doEvictions(long maxToFree, boolean ageOut, LrfuEvictionQueue<CacheEntry> queue) { if (queue == evictionQueue) { maxToFree = Math.min(maxToFree, this.maxProcessingBytes); } long freed = 0; while (freed <= maxToFree && ( ageOut || (queue == evictionQueue && activeBatchBytes.get() + overheadBytes.get() + this.maxReserveBytes/2 > reserveBatchBytes.get()) //nominal cleaning criterion || (queue != evictionQueue && activeBatchBytes.get() + overheadBytes.get() + 3*this.maxReserveBytes/4 > reserveBatchBytes.get()))) { //assume that basically all initial batches will need to be written out at some point CacheEntry ce = queue.firstEntry(!ageOut); if (ce == null) { break; } synchronized (ce) { if (!memoryEntries.containsKey(ce.getId())) { if (!ageOut) { queue.remove(ce); } continue; //not currently a valid eviction } } if (ageOut) { long lastAccess = ce.getKey().getLastAccess(); long currentTime = readAttempts.get(); long age = currentTime - lastAccess; if (age < MAX_READ_AGE) { ageOut = false; continue; } queue.remove(ce); } boolean evicted = true; try { evicted = evict(ce); } catch (Throwable e) { LogManager.logError(LogConstants.CTX_BUFFER_MGR, e, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30017, ce.getId() )); } finally { if (evicted) { synchronized (ce) { if (memoryEntries.remove(ce.getId()) != null) { freed += ce.getSizeEstimate(); activeBatchBytes.addAndGet(-ce.getSizeEstimate()); queue.remove(ce); //ensures that an intervening get will still be cleaned } } } } } return freed; } boolean evict(CacheEntry ce) throws Exception { Serializer<?> s = ce.getSerializer(); if (s == null) { return true; } boolean persist = false; synchronized (ce) { if (!ce.isPersistent()) { persist = true; ce.setPersistent(true); } } if (persist) { long count = writeCount.incrementAndGet(); if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.DETAIL)) { LogManager.logDetail(LogConstants.CTX_BUFFER_MGR, s.getId(), ce.getId(), "writing batch to storage, total writes: ", count); //$NON-NLS-1$ } } boolean result = cache.add(ce, s); if (s.useSoftCache()) { createSoftReference(ce); } else if (useWeakReferences) { weakReferenceCache.getValue(ce); //a get will set the value } return result; } private void createSoftReference(CacheEntry ce) { //if we don't set aside some reserve, we //will push the soft ref out of memory potentially too quickly int sizeEstimate = ce.getSizeEstimate()/2; BatchSoftReference ref = new BatchSoftReference(ce, SOFT_QUEUE, sizeEstimate); softCache.put(ce.getId(), ref); overheadBytes.addAndGet(sizeEstimate); } /** * Get a CacheEntry without hitting storage */ CacheEntry fastGet(Long batch, Boolean prefersMemory, boolean retain) { CacheEntry ce = null; if (retain) { ce = memoryEntries.get(batch); } else { ce = memoryEntries.remove(batch); } if (ce != null) { synchronized (ce) { if (retain) { //there is a minute chance the batch was evicted //this call ensures that we won't leak if (memoryEntries.containsKey(batch)) { if (ce.isPersistent()) { evictionQueue.touch(ce); } else { initialEvictionQueue.touch(ce); } } } else { evictionQueue.remove(ce); if (!ce.isPersistent()) { initialEvictionQueue.remove(ce); } } } if (!retain) { BufferManagerImpl.this.remove(ce, true); } return ce; } if (prefersMemory == null || prefersMemory) { BatchSoftReference bsr = softCache.remove(batch); if (bsr != null) { ce = bsr.get(); if (ce != null) { clearSoftReference(bsr); } } } if (ce == null && (prefersMemory == null || !prefersMemory) && useWeakReferences) { ce = weakReferenceCache.getByHash(batch); if (ce == null || !ce.getId().equals(batch)) { return null; } } if (ce != null && ce.getObject() != null) { referenceHit.getAndIncrement(); if (retain) { addMemoryEntry(ce); } else { BufferManagerImpl.this.remove(ce, false); } return ce; } return null; } private Options options; private Integer remove(Long gid, Long batch, boolean prefersMemory) { if (LogManager.isMessageToBeRecorded(LogConstants.CTX_BUFFER_MGR, MessageLevel.TRACE)) { LogManager.logTrace(LogConstants.CTX_BUFFER_MGR, "Removing batch from BufferManager", gid, batch); //$NON-NLS-1$ } cleanSoftReferences(); CacheEntry ce = fastGet(batch, prefersMemory, false); Integer result = null; if (ce == null) { result = removeFromCache(gid, batch); } else { result = ce.getSizeEstimate(); ce.nullOut(); } return result; } private void remove(CacheEntry ce, boolean inMemory) { if (inMemory) { activeBatchBytes.addAndGet(-ce.getSizeEstimate()); } assert activeBatchBytes.get() >= 0; Serializer<?> s = ce.getSerializer(); if (s != null) { removeFromCache(s.getId(), ce.getId()); } } void addMemoryEntry(CacheEntry ce) { persistBatchReferences(ce.getSizeEstimate()); synchronized (ce) { memoryEntries.put(ce.getId(), ce); if (!ce.isPersistent()) { initialEvictionQueue.add(ce); } else { evictionQueue.touch(ce); } } activeBatchBytes.getAndAdd(ce.getSizeEstimate()); } void removeCacheGroup(Long id, Boolean prefersMemory) { cleanSoftReferences(); if (cache == null) { return; //this could be called after shutdown } Collection<Long> vals = cache.removeCacheGroup(id); long overhead = vals.size() * BATCH_OVERHEAD; overheadBytes.addAndGet(-overhead); if (!vals.isEmpty()) { for (Long val : vals) { //TODO: we will unnecessarily call remove on the cache, but that should be low cost fastGet(val, prefersMemory, false); } } } void cleanSoftReferences() { for (int i = 0; i < 10; i++) { BatchSoftReference ref = (BatchSoftReference)SOFT_QUEUE.poll(); if (ref == null) { break; } softCache.remove(ref.key); clearSoftReference(ref); } } @Override public int getProcessorBatchSize(List<? extends Expression> schema) { return getSizeEstimates(schema)[0]; } private int[] getSizeEstimates(List<? extends Expression> elements) { int total = 0; boolean isValueCacheEnabled = DataTypeManager.isValueCacheEnabled(); for (int i = elements.size() - 1; i >= 0; i--) { Class<?> type = elements.get(i).getType(); total += SizeUtility.getSize(isValueCacheEnabled, type); } //assume 64-bit total += 8*elements.size() + 36; // column list / row overhead //nominal targetBytesPerRow but can scale up or down int totalCopy = total; boolean less = totalCopy < targetBytesPerRow; int rowCount = processorBatchSize; for (int i = 0; i < 3; i++) { if (less) { totalCopy <<= 1; } else { totalCopy >>= 2; } if (less && totalCopy > targetBytesPerRow || !less && totalCopy < targetBytesPerRow) { break; } if (less) { rowCount <<= 1; } else { rowCount >>= 1; } } rowCount = Math.max(1, rowCount); total *= rowCount; return new int[]{rowCount, Math.max(1, total)}; } @Override public int getSchemaSize(List<? extends Expression> elements) { return getSizeEstimates(elements)[1]; } public void shutdown() { this.cache.shutdown(); this.cache = null; this.memoryEntries.clear(); this.evictionQueue.getEvictionQueue().clear(); this.initialEvictionQueue.getEvictionQueue().clear(); this.cleaner.cancel(); } @Override public void addTupleBuffer(TupleBuffer tb) { cleanDefunctTupleBuffers(); this.tupleBufferMap.put(tb.getId(), new TupleReference(tb, this.tupleBufferQueue)); } @Override public void distributeTupleBuffer(String uuid, TupleBuffer tb) { tb.setId(uuid); addTupleBuffer(tb); } @Override public TupleBuffer getTupleBuffer(String id) { cleanDefunctTupleBuffers(); Reference<TupleBuffer> r = this.tupleBufferMap.get(id); if (r != null) { return r.get(); } return null; } private void cleanDefunctTupleBuffers() { while (true) { Reference<?> r = this.tupleBufferQueue.poll(); if (r == null) { break; } this.tupleBufferMap.remove(((TupleReference)r).id); } } static class TupleReference extends WeakReference<TupleBuffer>{ String id; public TupleReference(TupleBuffer referent, ReferenceQueue<? super TupleBuffer> q) { super(referent, q); id = referent.getId(); } } public void setUseWeakReferences(boolean useWeakReferences) { this.useWeakReferences = useWeakReferences; } @Override public void getState(OutputStream ostream) { } @Override public void getState(String state_id, OutputStream ostream) { TupleBuffer buffer = this.getTupleBuffer(state_id); if (buffer != null) { try { ObjectOutputStream out = new ObjectOutputStream(ostream); getTupleBufferState(out, buffer); out.flush(); } catch (TeiidComponentException e) { throw new TeiidRuntimeException(QueryPlugin.Event.TEIID30054, e); } catch (IOException e) { throw new TeiidRuntimeException(QueryPlugin.Event.TEIID30055, e); } } } private void getTupleBufferState(ObjectOutputStream out, TupleBuffer buffer) throws TeiidComponentException, IOException { out.writeLong(buffer.getRowCount()); out.writeInt(buffer.getBatchSize()); out.writeObject(buffer.getTypes()); for (int row = 1; row <= buffer.getRowCount(); row+=buffer.getBatchSize()) { TupleBatch b = buffer.getBatch(row); BatchSerializer.writeBatch(out, buffer.getTypes(), b.getTuples()); } } @Override public void setState(InputStream istream) { } @Override public void setState(String state_id, InputStream istream) { TupleBuffer buffer = this.getTupleBuffer(state_id); if (buffer == null) { try { ObjectInputStream in = new ObjectInputStream(istream); setTupleBufferState(state_id, in); } catch (IOException e) { throw new TeiidRuntimeException(QueryPlugin.Event.TEIID30056, e); } catch(ClassNotFoundException e) { throw new TeiidRuntimeException(QueryPlugin.Event.TEIID30057, e); } catch(TeiidComponentException e) { throw new TeiidRuntimeException(QueryPlugin.Event.TEIID30058, e); } } } private void setTupleBufferState(String state_id, ObjectInputStream in) throws IOException, ClassNotFoundException, TeiidComponentException { long rowCount = in.readLong(); int batchSize = in.readInt(); String[] types = (String[])in.readObject(); List<ElementSymbol> schema = new ArrayList<ElementSymbol>(types.length); for (int i = 0; i < types.length; i++) { ElementSymbol es = new ElementSymbol("x"); //$NON-NLS-1$ es.setType(DataTypeManager.getDataTypeClass(types[i])); schema.add(es); } TupleBuffer buffer = createTupleBuffer(schema, "cached", TupleSourceType.FINAL); //$NON-NLS-1$ buffer.setBatchSize(batchSize); buffer.setId(state_id); for (int row = 1; row <= rowCount; row+=batchSize) { List<List<Object>> batch = BatchSerializer.readBatch(in, types); for (int i = 0; i < batch.size(); i++) { buffer.addTuple(batch.get(i)); } } if (buffer.getRowCount() != rowCount) { buffer.remove(); throw new IOException(QueryPlugin.Util.getString("not_found_cache")); //$NON-NLS-1$ } buffer.close(); addTupleBuffer(buffer); } @Override public void setAddress(Serializable address) { } @Override public void droppedMembers(Collection<Serializable> addresses) { } public void setInlineLobs(boolean inlineLobs) { this.inlineLobs = inlineLobs; } public int getMaxReserveKB() { return (int)(maxReserveBytes>>10); } public void setCache(Cache cache) { this.cache = cache; this.storageManager = cache; } public int getMemoryCacheEntries() { return memoryEntries.size(); } public long getActiveBatchBytes() { return activeBatchBytes.get(); } @Override public boolean hasState(String stateId) { return this.getTupleBuffer(stateId) != null; } public long getReferenceHits() { return referenceHit.get(); } @Override public void persistLob(Streamable<?> lob, FileStore store, byte[] bytes) throws TeiidComponentException { LobManager.persistLob(lob, store, bytes, inlineLobs, DataTypeManager.MAX_LOB_MEMORY_BYTES); } public void invalidCacheGroup(Long gid) { removeCacheGroup(gid, null); } @Override public void setOptions(Options options) { this.options = options; } @Override public Options getOptions() { if (options == null) { options = new Options(); } return options; } public void setStorageManager(StorageManager ssm) { this.storageManager = ssm; } public void setMaxSessionBatchManagerSizeEstimate( long maxSessionBatchManagerSizeEstimate) { this.maxSessionBatchManagerSizeEstimate = maxSessionBatchManagerSizeEstimate; } }