/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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.
******************************************************************************/
package de.mxro.thrd.jdbm2V22.recman;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
import de.mxro.thrd.jdbm2V22.helper.Serialization;
/**
* 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 DataOutputStream 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 = 1;
/**
* 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<BlockIo>[] txns = new ArrayList[DEFAULT_TXNS_IN_LOG];
private int curTxn = -1;
/** Extension of a log file. */
static final String extension = ".t";
/**
* 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 */
protected void synchronizeLogFromMemory() throws IOException {
close();
TreeSet<BlockIo> blockList = new TreeSet<BlockIo>( 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<BlockIo> k = txns[i].iterator(); k.hasNext(); ) {
BlockIo block = 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, true);
owner.sync();
open();
}
/** Opens the log file */
protected void open() throws IOException {
fos = new FileOutputStream(makeLogName());
oos = new DataOutputStream(new BufferedOutputStream(fos));
oos.writeShort(Magic.LOGFILE_HEADER);
oos.flush();
curTxn = -1;
}
/** Startup recovery on all files */
protected 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);
DataInputStream ois = new DataInputStream(new BufferedInputStream(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<BlockIo> blocks = null;
try {
blocks = (ArrayList<BlockIo>) Serialization.readObject(ois);
} catch (ClassNotFoundException e) {
throw new Error("Unexcepted exception: " + e);
} catch (IOException e) {
// corrupted logfile, ignore rest of transactions
break;
}
synchronizeBlocks(blocks, false);
// ObjectInputStream must match exactly each
// ObjectOutputStream created during writes
// try {
ois = new DataInputStream(fis);
// } catch (IOException e) {
// // corrupted logfile, ignore rest of transactions
// break;
// }
}
owner.sync();
logFile.delete();
}
/** Synchronizes the indicated blocks with the owner. */
protected void synchronizeBlocks(Iterable<BlockIo> blocks, boolean fromCore)
throws IOException {
// write block vector elements to the data file.
for(BlockIo cur:blocks){
owner.synch(cur);
if (fromCore) {
cur.decrementTransactionCount();
if (!cur.isInTransaction()) {
owner.releaseFromTransaction(cur, true);
}
}
}
}
/** Set clean flag on the blocks. */
protected void setClean(ArrayList<BlockIo> blocks)
throws IOException {
for (BlockIo cur : blocks) {
cur.setClean();
}
}
/** Discards the indicated blocks and notify the owner. */
protected void discardBlocks(ArrayList<BlockIo> blocks)
throws IOException {
for (BlockIo cur:blocks) {
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 {
Serialization.writeObject(oos, 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 DataOutputStream(new BufferedOutputStream(fos));
}
/** Flushes and syncs */
protected 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.
*/
protected 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<BlockIo>
{
public int compare( BlockIo block1, BlockIo block2 ) {
if ( block1.getBlockId() == block2.getBlockId() ) {
return 0;
}
else if ( block1.getBlockId() < block2.getBlockId() ) {
return -1;
}
else {
return 1;
}
}
public boolean equals(Object obj) {
return super.equals(obj);
}
} // class BlockIOComparator
}