package com.yahoo.glimmer.util;
/*
* Copyright (c) 2012 Yahoo! 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.
* See accompanying LICENSE file.
*/
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
/**
* A LRU cache of fixed size byte arrays(blocks). Intended use is for caching
* uncompressed BZip2 blocks.
*
* Should be thread safe if the given BlockReader is thread safe.
*
* @author tep
*/
public class BlockCache {
public interface BlockReader {
/**
* Implementations should set bytes in the buffer starting at offset 0.
*
* @param blockIndex
* @param buffer
* @throws IOException
* @throws IndexOutOfBoundsException
* if blockIndex is..
* @return Number of bytes written to buffer
*/
public int readBlock(long blockIndex, byte[] buffer) throws IOException, IndexOutOfBoundsException;
}
public static class Block {
private long index = -1;
private final byte[] bytes;
private int length;
public Block(int blockSize) {
bytes = new byte[blockSize];
}
public boolean hasData() {
return length != 0;
}
public void clear() {
index = -1;
length = 0;
}
public long getIndex() {
return index;
}
public int getLength() {
return length;
}
public byte[] getBytes() {
return bytes;
}
@Override
public String toString() {
return "Index:" + index + " Length:" + length + " Starts:" + new String(bytes, 0, length < 64 ? length : 64);
}
}
private final LoadingCache<Long, Block> blocksCache;
private final ConcurrentLinkedQueue<Block> freeBlocks;
private final long lastBlockIndex;
private final int inputStreamBufferSize;
public BlockCache(final BlockReader blockReader, final long lastBlockIndex, final int blockSize, final int cacheSizeInBlocks) {
// Fixed size LRU with block recycling.
blocksCache = CacheBuilder.newBuilder().maximumSize(cacheSizeInBlocks).removalListener(new RemovalListener<Long, Block>() {
@Override
public void onRemoval(RemovalNotification<Long, Block> notification) {
Block block = notification.getValue();
block.index = -1;
block.length = 0;
freeBlocks.add(block);
}
}).build(new CacheLoader<Long, Block>() {
@Override
public Block load(Long blockIndex) throws Exception {
Block block = freeBlocks.poll();
if (block == null) {
block = new Block(blockSize);
}
int bytesRead = -1;
try {
bytesRead = blockReader.readBlock(blockIndex, block.bytes);
} catch (Exception e) {
freeBlocks.add(block);
throw e;
}
if (bytesRead < 0 || bytesRead > block.bytes.length) {
freeBlocks.add(block);
throw new RuntimeException("bytesRead(" + bytesRead + ") is not in range 0 to " + block.bytes.length);
}
block.index = blockIndex;
block.length = bytesRead;
return block;
}
});
freeBlocks = new ConcurrentLinkedQueue<Block>();
this.lastBlockIndex = lastBlockIndex;
int inputStreamBufferSize = blockSize;
while (inputStreamBufferSize > 8196) {
inputStreamBufferSize >>= 1;
}
this.inputStreamBufferSize = inputStreamBufferSize;
}
/**
* @param blockIndex
* @return The block or null if the blockIndex is out of bounds.
* @throws IOException
*/
public Block getBlock(final long blockIndex) throws IOException, IndexOutOfBoundsException {
if (blockIndex < 0 || blockIndex > lastBlockIndex) {
return null;
}
try {
return blocksCache.get(blockIndex);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else if (cause instanceof IndexOutOfBoundsException) {
throw (IndexOutOfBoundsException) cause;
}
throw new RuntimeException(e);
}
}
public InputStream getInputStream(final long blockIndex, final int startByteIndexInFirstBlock) throws IOException {
return new BlockInputStream(blockIndex, startByteIndexInFirstBlock, inputStreamBufferSize);
}
private class BlockInputStream extends InputStream {
private final byte[] buffer;
private long currentBlockIndex;
private int currentBlockByteIndex;
private int bufferReadIndex;
private int bufferByteCount;
public BlockInputStream(long startBlockIndex, int startBlockByteIndex, int bufferSize) throws IOException {
buffer = new byte[bufferSize];
this.currentBlockIndex = startBlockIndex;
this.currentBlockByteIndex = startBlockByteIndex;
if (startBlockByteIndex < 0) {
throw new IndexOutOfBoundsException("given startBlockIndex(" + startBlockByteIndex + ") < 0");
}
fillBuffer();
}
private boolean fillBuffer() throws IOException {
bufferReadIndex = 0;
bufferByteCount = 0;
if (currentBlockIndex >= 0) {
while (bufferByteCount < buffer.length) {
Block currentBlock = getBlock(currentBlockIndex);
if (currentBlock != null) {
int currentBlockBytesRemaining = currentBlock.length - currentBlockByteIndex;
int bytesNeededToFillBuffer = buffer.length - bufferByteCount;
if (currentBlockBytesRemaining > bytesNeededToFillBuffer) {
System.arraycopy(currentBlock.bytes, currentBlockByteIndex, buffer, bufferByteCount, bytesNeededToFillBuffer);
currentBlockByteIndex += bytesNeededToFillBuffer;
bufferByteCount = buffer.length; // +=
// bytesNeededToFillBuffer;
} else {
System.arraycopy(currentBlock.bytes, currentBlockByteIndex, buffer, bufferByteCount, currentBlockBytesRemaining);
currentBlockByteIndex = 0;
currentBlockIndex++;
bufferByteCount += currentBlockBytesRemaining;
}
} else {
// No more blocks.
currentBlockIndex = -1;
return bufferReadIndex < bufferByteCount;
}
}
return true;
}
return false;
}
@Override
public int read() throws IOException {
if (bufferReadIndex < bufferByteCount || fillBuffer()) {
return buffer[bufferReadIndex++] & 0xFF;
}
return -1;
}
}
}