blob: ff64b96ded07e8df7d970db6807df7ae889be344 [file] [log] [blame]
/**
* JDBM LICENSE v1.00
*
* Redistribution and use of this software and associated documentation
* ("Software"), with or without modification, are permitted provided
* that the following conditions are met:
*
* 1. Redistributions of source code must retain copyright
* statements and notices. Redistributions must also contain a
* copy of this document.
*
* 2. Redistributions in binary form must reproduce the
* above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. The name "JDBM" must not be used to endorse or promote
* products derived from this Software without prior written
* permission of Cees de Groot. For written permission,
* please contact cg@cdegroot.com.
*
* 4. Products derived from this Software may not be called "JDBM"
* nor may "JDBM" appear in their names without prior written
* permission of Cees de Groot.
*
* 5. Due credit should be given to the JDBM Project
* (http://jdbm.sourceforge.net/).
*
* THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
* NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Copyright 2000 (C) Cees de Groot. All Rights Reserved.
* Contributions are Copyright (C) 2000 by their associated contributors.
*
* $Id: RecordFile.java,v 1.6 2005/06/25 23:12:32 doomdark Exp $
*/
package jdbm.recman;
import java.io.*;
import java.util.*;
import org.apache.directory.server.i18n.I18n;
/**
* This class represents a random access file as a set of fixed size
* records. Each record has a physical record number, and records are
* cached in order to improve access.
*<p>
* The set of dirty records on the in-use list constitutes a transaction.
* Later on, we will send these records to some recovery thingy.
*/
public final class RecordFile
{
private TransactionManager transactionManager;
// state transitions: free -> inUse -> dirty -> inTxn -> free
// free is a cache, thus a FIFO. The rest are hashes.
private final LinkedList<BlockIo> free = new LinkedList<BlockIo>();
private final HashMap<Long,BlockIo> inUse = new HashMap<Long,BlockIo>();
private final HashMap<Long,BlockIo> dirty = new HashMap<Long,BlockIo>();
private final HashMap<Long,BlockIo> inTxn = new HashMap<Long,BlockIo>();
// transactions disabled?
private boolean transactionsDisabled = false;
/** The length of a single block. */
public final static int BLOCK_SIZE = 8192;//4096;
/** The extension of a record file */
final static String extension = ".db";
/** A block of clean data to wipe clean pages. */
final static byte[] cleanData = new byte[BLOCK_SIZE];
private RandomAccessFile file;
private final String fileName;
/**
* Creates a new object on the indicated filename. The file is
* opened in read/write mode.
*
* @param fileName the name of the file to open or create, without
* an extension.
* @throws IOException whenever the creation of the underlying
* RandomAccessFile throws it.
*/
RecordFile ( String fileName ) throws IOException
{
this.fileName = fileName;
file = new RandomAccessFile(fileName + extension, "rw");
}
TransactionManager getTxnMgr() throws IOException
{
if ( transactionsDisabled )
{
throw new IllegalStateException( "Transactions are disabled." );
}
if ( transactionManager == null )
{
transactionManager = new TransactionManager( this );
}
return transactionManager;
}
/**
* Returns the file name.
*/
String getFileName()
{
return fileName;
}
/**
* Disables transactions: doesn't sync and doesn't use the
* transaction manager.
*/
void disableTransactions()
{
transactionsDisabled = true;
}
/**
* Gets a block from the file. The returned byte array is the in-memory
* copy of the record, and thus can be written (and subsequently released
* with a dirty flag in order to write the block back).
*
* @param blockid The record number to retrieve.
*/
BlockIo get( long blockid ) throws IOException
{
// try in transaction list, dirty list, free list
BlockIo node = inTxn.get( blockid );
if ( node != null )
{
inTxn.remove( blockid );
inUse.put( blockid, node );
return node;
}
node = dirty.get( blockid );
if ( node != null )
{
dirty.remove( blockid );
inUse.put( blockid, node );
return node;
}
for ( Iterator<BlockIo> i = free.iterator(); i.hasNext(); )
{
BlockIo cur = i.next();
if ( cur.getBlockId() == blockid )
{
node = cur;
i.remove();
inUse.put( blockid, node );
return node;
}
}
// sanity check: can't be on in use list
if ( inUse.get( blockid ) != null )
{
throw new Error( I18n.err( I18n.ERR_554, blockid ) );
}
// get a new node and read it from the file
node = getNewNode( blockid );
long offset = blockid * BLOCK_SIZE;
if ( file.length() > 0 && offset <= file.length() )
{
read( file, offset, node.getData(), BLOCK_SIZE );
}
else
{
System.arraycopy( cleanData, 0, node.getData(), 0, BLOCK_SIZE );
}
inUse.put( blockid, node );
node.setClean();
return node;
}
/**
* Releases a block.
*
* @param blockid The record number to release.
* @param isDirty If true, the block was modified since the get().
*/
void release( long blockid, boolean isDirty ) throws IOException
{
BlockIo node = inUse.get( blockid );
if ( node == null )
{
throw new IOException( I18n.err( I18n.ERR_555, blockid ) );
}
if ( ! node.isDirty() && isDirty )
{
node.setDirty();
}
release( node );
}
/**
* Releases a block.
*
* @param block The block to release.
*/
void release( BlockIo block )
{
inUse.remove( block.getBlockId() );
if ( block.isDirty() )
{
// System.out.println( "Dirty: " + key + block );
dirty.put( block.getBlockId(), block );
}
else
{
if ( ! transactionsDisabled && block.isInTransaction() )
{
inTxn.put( block.getBlockId(), block );
}
else
{
free.add( block );
}
}
}
/**
* Discards a block (will not write the block even if it's dirty)
*
* @param block The block to discard.
*/
void discard( BlockIo block )
{
inUse.remove( block.getBlockId() );
// note: block not added to free list on purpose, because
// it's considered invalid
}
/**
* Commits the current transaction by flushing all dirty buffers to disk.
*/
void commit() throws IOException
{
// debugging...
if ( ! inUse.isEmpty() && inUse.size() > 1 )
{
showList( inUse.values().iterator() );
throw new Error( I18n.err( I18n.ERR_556, inUse.size() ) );
}
// System.out.println("committing...");
if ( dirty.size() == 0 )
{
// if no dirty blocks, skip commit process
return;
}
if ( ! transactionsDisabled )
{
getTxnMgr().start();
}
for ( Iterator<BlockIo> i = dirty.values().iterator(); i.hasNext(); )
{
BlockIo node = ( BlockIo ) i.next();
i.remove();
// System.out.println("node " + node + " map size now " + dirty.size());
if ( transactionsDisabled )
{
long offset = node.getBlockId() * BLOCK_SIZE;
file.seek( offset );
file.write( node.getData() );
node.setClean();
free.add( node );
}
else
{
getTxnMgr().add( node );
inTxn.put( node.getBlockId(), node );
}
}
if ( ! transactionsDisabled )
{
getTxnMgr().commit();
}
}
/**
* Rollback the current transaction by discarding all dirty buffers
*/
void rollback() throws IOException
{
// debugging...
if ( ! inUse.isEmpty() )
{
showList( inUse.values().iterator() );
throw new Error( I18n.err( I18n.ERR_557, inUse.size() ) );
}
// System.out.println("rollback...");
dirty.clear();
if ( ! transactionsDisabled )
{
getTxnMgr().synchronizeLogFromDisk();
}
if ( ! inTxn.isEmpty() )
{
showList( inTxn.values().iterator() );
throw new Error( I18n.err( I18n.ERR_558, inTxn.size() ) );
}
}
/**
* Commits and closes file.
*/
void close() throws IOException
{
if ( ! dirty.isEmpty() )
{
commit();
}
if( ! transactionsDisabled )
{
getTxnMgr().shutdown();
}
if ( ! inTxn.isEmpty() )
{
showList( inTxn.values().iterator() );
throw new Error( I18n.err( I18n.ERR_559 ) );
}
// these actually ain't that bad in a production release
if ( ! dirty.isEmpty() )
{
System.out.println( "ERROR: dirty blocks at close time" );
showList( dirty.values().iterator() );
throw new Error( I18n.err( I18n.ERR_560 ) );
}
if ( ! inUse.isEmpty() )
{
System.out.println( "ERROR: inUse blocks at close time" );
showList( inUse.values().iterator() );
throw new Error( I18n.err( I18n.ERR_561 ) );
}
// debugging stuff to keep an eye on the free list
// System.out.println("Free list size:" + free.size());
file.close();
file = null;
}
/**
* Force closing the file and underlying transaction manager.
* Used for testing purposed only.
*/
void forceClose() throws IOException
{
if ( ! transactionsDisabled )
{
getTxnMgr().forceClose();
}
file.close();
}
/**
* Prints contents of a list
*/
private void showList( Iterator<BlockIo> i )
{
int cnt = 0;
while ( i.hasNext() )
{
System.out.println( "elem " + cnt + ": " + i.next() );
cnt++;
}
}
/**
* Returns a new node. The node is retrieved (and removed) from the
* released list or created new.
*/
private BlockIo getNewNode( long blockid ) throws IOException
{
BlockIo retval = null;
if ( ! free.isEmpty() )
{
retval = ( BlockIo ) free.removeFirst();
}
if ( retval == null )
{
retval = new BlockIo( 0, new byte[BLOCK_SIZE] );
}
retval.setBlockId(blockid);
retval.setView( null );
return retval;
}
/**
* Synchronizes a node to disk. This is called by the transaction manager's
* synchronization code.
*/
void synch( BlockIo node ) throws IOException
{
byte[] data = node.getData();
if ( data != null )
{
long offset = node.getBlockId() * BLOCK_SIZE;
file.seek( offset );
file.write( data );
}
}
/**
* Releases a node from the transaction list, if it was sitting there.
*
* @param recycle true if block data can be reused
*/
void releaseFromTransaction( BlockIo node, boolean recycle ) throws IOException
{
if ( ( inTxn.remove( node.getBlockId() ) != null ) && recycle )
{
free.add( node );
}
}
/**
* Synchronizes the file.
*/
void sync() throws IOException
{
file.getFD().sync();
}
/**
* Utility method: Read a block from a RandomAccessFile
*/
private static void read( RandomAccessFile file, long offset, byte[] buffer, int nBytes ) throws IOException
{
file.seek( offset );
int remaining = nBytes;
int pos = 0;
while ( remaining > 0 )
{
int read = file.read( buffer, pos, remaining );
if ( read == -1 )
{
System.arraycopy( cleanData, 0, buffer, pos, remaining );
break;
}
remaining -= read;
pos += read;
}
}
}