/**
* VMware Continuent Tungsten Replicator
* Copyright (C) 2015 VMware, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Initial developer(s): Robert Hodges
* Contributor(s):
*/
package com.continuent.tungsten.common.cache;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.apache.log4j.Logger;
/**
* Manages a cache of resizable raw byte vectors. This class is ensures that the
* cache does not exceed resource limits, which include the following:
* <ul>
* <li>Number of bytes in memory for a single vector</li>
* <li>Number of bytes in memory for all vectors</li>
* <li>Number of open file descriptors writing to storage</li>
* </ul>
* Each vector has a key associated with it. It is the responsibility of clients
* to create a unique key. The cache class offers methods to allocate, append,
* read, resize, and deallocate vectors using that key.
* <p/>
* Individual vectors are initially as a set of extents in memory. Once the
* vector exceeds the number of memory bytes allowed for a single vector, or the
* cache exceeds remaining memory bytes, new bytes are written to storage. To
* keep things sane, bytes are effectively immutable unless the vector is
* resized to a smaller length, in which case any extra bytes are truncated.
*/
public class RawByteCache
{
private static final Logger logger = Logger.getLogger(RawByteCache.class);
// Cache settings. Initial maximum sizes will cause everything to remain in
// memory.
private final File cacheDir;
private final long maxCacheBytes;
private final long maxObjectBytes;
private final int maxOpenFiles;
// Cache control properties.
private long currentMemoryBytes = 0;
private long currentStorageBytes = 0;
private Map<Object, RawByteAllocator> rawByteAllocatorCache = new HashMap<Object, RawByteAllocator>();
private IndexedLRUCache<FileOutputStream> outputStreamCache;
/**
* Instantiates a new cache.
*
* @param cacheDir Location of cache files
* @param maxCacheBytes Maximum overall bytes to store in memory; above this
* level all new vectors flush to storage
* @param maxObjectBytes Maximum bytes to store for a single vector; bytes
* go to storage above this level
* @param maxOpenFiles Maximum number of cached files that may be open at
* once
*/
public RawByteCache(File cacheDir, long maxCacheBytes, long maxObjectBytes,
int maxOpenFiles)
{
this.cacheDir = cacheDir;
this.maxCacheBytes = maxCacheBytes;
this.maxObjectBytes = maxObjectBytes;
this.maxOpenFiles = maxOpenFiles;
}
/** Return number of elements in the cache. */
public long getSize()
{
return rawByteAllocatorCache.size();
}
/** Return number of bytes of memory used by cache. */
public long getCurrentMemoryBytes()
{
return currentMemoryBytes;
}
/** Return number of bytes of storage used by cache. */
public long getCurrentStorageBytes()
{
return currentStorageBytes;
}
/**
* Prepares the cache for use.
*/
public synchronized void prepare()
{
// Ensure object cache exists.
logger.info("Opening byte vector cache: " + cacheDir.getAbsolutePath());
cacheDir.mkdirs();
if (!cacheDir.exists() || !cacheDir.isDirectory())
{
throw new RuntimeException(
"Unable to access byte vector cache directory: "
+ cacheDir.getAbsolutePath());
}
// Delete any files currently in the cache.
for (File cachedFile : cacheDir.listFiles())
{
if (!cachedFile.delete())
{
throw new RuntimeException("Unable to clear cached vector: "
+ cachedFile.getAbsolutePath());
}
}
// Create the LRU cache for managing output streams with an inner class
// to release streams after use.
CacheResourceManager<FileOutputStream> cacheResourceManager = new CacheResourceManager<FileOutputStream>()
{
public void release(FileOutputStream stream)
{
try
{
stream.close();
}
catch (IOException e)
{
}
}
};
outputStreamCache = new IndexedLRUCache<FileOutputStream>(maxOpenFiles,
cacheResourceManager);
}
/**
* Releases the cache and deletes files.
*/
public synchronized void release()
{
// Invalidate the OutputStream cache, which closes any outstanding
// streams.
if (outputStreamCache != null)
outputStreamCache.invalidateAll();
// Clear the cache.
if (cacheDir.exists() && cacheDir.isDirectory())
{
for (File cachedFile : cacheDir.listFiles())
{
if (!cachedFile.delete())
{
logger.warn("Unable to clear cached vector: "
+ cachedFile.getAbsolutePath());
}
}
}
}
/**
* Allocates a cached vector, failing if the vector is already present.
*
* @param key Object reference
*/
public synchronized void allocate(String key)
{
if (this.rawByteAllocatorCache.get(key) == null)
{
RawByteAllocator alloc = new RawByteAllocator();
alloc.key = key;
rawByteAllocatorCache.put(key, alloc);
}
else
{
throw new RuntimeException(
"Attempt to allocate existing vector: key=" + key);
}
}
/**
* Deallocates the cached vector, freeing all associated resources. Fails if
* the vector is not cached.
*
* @param key Object reference
*/
public void deallocate(String key)
{
RawByteAllocator alloc = rawByteAllocatorCache.remove(key);
if (key == null)
{
logger.warn("Attempt to deallocate unknown vector from cache: key="
+ key);
}
else
{
// Null out array to free memory buffers.
alloc.buffers = null;
currentMemoryBytes -= alloc.memoryLength;
// Invalidate any open output stream on the file system and
// delete storage.
if (alloc.storageLength > 0)
{
outputStreamCache.invalidate(key);
alloc.cacheFile.delete();
this.currentStorageBytes -= alloc.storageLength;
}
}
}
/**
* Adds a range of bytes to the end of the vector.
*
* @param key Object reference
* @param bytes Byte buffer to write
*/
public synchronized void append(String key, byte[] bytes)
{
// Locate the vector and compute expected sizes of vector and overall
// cache.
RawByteAllocator alloc = findRawByteAllocator(key);
long newObjectBytes = alloc.memoryLength + bytes.length;
long newCacheBytes = this.currentMemoryBytes + bytes.length;
// There are two cases to consider: either the new buffer fits in memory
// or it does not.
if (newObjectBytes <= maxObjectBytes && newCacheBytes <= maxCacheBytes)
{
// Case 1: New buffer fits within single vector and cache memory
// limits.
alloc.buffers.add(bytes);
// Update cache sizes.
alloc.memoryLength += bytes.length;
currentMemoryBytes += bytes.length;
}
else
{
// Case 2: New buffer exceeds resource limits, hence must go to
// storage.
// We start by ensuring that the storage file exists
if (alloc.storageLength == 0)
{
// Generate the storage file name.
alloc.cacheFile = new File(cacheDir, key);
}
// Write buffer to storage.
try
{
// Fetch the storage output stream from the LRU cache. If it
// does not exist, create a new one.
FileOutputStream output = outputStreamCache.get(key);
if (output == null)
{
// Create and store the output stream.
output = new FileOutputStream(alloc.cacheFile);
outputStreamCache.put(key, output);
// Seek to the end of the currently stored information so
// the next write picks up the previous position (including
// 0 if this is the first write).
FileChannel channel = output.getChannel();
channel.position(alloc.storageLength);
}
output.write(bytes);
}
catch (IOException e)
{
throw new RuntimeException(String.format(
"Unable to append bytes to storage: key=%s file=%s buffer length=%d",
key, alloc.cacheFile.getAbsolutePath(), bytes.length));
}
// Adjust object length and storage cache size.
alloc.storageLength += bytes.length;
currentStorageBytes += bytes.length;
}
}
/**
* Returns an input stream to read the vector starting from the first byte.
*
* @param key Object reference
* @return An InputStream to read the vector
*/
public InputStream allocateStream(String key)
{
RawByteAllocator alloc = findRawByteAllocator(key);
InputStream input = new RawByteInputStream(alloc);
return input;
}
/**
* Current size of the vector.
*
* @param key Object reference
* @return Size in bytes
*/
public long size(String key)
{
RawByteAllocator alloc = findRawByteAllocator(key);
return alloc.memoryLength + alloc.storageLength;
}
/**
* Resizes the byte vector to a specified length.
*
* @param key Object reference
* @param length New length of vector
*/
public void resize(String key, long length)
{
RawByteAllocator alloc = findRawByteAllocator(key);
// First step of resizing is to drop storage.
if (alloc.cacheFile != null)
{
long newFileLength = length - alloc.memoryLength;
if (newFileLength > 0)
{
// This truncate operation is entirely in storage and just
// shortens the file.
int excessLength = (int) (alloc.storageLength - newFileLength);
FileOutputStream output = outputStreamCache.get(key);
try
{
if (output == null)
{
// Create and store the output stream.
output = new FileOutputStream(alloc.cacheFile, true);
outputStreamCache.put(key, output);
}
// Since this is an existing file, we need to position
// at the end.
FileChannel channel = output.getChannel();
channel.truncate(newFileLength);
channel.position(newFileLength);
// Flush and then invalidate the LRU cache entry to close
// the file.
output.flush();
outputStreamCache.invalidate(key);
// Adjust length of object as well as cache storage
// bytes.
alloc.storageLength = newFileLength;
currentStorageBytes -= excessLength;
}
catch (IOException e)
{
throw new RuntimeException(String.format(
"Unable to truncate vector in storage: key=%s file=%s length=%d truncated length=%d",
key, alloc.cacheFile.getAbsolutePath(),
alloc.memoryLength + alloc.storageLength, length));
}
// Operation affects only storage, so we can return now.
return;
}
else
{
// Storage file is deleted.
outputStreamCache.invalidate(key);
alloc.cacheFile.delete();
alloc.cacheFile = null;
currentStorageBytes -= alloc.storageLength;
alloc.storageLength = 0;
}
}
// Now we have to see if any buffers should be truncated.
int lastBuffer = -1;
int totalBufferBytes = 0;
for (int i = 0; i < alloc.buffers.size(); i++)
{
byte[] buffer = alloc.buffers.get(i);
totalBufferBytes += buffer.length;
if (totalBufferBytes == length)
{
// We have to truncate any succeeding buffers.
lastBuffer = i;
break;
}
else if (totalBufferBytes > length)
{
// We have to truncate any succeeding buffers *and* part of
// this one.
lastBuffer = i;
int excessLength = totalBufferBytes - (int) length;
int shortenedBufferLength = buffer.length - excessLength;
byte[] shortenedBuffer = Arrays.copyOf(buffer,
shortenedBufferLength);
alloc.buffers.set(i, shortenedBuffer);
// Subtracted dropped bytes from cache and vector size.
currentMemoryBytes -= excessLength;
alloc.memoryLength -= excessLength;
break;
}
}
// Remove excess buffers, if any. Subtract the size of each excess
// buffer from the vector and cache length.
if (lastBuffer > -1)
{
for (int i = alloc.buffers.size() - 1; i > lastBuffer; i--)
{
byte[] buffer = alloc.buffers.remove(i);
currentMemoryBytes -= buffer.length;
alloc.memoryLength -= buffer.length;
}
}
}
/**
* Finds an existing RawByteAllocator or throws an exception.
*/
private RawByteAllocator findRawByteAllocator(String key)
{
RawByteAllocator alloc = rawByteAllocatorCache.get(key);
if (alloc == null)
{
throw new RuntimeException("Non-existence vector: key=" + key);
}
return alloc;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
StringBuffer sb = new StringBuffer();
sb.append(this.getClass().getSimpleName()).append(":");
sb.append(" elements=" + this.rawByteAllocatorCache.size());
sb.append(" memoryBytes=" + this.currentMemoryBytes);
sb.append(" storageBytes=" + this.currentStorageBytes);
return sb.toString();
}
}