/* * Bitronix Transaction Manager * * Copyright (c) 2010, Bitronix Software. * * 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; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.CRC32; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import bitronix.tm.utils.Decoder; import bitronix.tm.utils.MonotonicClock; import bitronix.tm.utils.Uid; /** * Representation of a transaction log record on disk. * <p>On-disk format has been implemented following Mike Spille's recommendations. Quoting him:</p> * <p/> * <i><p><code> * [RECORD_TYPE :4] * [RECORD_LEN :4] * [HEADER_LEN :4] * [System.currentTimeMillis :8] * [Sequence number :4] * [Checksum :4] * [Payload :X] * [END_RECORD_INDICATOR :4] * </code></p> * <p>Where [RECORD_TYPE] is a passed-in record type from the TM. [RECORD_LEN] is the overall record length * (sans [RECORD_TYPE and [RECORD_LEN]). [HEADER_LEN] is the length of the remainder of the header - important if you * want to support easy upgrades of your format. The remaining pieces are the rest of the header, and the payload. The * header at least should have [System.currentTimeMillis] and [Sequence number], with the [sequence number] coming from * some monotically increasing sequence generator unique to the process. The [checksum] is optional for the paranoid * among us. The time information can be very useful for profiling and tracking down problems in production, and in * conjunction with the sequence number it can give you precise ordering. This doesn't give you much in this solution, * but can be priceless if you ever move to a system with multiple dual log file pairs to lessen single-threading on a * single log file pair. Finally, I like having an [END_RECORD_INDICATOR] as an extra corruption detector device - I'm * a suspenders and belt kind of guy. Actually, the END_RECORD_INDICATOR and [RECORD_LEN] in conjunction are very * useful in development, as well, to catch programming mistakes in the log system early.</p></i> * <p/> * <p>Payload contains <code>[GTRID LENGTH :1] [GTRID :A] [UNIQUE NAMES COUNT :4] ([UNIQUE NAME LENGTH :2] [UNIQUE NAME :Y] ...)</code> * which makes a major difference with Mike's proposed format because here a record can vary in length: the GTRID size * is A bytes long (A being the GTRID length) and there can be X unique names that are Y characters long, Y being eventually * different for each name.</p> * * @see <a href="http://jroller.com/page/pyrasun?entry=xa_exposed_part_iii_the">XA Exposed, Part III: The Implementor's Notebook</a> * @author lorban */ public class TransactionLogRecord implements JournalRecord { private final static Logger log = LoggerFactory.getLogger(TransactionLogRecord.class); // status + record length + record header length + current time + sequence number + checksum private final static int RECORD_HEADER_LENGTH = 4 + 4 + 4 + 8 + 4 + 4; private final static AtomicInteger sequenceGenerator = new AtomicInteger(); private final int status; private int recordLength; private int headerLength; private final long time; private final int sequenceNumber; private int crc32; private final Uid gtrid; private final SortedSet<String> uniqueNames; private final int endRecord; private long writePosition; /** * Use this constructor when restoring a log from the disk. * * @param status record type * @param recordLength record length excluding status and recordLength * @param headerLength length of all fields except gtrid, uniqueNames and endRecord * @param time current time in milliseconds * @param sequenceNumber atomically generated sequence number during a JVM's lifespan * @param crc32 checksum of the full record * @param gtrid global transaction id * @param uniqueNames unique names of XA data sources used in this transaction * @param endRecord end of record marker */ public TransactionLogRecord(int status, int recordLength, int headerLength, long time, int sequenceNumber, int crc32, Uid gtrid, Set<String> uniqueNames, int endRecord) { this.status = status; this.recordLength = recordLength; this.headerLength = headerLength; this.time = time; this.sequenceNumber = sequenceNumber; this.crc32 = crc32; this.gtrid = gtrid; this.uniqueNames = new TreeSet<String>(uniqueNames); this.endRecord = endRecord; } /** * Create a new transaction log ready to be stored. * @param status record type * @param gtrid global transaction id * @param uniqueNames unique names of XA data sources used in this transaction */ public TransactionLogRecord(int status, Uid gtrid, Set<String> uniqueNames) { this.status = status; this.time = MonotonicClock.currentTimeMillis(); this.sequenceNumber = sequenceGenerator.incrementAndGet(); this.gtrid = gtrid; this.uniqueNames = new TreeSet<String>(uniqueNames); this.endRecord = TransactionLogAppender.END_RECORD; this.headerLength = RECORD_HEADER_LENGTH; refresh(); } public int getStatus() { return status; } public int getRecordLength() { return recordLength; } public int getHeaderLength() { return headerLength; } public long getTime() { return time; } public int getSequenceNumber() { return sequenceNumber; } public int getCrc32() { return crc32; } public Uid getGtrid() { return gtrid; } public long getWritePosition() { return writePosition; } public void setWritePosition(long position) { writePosition = position; } public Set<String> getUniqueNames() { return Collections.unmodifiableSortedSet(uniqueNames); } public int getEndRecord() { return endRecord; } /** * Recalculate and store the dynamic values of this record: {@link #getRecordLength()}, {@link #getRecordHeaderLength()} * and {@link #calculateCrc32()}. This method must be called each time after the set of contained unique names is updated. */ private void refresh() { crc32 = calculateCrc32(); } /** * Recalculate the CRC32 value of this record (using {@link #calculateCrc32()}) and compare it with the stored value. * @return true if the recalculated value equals the stored one, false otherwise. */ public boolean isCrc32Correct() { return calculateCrc32() == getCrc32(); } /** * {@inheritDoc} */ public boolean isValid() { return isCrc32Correct(); } /** * {@inheritDoc} */ public Map<String, ?> getRecordProperties() { Map<String, Object> props = new LinkedHashMap<String, Object>(4); props.put("recordLength", recordLength); props.put("headerLength", headerLength); props.put("sequenceNumber", sequenceNumber); props.put("crc32", crc32); return props; } /** * Calculate the CRC32 value of this record. * @return the CRC32 value of this record. */ public int calculateCrc32() { int total = 0; for (String uniqueName : uniqueNames) { total += 2 + uniqueName.length(); // 2 bytes for storing the unique name length + unique name length } recordLength = total + getFixedRecordLength(); ByteBuffer buf = ByteBuffer.allocate(24 + gtrid.length() + 4 /*uniqueNames.size*/ + total + 4 /*endRecord*/); buf.putInt(status); // offset: 0 buf.putInt(recordLength); // offset: 4 buf.putInt(headerLength); // offset: 8 buf.putLong(time); // offset: 12 buf.putInt(sequenceNumber); // offset: 20 buf.put(gtrid.getArray()); // offset: 24 buf.putInt(uniqueNames.size()); // offset: 24 + gtridArray.length for (String name : uniqueNames) { buf.putShort((short) name.length()); try { buf.put(name.getBytes("US-ASCII")); } catch (UnsupportedEncodingException ex) { log.error("unable to convert unique name bytes to US-ASCII", ex); } } buf.putInt(endRecord); CRC32 crc32 = new CRC32(); crc32.update(buf.array()); return (int) crc32.getValue(); } public String toString() { StringBuilder sb = new StringBuilder(128); sb.append("a Bitronix TransactionLogRecord with "); sb.append("status="); sb.append(Decoder.decodeStatus(status)); sb.append(", "); sb.append("recordLength="); sb.append(recordLength); sb.append(", "); sb.append("headerLength="); sb.append(headerLength); sb.append(", "); sb.append("time="); sb.append(time); sb.append(", "); sb.append("sequenceNumber="); sb.append(sequenceNumber); sb.append(", "); sb.append("crc32="); sb.append(crc32); sb.append(", "); sb.append("gtrid="); sb.append(gtrid.toString()); sb.append(", "); sb.append("uniqueNames="); Iterator<String> it = uniqueNames.iterator(); while (it.hasNext()) { String s = it.next(); sb.append(s); if (it.hasNext()) sb.append(','); } return sb.toString(); } /** * this is the total size on disk of a TransactionLog. * @return recordLength */ int calculateTotalRecordSize() { return recordLength + 4 + 4; // + status + record length } /** * Length of all the fixed size fields part of the record length header except status and record length. * @return fixedRecordLength */ private int getFixedRecordLength() { // record header length + current time + sequence number + checksum + GTRID size + GTRID + unique names count + end record marker return 4 + 8 + 4 + 4 + 1 + gtrid.length() + 4 + 4; } static class NullOutputStream extends OutputStream { static final NullOutputStream INSTANCE = new NullOutputStream(); private NullOutputStream() { } public void write(int b) throws IOException { } } }