blob: 7ab220e6caf31c7b68d27eb0b5aa20b6dcfd4167 [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: TransactionManager.java,v 1.7 2005/06/25 23:12:32 doomdark Exp $
*/
package jdbm.recman;
import java.io.*;
import java.util.*;
/**
* This class manages the transaction log that belongs to every
* {@link RecordFile}. The transaction log is either clean, or
* in progress. In the latter case, the transaction manager
* takes care of a roll forward.
*<p>
* Implementation note: this is a proof-of-concept implementation
* which hasn't been optimized for speed. For instance, all sorts
* of streams are created for every transaction.
*/
// TODO: Handle the case where we are recovering lg9 and lg0, were we
// should start with lg9 instead of lg0!
public final class TransactionManager {
private RecordFile owner;
// streams for transaction log.
private FileOutputStream fos;
private ObjectOutputStream oos;
/**
* By default, we keep 10 transactions in the log file before
* synchronizing it with the main database file.
*/
static final int DEFAULT_TXNS_IN_LOG = 10;
/**
* Maximum number of transactions before the log file is
* synchronized with the main database file.
*/
private int _maxTxns = DEFAULT_TXNS_IN_LOG;
/**
* In-core copy of transactions. We could read everything back from
* the log file, but the RecordFile needs to keep the dirty blocks in
* core anyway, so we might as well point to them and spare us a lot
* of hassle.
*/
private ArrayList[] txns = new ArrayList[DEFAULT_TXNS_IN_LOG];
private int curTxn = -1;
/** Extension of a log file. */
static final String extension = ".lg";
/**
* Instantiates a transaction manager instance. If recovery
* needs to be performed, it is done.
*
* @param owner the RecordFile instance that owns this transaction mgr.
*/
TransactionManager(RecordFile owner) throws IOException {
this.owner = owner;
recover();
open();
}
/**
* Synchronize log file data with the main database file.
* <p>
* After this call, the main database file is guaranteed to be
* consistent and guaranteed to be the only file needed for
* backup purposes.
*/
public void synchronizeLog()
throws IOException
{
synchronizeLogFromMemory();
}
/**
* Set the maximum number of transactions to record in
* the log (and keep in memory) before the log is
* synchronized with the main database file.
* <p>
* This method must be called while there are no
* pending transactions in the log.
*/
public void setMaximumTransactionsInLog( int maxTxns )
throws IOException
{
if ( maxTxns <= 0 ) {
throw new IllegalArgumentException(
"Argument 'maxTxns' must be greater than 0." );
}
if ( curTxn != -1 ) {
throw new IllegalStateException(
"Cannot change setting while transactions are pending in the log" );
}
_maxTxns = maxTxns;
txns = new ArrayList[ maxTxns ];
}
/** Builds logfile name */
private String makeLogName() {
return owner.getFileName() + extension;
}
/** Synchs in-core transactions to data file and opens a fresh log */
private void synchronizeLogFromMemory() throws IOException {
close();
TreeSet blockList = new TreeSet( new BlockIoComparator() );
int numBlocks = 0;
int writtenBlocks = 0;
for (int i = 0; i < _maxTxns; i++) {
if (txns[i] == null)
continue;
// Add each block to the blockList, replacing the old copy of this
// block if necessary, thus avoiding writing the same block twice
for (Iterator k = txns[i].iterator(); k.hasNext(); ) {
BlockIo block = (BlockIo)k.next();
if ( blockList.contains( block ) ) {
block.decrementTransactionCount();
}
else {
writtenBlocks++;
boolean result = blockList.add( block );
}
numBlocks++;
}
txns[i] = null;
}
// Write the blocks from the blockList to disk
synchronizeBlocks(blockList.iterator(), true);
owner.sync();
open();
}
/** Opens the log file */
private void open() throws IOException {
fos = new FileOutputStream(makeLogName());
oos = new ObjectOutputStream(fos);
oos.writeShort(Magic.LOGFILE_HEADER);
oos.flush();
curTxn = -1;
}
/** Startup recovery on all files */
private void recover() throws IOException {
String logName = makeLogName();
File logFile = new File(logName);
if (!logFile.exists())
return;
if (logFile.length() == 0) {
logFile.delete();
return;
}
FileInputStream fis = new FileInputStream(logFile);
ObjectInputStream ois = new ObjectInputStream(fis);
try {
if (ois.readShort() != Magic.LOGFILE_HEADER)
throw new Error("Bad magic on log file");
} catch (IOException e) {
// corrupted/empty logfile
logFile.delete();
return;
}
while (true) {
ArrayList blocks = null;
try {
blocks = (ArrayList) ois.readObject();
} catch (ClassNotFoundException e) {
throw new Error("Unexcepted exception: " + e);
} catch (IOException e) {
// corrupted logfile, ignore rest of transactions
break;
}
synchronizeBlocks(blocks.iterator(), false);
// ObjectInputStream must match exactly each
// ObjectOutputStream created during writes
try {
ois = new ObjectInputStream(fis);
} catch (IOException e) {
// corrupted logfile, ignore rest of transactions
break;
}
}
owner.sync();
logFile.delete();
}
/** Synchronizes the indicated blocks with the owner. */
private void synchronizeBlocks(Iterator blockIterator, boolean fromCore)
throws IOException {
// write block vector elements to the data file.
while ( blockIterator.hasNext() ) {
BlockIo cur = (BlockIo)blockIterator.next();
owner.synch(cur);
if (fromCore) {
cur.decrementTransactionCount();
if (!cur.isInTransaction()) {
owner.releaseFromTransaction(cur, true);
}
}
}
}
/** Set clean flag on the blocks. */
private void setClean(ArrayList blocks)
throws IOException {
for (Iterator k = blocks.iterator(); k.hasNext(); ) {
BlockIo cur = (BlockIo) k.next();
cur.setClean();
}
}
/** Discards the indicated blocks and notify the owner. */
private void discardBlocks(ArrayList blocks)
throws IOException {
for (Iterator k = blocks.iterator(); k.hasNext(); ) {
BlockIo cur = (BlockIo) k.next();
cur.decrementTransactionCount();
if (!cur.isInTransaction()) {
owner.releaseFromTransaction(cur, false);
}
}
}
/**
* Starts a transaction. This can block if all slots have been filled
* with full transactions, waiting for the synchronization thread to
* clean out slots.
*/
void start() throws IOException {
curTxn++;
if (curTxn == _maxTxns) {
synchronizeLogFromMemory();
curTxn = 0;
}
txns[curTxn] = new ArrayList();
}
/**
* Indicates the block is part of the transaction.
*/
void add(BlockIo block) throws IOException {
block.incrementTransactionCount();
txns[curTxn].add(block);
}
/**
* Commits the transaction to the log file.
*/
void commit() throws IOException {
oos.writeObject(txns[curTxn]);
sync();
// set clean flag to indicate blocks have been written to log
setClean(txns[curTxn]);
// open a new ObjectOutputStream in order to store
// newer states of BlockIo
oos = new ObjectOutputStream(fos);
}
/** Flushes and syncs */
private void sync() throws IOException {
oos.flush();
fos.flush();
fos.getFD().sync();
}
/**
* Shutdowns the transaction manager. Resynchronizes outstanding
* logs.
*/
void shutdown() throws IOException {
synchronizeLogFromMemory();
close();
}
/**
* Closes open files.
*/
private void close() throws IOException {
sync();
oos.close();
fos.close();
oos = null;
fos = null;
}
/**
* Force closing the file without synchronizing pending transaction data.
* Used for testing purposes only.
*/
void forceClose() throws IOException {
oos.close();
fos.close();
oos = null;
fos = null;
}
/**
* Use the disk-based transaction log to synchronize the data file.
* Outstanding memory logs are discarded because they are believed
* to be inconsistent.
*/
void synchronizeLogFromDisk() throws IOException {
close();
for ( int i=0; i < _maxTxns; i++ ) {
if (txns[i] == null)
continue;
discardBlocks(txns[i]);
txns[i] = null;
}
recover();
open();
}
/** INNER CLASS.
* Comparator class for use by the tree set used to store the blocks
* to write for this transaction. The BlockIo objects are ordered by
* their blockIds.
*/
public static class BlockIoComparator
implements Comparator
{
public int compare( Object o1, Object o2 ) {
BlockIo block1 = (BlockIo)o1;
BlockIo block2 = (BlockIo)o2;
int result = 0;
if ( block1.getBlockId() == block2.getBlockId() ) {
result = 0;
}
else if ( block1.getBlockId() < block2.getBlockId() ) {
result = -1;
}
else {
result = 1;
}
return result;
}
public boolean equals(Object obj) {
return super.equals(obj);
}
} // class BlockIOComparator
}