/*
* Bitronix Transaction Manager
*
* Copyright (c) 2011, Juergen Kellerer.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package bitronix.tm.journal.nio;
import bitronix.tm.journal.nio.util.CompositeIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import static bitronix.tm.journal.nio.NioJournalFileRecord.readRecords;
/**
* Low level file handling implementation.
*
* @author juergen kellerer, 2011-04-30
*/
class NioJournalFile implements NioJournalConstants {
private static final Logger log = LoggerFactory.getLogger(NioJournalFile.class);
static byte[] nameBytes(String nameValue) {
ByteBuffer encoded = NAME_CHARSET.encode(nameValue);
if (encoded.remaining() == encoded.capacity() && encoded.hasArray())
return encoded.array();
else {
byte[] bytes = new byte[encoded.remaining()];
encoded.get(bytes);
return bytes;
}
}
private static final String NL = "\r\n";
private static final String JOURNAL_HEADER_MAGIC_VALUE = "BTM-NTJ-[Version 1.0]";
private static final byte[] JOURNAL_HEADER_PREFIX = nameBytes(JOURNAL_HEADER_MAGIC_VALUE + NL +
NL +
"--------- Bitronix Transaction Manager :: Nio Transaction Journal File ---------" + NL +
NL +
" This is a delimiter based rolling binary file format belonging to BTM." + NL +
" The purpose of this file is to persist JTA transaction states for " + NL +
" providing crash recovery on broken commits and rollbacks." + NL +
NL +
"--------------------------------------------------------------------------------" + NL +
NL);
private static final byte[] JOURNAL_HEADER_SUFFIX = nameBytes(NL + NL);
static final int FIXED_HEADER_SIZE = 1024;
private volatile UUID previousDelimiter = UUID.randomUUID();
private volatile UUID delimiter = UUID.randomUUID();
private ByteBuffer writeBuffer;
private final File file;
private final RandomAccessFile randomAccessFile;
private FileLock lock;
private FileChannel fileChannel;
private final AtomicLong journalSize = new AtomicLong();
private final AtomicLong lastModified = new AtomicLong(), lastForced = new AtomicLong();
/**
* Constructs a new journal file instance using the given storage path and initial file size.
* <p/>
* If the given file does not exist or is empty, a new journal is created with the given initial size.
* When the file does exist, new record are appended at the end of the journal and the size is increased
* if initial size is greater than the current journal size.
*
* @param file the journal file to write to.
* @param initialJournalSize the initial size to pre-allocated for the journal.
* @throws IOException if opening the file fails.
*/
public NioJournalFile(File file, long initialJournalSize) throws IOException {
this.file = file;
boolean success = false;
randomAccessFile = new RandomAccessFile(file, "rw");
try {
fileChannel = randomAccessFile.getChannel();
lock = fileChannel.tryLock();
if (lock == null)
throw new IOException("Failed to acquire an exclusive lock on file " + file + ". It seems the journal is opened in another process.");
final boolean createHeader = randomAccessFile.length() == 0;
if (!createHeader) {
try {
readJournalHeader();
} catch (IOException e) {
log.error("Failed reading journal header, refusing to open the file " + file + ".", e);
throw e;
}
}
// We can increase but not shrink the journal.
this.journalSize.set(Math.max(initialJournalSize, randomAccessFile.length()));
growJournal(this.journalSize.get());
if (createHeader) {
writeJournalHeader();
log.info("Created a new transaction journal in file " + file + ", insert position is at offset " + fileChannel.position());
} else {
if (log.isDebugEnabled()) { log.debug("Found existing transaction journal in file " + file + " looking after the insert position."); }
NioJournalFileIterable it = (NioJournalFileIterable) readRecords(delimiter, fileChannel, false);
long position = it.findPositionAfterLastRecord();
fileChannel.position(Math.max(FIXED_HEADER_SIZE, position));
long insertPosition = fileChannel.position();
log.info("Opened existing transaction journal in file " + file + ", insert position is at offset " + insertPosition + ".");
if (insertPosition == FIXED_HEADER_SIZE)
log.warn("The journal file " + file + " appears to be empty though it was not just created.");
}
success = true;
} finally {
if (!success)
close();
}
}
public File getFile() {
return file;
}
public synchronized long getSize() {
return journalSize.get();
}
public long getPosition() throws IOException {
return fileChannel.position();
}
/**
* Closes the journal.
*
* @throws IOException in case of the operation failed.
*/
public synchronized void close() throws IOException {
try {
if (fileChannel != null) {
force();
try {
if (lock != null)
lock.release();
} finally {
fileChannel.close();
}
}
} finally {
fileChannel = null;
lock = null;
}
}
/**
* Grows the journal to the specified size, does nothing if newSize is smaller than the current journal size.
*
* @param newSize the new size to grow the journal to.
* @throws IOException in case of there's no space available or the underlying device is broken.
*/
public synchronized void growJournal(long newSize) throws IOException {
if (newSize >= journalSize.get()) {
randomAccessFile.setLength(newSize);
journalSize.set(newSize);
}
}
/**
* Returns an iterable over all records that are contained in the record.
*
* @param includeInvalid specifies whether records that fail the CRC32 checks should be returned as well.
* @return an iterable over all records that are contained in the record.
* @throws IOException in case of the file cannot be accessed.
*/
public synchronized Iterable<NioJournalFileRecord> readAll(boolean includeInvalid) throws IOException {
final Iterable<NioJournalFileRecord> first = readRecords(previousDelimiter, fileChannel, includeInvalid);
final Iterable<NioJournalFileRecord> second = readRecords(delimiter, fileChannel, includeInvalid);
return new Iterable<NioJournalFileRecord>() {
public Iterator<NioJournalFileRecord> iterator() {
@SuppressWarnings("unchecked")
List<Iterable<NioJournalFileRecord>> iterables = Arrays.asList(first, second);
return new CompositeIterator<NioJournalFileRecord>(iterables);
}
};
}
private void assertHeaderPartEquals(ByteBuffer buffer, byte[] value) throws IOException {
byte[] prefix = new byte[value.length];
buffer.get(prefix);
if (!Arrays.equals(prefix, value)) {
// If we'd had multiple version, legacy handling would go in here.
throw new IOException("Failed opening journal file '" + file + "', expected a file header of <" +
NioJournalFileRecord.bufferToString(ByteBuffer.wrap(value)) + "> but was <" +
NioJournalFileRecord.bufferToString(ByteBuffer.wrap(prefix)) + ">");
}
}
private void readJournalHeader() throws IOException {
if (fileChannel.size() == 0)
return; // new file.
ByteBuffer buffer = getWriteBuffer(FIXED_HEADER_SIZE);
fileChannel.read(buffer, 0);
buffer.flip();
try {
assertHeaderPartEquals(buffer, JOURNAL_HEADER_PREFIX);
previousDelimiter = NioJournalFileRecord.readUUID(buffer);
delimiter = NioJournalFileRecord.readUUID(buffer);
assertHeaderPartEquals(buffer, JOURNAL_HEADER_SUFFIX);
fileChannel.position(FIXED_HEADER_SIZE);
} catch (IOException e) {
previousDelimiter = UUID.randomUUID();
delimiter = UUID.randomUUID();
throw e;
}
}
private void writeJournalHeader() throws IOException {
if (fileChannel.position() != 0)
throw new IllegalStateException("File channel is not positioned at the header location.");
ByteBuffer buffer = getWriteBuffer(FIXED_HEADER_SIZE);
buffer.put(JOURNAL_HEADER_PREFIX);
NioJournalFileRecord.writeUUID(previousDelimiter, buffer);
NioJournalFileRecord.writeUUID(delimiter, buffer);
buffer.put(JOURNAL_HEADER_SUFFIX);
fileChannel.write((ByteBuffer) buffer.flip());
// Set position to data area
fileChannel.position(FIXED_HEADER_SIZE);
}
/**
* Rollover to the beginning of the journal file.
*
* @throws IOException in case of the operation failed.
*/
public synchronized void rollover() throws IOException {
eraseRemainingBytesInJournal();
fileChannel.position(0);
previousDelimiter = delimiter;
delimiter = UUID.randomUUID();
writeJournalHeader();
}
private void eraseRemainingBytesInJournal() throws IOException {
final int blockSize = 4 * 1024;
final ByteBuffer buffer = getWriteBuffer(blockSize);
while (buffer.hasRemaining())
buffer.put((byte) ' ');
do {
buffer.flip().limit((int) Math.min(remainingCapacity(), blockSize));
} while (fileChannel.write(buffer) != 0);
}
/**
* Creates an empty record that may be used to write it to the journal.
*
* @return an empty record that can be written to the journal.
*/
public NioJournalFileRecord createEmptyRecord() {
return new NioJournalFileRecord(delimiter);
}
/**
* Writes the given records to this journal.
*
* @param records the records to write.
* @return the number of written bytes.
* @throws IOException in case of the operation failed.
*/
public synchronized long write(List<NioJournalFileRecord> records) throws IOException {
try {
final int requiredBytes = NioJournalFileRecord.calculateRequiredBytes(records);
final long remainingBytes = remainingCapacity();
if (requiredBytes > remainingBytes) {
throw new IOException("Journal requires a rollover (remaining capacity: " + remainingBytes +
", required: " + requiredBytes + "). Manually trigger this before writing new content.");
}
// the implementation of gathering and scattering byte channels is not very fast.
// using an intermediate buffer improves speed by factor 4 to 5 (direct buffer is ~25% improvement on top).
final UUID targetDelimiter = delimiter;
final ByteBuffer writeBuffer = getWriteBuffer(requiredBytes);
for (NioJournalFileRecord record : records)
record.writeRecord(targetDelimiter, writeBuffer);
writeBuffer.flip();
return fileChannel.write(writeBuffer);
} finally {
lastModified.set(System.currentTimeMillis());
}
}
private ByteBuffer getWriteBuffer(int requiredBytes) {
ByteBuffer buffer = writeBuffer;
if (buffer == null || buffer.capacity() < requiredBytes)
writeBuffer = buffer = USE_DIRECT_BUFFERS ? ByteBuffer.allocateDirect(requiredBytes) : ByteBuffer.allocate(requiredBytes);
buffer.clear().limit(requiredBytes);
return buffer;
}
/**
* Returns the remaining capacity in this journal until the rollover happens.
*
* @return the remaining capacity in this journal until the rollover happens.
* @throws IOException in case of the operation failed.
*/
public long remainingCapacity() throws IOException {
return Math.max(0, journalSize.get() - fileChannel.position());
}
/**
* Forces the journal to disk (fsync).
*
* @throws IOException in case of the operation failed.
*/
public void force() throws IOException {
final boolean debug = log.isDebugEnabled();
if (lastForced.get() != lastModified.get()) {
if (debug) { log.debug("Forcing (fsync) the file " + file + " now. Insert position is at " + fileChannel.position()); }
fileChannel.force(false);
lastForced.set(lastModified.get());
} else {
if (debug) { log.debug("Force not required on file " + file + " as no modifications were written since last call."); }
}
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "NioJournalFile{" +
"previousDelimiter=" + previousDelimiter +
", delimiter=" + delimiter +
", lastModified=" + lastModified +
", lastForced=" + lastForced +
", file=" + file +
", journalSize=" + journalSize +
'}';
}
}