package org.apache.commons.jcs.auxiliary.disk.block; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.io.Serializable; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.jcs.engine.behavior.IElementSerializer; import org.apache.commons.jcs.utils.serialization.StandardSerializer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * This class manages reading an writing data to disk. When asked to write a value, it returns a * block array. It can read an object from the block numbers in a byte array. * <p> * @author Aaron Smuts */ public class BlockDisk { /** The logger */ private static final Log log = LogFactory.getLog( BlockDisk.class ); /** The size of the header that indicates the amount of data stored in an occupied block. */ public static final byte HEADER_SIZE_BYTES = 4; // N.B. 4 bytes is the size used for ByteBuffer.putInt(int value) and ByteBuffer.getInt() /** defaults to 4kb */ private static final int DEFAULT_BLOCK_SIZE_BYTES = 4 * 1024; /** Size of the blocks */ private final int blockSizeBytes; /** * the total number of blocks that have been used. If there are no free, we will use this to * calculate the position of the next block. */ private final AtomicInteger numberOfBlocks = new AtomicInteger(0); /** Empty blocks that can be reused. */ private final ConcurrentLinkedQueue<Integer> emptyBlocks = new ConcurrentLinkedQueue<Integer>(); /** The serializer. */ private final IElementSerializer elementSerializer; /** Location of the spot on disk */ private final String filepath; /** File channel for multiple concurrent reads and writes */ private final FileChannel fc; /** How many bytes have we put to disk */ private final AtomicLong putBytes = new AtomicLong(0); /** How many items have we put to disk */ private final AtomicLong putCount = new AtomicLong(0); /** * Constructor for the Disk object * <p> * @param file * @param elementSerializer * @throws IOException */ public BlockDisk( File file, IElementSerializer elementSerializer ) throws IOException { this( file, DEFAULT_BLOCK_SIZE_BYTES, elementSerializer ); } /** * Creates the file and set the block size in bytes. * <p> * @param file * @param blockSizeBytes * @throws IOException */ public BlockDisk( File file, int blockSizeBytes ) throws IOException { this( file, blockSizeBytes, new StandardSerializer() ); } /** * Creates the file and set the block size in bytes. * <p> * @param file * @param blockSizeBytes * @param elementSerializer * @throws IOException */ public BlockDisk( File file, int blockSizeBytes, IElementSerializer elementSerializer ) throws IOException { this.filepath = file.getAbsolutePath(); RandomAccessFile raf = new RandomAccessFile( filepath, "rw" ); this.fc = raf.getChannel(); this.numberOfBlocks.set((int) Math.ceil(1f * this.fc.size() / blockSizeBytes)); if ( log.isInfoEnabled() ) { log.info( "Constructing BlockDisk, blockSizeBytes [" + blockSizeBytes + "]" ); } this.blockSizeBytes = blockSizeBytes; this.elementSerializer = elementSerializer; } /** * Allocate a given number of blocks from the available set * * @param numBlocksNeeded * @return an array of allocated blocks */ private int[] allocateBlocks(int numBlocksNeeded) { assert numBlocksNeeded >= 1; int[] blocks = new int[numBlocksNeeded]; // get them from the empty list or take the next one for (int i = 0; i < numBlocksNeeded; i++) { Integer emptyBlock = emptyBlocks.poll(); if (emptyBlock == null) { emptyBlock = Integer.valueOf(numberOfBlocks.getAndIncrement()); } blocks[i] = emptyBlock.intValue(); } return blocks; } /** * This writes an object to disk and returns the blocks it was stored in. * <p> * The program flow is as follows: * <ol> * <li>Serialize the object.</li> * <li>Determine the number of blocks needed.</li> * <li>Look for free blocks in the emptyBlock list.</li> * <li>If there were not enough in the empty list. Take the nextBlock and increment it.</li> * <li>If the data will not fit in one block, create sub arrays.</li> * <li>Write the subarrays to disk.</li> * <li>If the process fails we should decrement the block count if we took from it.</li> * </ol> * @param object * @return the blocks we used. * @throws IOException */ protected int[] write( Serializable object ) throws IOException { // serialize the object byte[] data = elementSerializer.serialize(object); if ( log.isDebugEnabled() ) { log.debug( "write, total pre-chunking data.length = " + data.length ); } this.putBytes.addAndGet(data.length); this.putCount.incrementAndGet(); // figure out how many blocks we need. int numBlocksNeeded = calculateTheNumberOfBlocksNeeded(data); if ( log.isDebugEnabled() ) { log.debug( "numBlocksNeeded = " + numBlocksNeeded ); } // allocate blocks int[] blocks = allocateBlocks(numBlocksNeeded); int offset = 0; final int maxChunkSize = blockSizeBytes - HEADER_SIZE_BYTES; ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_SIZE_BYTES); for (int i = 0; i < numBlocksNeeded; i++) { headerBuffer.clear(); int length = Math.min(maxChunkSize, data.length - offset); headerBuffer.putInt(length); ByteBuffer dataBuffer = ByteBuffer.wrap(data, offset, length); long position = calculateByteOffsetForBlockAsLong(blocks[i]); // write the header headerBuffer.flip(); int written = fc.write(headerBuffer, position); assert written == HEADER_SIZE_BYTES; //write the data written = fc.write(dataBuffer, position + HEADER_SIZE_BYTES); assert written == length; offset += length; } //fc.force(false); return blocks; } /** * Return the amount to put in each block. Fill them all the way, minus the header. * <p> * @param complete * @param numBlocksNeeded * @return byte[][] */ protected byte[][] getBlockChunks( byte[] complete, int numBlocksNeeded ) { byte[][] chunks = new byte[numBlocksNeeded][]; if ( numBlocksNeeded == 1 ) { chunks[0] = complete; } else { int maxChunkSize = this.blockSizeBytes - HEADER_SIZE_BYTES; int totalBytes = complete.length; int totalUsed = 0; for ( short i = 0; i < numBlocksNeeded; i++ ) { // use the max that can be written to a block or whatever is left in the original // array int chunkSize = Math.min( maxChunkSize, totalBytes - totalUsed ); byte[] chunk = new byte[chunkSize]; // copy from the used position to the chunk size on the complete array to the chunk // array. System.arraycopy( complete, totalUsed, chunk, 0, chunkSize ); chunks[i] = chunk; totalUsed += chunkSize; } } return chunks; } /** * Reads an object that is located in the specified blocks. * <p> * @param blockNumbers * @return Serializable * @throws IOException * @throws ClassNotFoundException */ protected <T extends Serializable> T read( int[] blockNumbers ) throws IOException, ClassNotFoundException { byte[] data = null; if ( blockNumbers.length == 1 ) { data = readBlock( blockNumbers[0] ); } else { ByteArrayOutputStream baos = new ByteArrayOutputStream(getBlockSizeBytes()); // get all the blocks into data for ( short i = 0; i < blockNumbers.length; i++ ) { byte[] chunk = readBlock( blockNumbers[i] ); baos.write(chunk); } data = baos.toByteArray(); baos.close(); } if ( log.isDebugEnabled() ) { log.debug( "read, total post combination data.length = " + data.length ); } return elementSerializer.deSerialize( data, null ); } /** * This reads the occupied data in a block. * <p> * The first four bytes of the record should tell us how long it is. The data is read into a * byte array and then an object is constructed from the byte array. * <p> * @return byte[] * @param block * @throws IOException */ private byte[] readBlock( int block ) throws IOException { int datalen = 0; String message = null; boolean corrupted = false; long fileLength = fc.size(); long position = calculateByteOffsetForBlockAsLong( block ); // if ( position > fileLength ) // { // corrupted = true; // message = "Record " + position + " starts past EOF."; // } // else { ByteBuffer datalength = ByteBuffer.allocate(HEADER_SIZE_BYTES); fc.read(datalength, position); datalength.flip(); datalen = datalength.getInt(); if ( position + datalen > fileLength ) { corrupted = true; message = "Record " + position + " exceeds file length."; } } if ( corrupted ) { log.warn( "\n The file is corrupt: " + "\n " + message ); throw new IOException( "The File Is Corrupt, need to reset" ); } ByteBuffer data = ByteBuffer.allocate(datalen); fc.read(data, position + HEADER_SIZE_BYTES); data.flip(); return data.array(); } /** * Add these blocks to the emptyBlock list. * <p> * @param blocksToFree */ protected void freeBlocks( int[] blocksToFree ) { if ( blocksToFree != null ) { for ( short i = 0; i < blocksToFree.length; i++ ) { emptyBlocks.offer( Integer.valueOf( blocksToFree[i] ) ); } } } /** * Calculates the file offset for a particular block. * <p> * @param block number * @return the byte offset for this block in the file as an int; may overflow * @deprecated (since 2.0) use {@link #calculateByteOffsetForBlockAsLong(int)} instead */ @Deprecated protected int calculateByteOffsetForBlock( int block ) { return block * blockSizeBytes; } /** * Calculates the file offset for a particular block. * <p> * @param block number * @return the byte offset for this block in the file as a long * @since 2.0 */ protected long calculateByteOffsetForBlockAsLong( int block ) { return (long) block * blockSizeBytes; } /** * The number of blocks needed. * <p> * @param data * @return the number of blocks needed to store the byte array */ protected int calculateTheNumberOfBlocksNeeded( byte[] data ) { int dataLength = data.length; int oneBlock = blockSizeBytes - HEADER_SIZE_BYTES; // takes care of 0 = HEADER_SIZE_BYTES + blockSizeBytes if ( dataLength <= oneBlock ) { return 1; } int dividend = dataLength / oneBlock; if ( dataLength % oneBlock != 0 ) { dividend++; } return dividend; } /** * Returns the file length. * <p> * @return the size of the file. * @throws IOException */ protected long length() throws IOException { return fc.size(); } /** * Closes the file. * <p> * @throws IOException */ protected void close() throws IOException { fc.close(); } /** * Resets the file. * <p> * @throws IOException */ protected synchronized void reset() throws IOException { this.numberOfBlocks.set(0); this.emptyBlocks.clear(); fc.truncate(0); fc.force(true); } /** * @return Returns the numberOfBlocks. */ protected int getNumberOfBlocks() { return numberOfBlocks.get(); } /** * @return Returns the blockSizeBytes. */ protected int getBlockSizeBytes() { return blockSizeBytes; } /** * @return Returns the average size of the an element inserted. */ protected long getAveragePutSizeBytes() { long count = this.putCount.get(); if (count == 0 ) { return 0; } return this.putBytes.get() / count; } /** * @return Returns the number of empty blocks. */ protected int getEmptyBlocks() { return this.emptyBlocks.size(); } /** * For debugging only. * <p> * @return String with details. */ @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append( "\nBlock Disk " ); buf.append( "\n Filepath [" + filepath + "]" ); buf.append( "\n NumberOfBlocks [" + this.numberOfBlocks.get() + "]" ); buf.append( "\n BlockSizeBytes [" + this.blockSizeBytes + "]" ); buf.append( "\n Put Bytes [" + this.putBytes + "]" ); buf.append( "\n Put Count [" + this.putCount + "]" ); buf.append( "\n Average Size [" + getAveragePutSizeBytes() + "]" ); buf.append( "\n Empty Blocks [" + this.getEmptyBlocks() + "]" ); try { buf.append( "\n Length [" + length() + "]" ); } catch ( IOException e ) { // swallow } return buf.toString(); } /** * This is used for debugging. * <p> * @return the file path. */ protected String getFilePath() { return filepath; } }