/* * 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.JournalRecord; import bitronix.tm.utils.Uid; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharsetEncoder; import java.util.*; import java.util.concurrent.atomic.AtomicLong; import static java.util.Collections.unmodifiableSet; /** * Implements {@code TransactionLogRecord} for the NioJournal. * * @author juergen kellerer, 2011-04-30 */ class NioJournalRecord implements JournalRecord, NioJournalConstants { // Starts from 0 for every new runtime session. private static final AtomicLong JOURNAL_RECORD_SEQUENCE = new AtomicLong(); /** * Provides thread safe access to name encoders. * (Similar to the way it is done inside the JRE as constructing the encoder is relatively expensive) */ public static final ThreadLocal<CharsetEncoder> NAME_ENCODERS = new ThreadLocal<CharsetEncoder>() { @Override protected CharsetEncoder initialValue() { return NAME_CHARSET.newEncoder(); } }; private static final byte[][] txStatusStrings; static { txStatusStrings = new byte[TRANSACTION_STATUS_STRINGS.size()][]; for (int i = 0; i < txStatusStrings.length; i++) { String statusString = TRANSACTION_STATUS_STRINGS.get(i); try { txStatusStrings[i] = statusString.getBytes(NAME_CHARSET.name()); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } if (txStatusStrings[i].length != 6) throw new IllegalStateException("The TX status string '" + statusString + "' encodes to a length != 6. Please fix this."); } } /** * Specifies the static length of a serialized record from this class. */ public static final int STATIC_RECORD_LENGTH = /* status-string */ txStatusStrings[0].length + /* status */ 4 + /* recordLength */ 4 + /* time */ 8 + /* sequenceNumber */ 8 + /* rolled over flag */ 1 + /* gtrid (static) */ 1 + /* uniqueNames (static) */ 2; private static byte[] statusToBytes(int status) { if (status >= 0 && status < txStatusStrings.length) return txStatusStrings[status]; return txStatusStrings[txStatusStrings.length - 1]; } private static ByteBuffer skipStatusString(ByteBuffer buffer) { return (ByteBuffer) buffer.position(buffer.position() + txStatusStrings[0].length); } private static int calculateRecordLength(Uid gtrid, Set<String> names) { int length = STATIC_RECORD_LENGTH + gtrid.getArray().length; for (String name : names) length += 2 + name.length(); return length; } private static Set<String> namesFromBuffer(ByteBuffer buffer) { final int count = buffer.getShort(); final Set<String> names = new HashSet<String>(count); for (int i = 0, len; i < count; i++) { // Note: Decoding may be implemented without max. speed optimization as it is only used when // reading the journal file (happens only once) len = buffer.getShort(); String name = NAME_CHARSET.decode((ByteBuffer) buffer.slice().limit(len)).toString(); // Note: Unique names should be only a couple, but many log records may be created when reading a file. // internalizing strings can significantly reduce the memory footprint. names.add(name.intern()); buffer.position(buffer.position() + len); } return names; } private static void namesToBuffer(Set<String> uniqueNames, ByteBuffer buffer) { final CharsetEncoder charsetEncoder = NAME_ENCODERS.get(); assertIsInRange(uniqueNames, uniqueNames.size(), Short.MAX_VALUE); buffer.putShort((short) uniqueNames.size()); for (Object un : uniqueNames) { String name = (String) un; final int length = name.length(); assertIsInRange(un, length, Short.MAX_VALUE); buffer.putShort((short) length); charsetEncoder.encode(CharBuffer.wrap(name), buffer, true); } } private static Uid uidFromBuffer(ByteBuffer buffer) { byte[] arr = new byte[buffer.get() & 0xff]; buffer.get(arr); return new Uid(arr); } private static void uidToBuffer(Uid uid, ByteBuffer buffer) { byte[] array = uid.getArray(); assertIsInRange(uid, array.length, Byte.MAX_VALUE); buffer.put((byte) array.length); buffer.put(array); } private static void assertIsInRange(Object element, int length, int limit) { if (length > limit) throw new IllegalArgumentException("Cannot encode " + element + " as its size exceeds the limit of " + limit); } private final int status; private final Uid gtrid; private final Set<String> uniqueNames; private final long time, sequenceNumber; private final int recordLength; private final boolean valid, rolledOverFlag; NioJournalRecord(int status, int recordLength, long time, long sequenceNumber, boolean rolledOverFlag, Uid gtrid, Set<String> uniqueNames, boolean valid) { // Note: This constructor should not be used outside of unit tests or this class. this.status = status; this.gtrid = gtrid; this.uniqueNames = uniqueNames; this.time = time; this.sequenceNumber = sequenceNumber; this.recordLength = recordLength; this.valid = valid; this.rolledOverFlag = rolledOverFlag; } /** * Constructs a new record of the given values. * * @param status the TX status, see {@link javax.transaction.Status}. * @param gtrid the global transaction id. * @param uniqueNames the unique names identifying the resources participating in the transaction. */ public NioJournalRecord(int status, Uid gtrid, Set<String> uniqueNames) { this(status, calculateRecordLength(gtrid, uniqueNames), System.currentTimeMillis(), JOURNAL_RECORD_SEQUENCE.incrementAndGet(), false, gtrid, unmodifiableSet(new HashSet<String>(uniqueNames)), true); } /** * Constructs a new record by de-serializing the state from the given byte buffer. * <p/> * Note: When valid is set to false, the buffer must still contain decodeable data. * Data that fails decoding will cause runtime exceptions. * * @param buffer the buffer containing the serialized record state. * @param valid specifies whether the record should be marked valid. */ public NioJournalRecord(ByteBuffer buffer, boolean valid) { this( skipStatusString(buffer).getInt(), // status buffer.getInt(), // recordLength buffer.getLong(), // time buffer.getLong(), // sequenceNumber buffer.get() == 1, // rolledOver flag uidFromBuffer(buffer), // gtrid unmodifiableSet(namesFromBuffer(buffer)), // uniqueNames valid ); } /** * Encodes this instance to the given buffer. * * @param buffer the buffer to use for writing. * @param rolledOver specifies whether the rollover flag should be set, indicating that the record was rewritten during a journal rollover. */ protected void encodeTo(ByteBuffer buffer, boolean rolledOver) { buffer.put(statusToBytes(getStatus())); buffer.putInt(getStatus()); buffer.putInt(getRecordLength()); buffer.putLong(getTime()); buffer.putLong(getSequenceNumber()); buffer.put((byte) (rolledOver ? 1 : 0)); uidToBuffer(getGtrid(), buffer); namesToBuffer(getUniqueNames(), buffer); } /** * Returns a copy of this record with unique names being reduced by the given record. * * @param comittedOrRolledbackRecord a record in committed or rolledback state used to reduce the unique names of this record. * @return a copy of this record with a reduced set of names. If no reduction happened 'this' is returned. */ protected NioJournalRecord createNameReducedCopy(NioJournalRecord comittedOrRolledbackRecord) { if (!FINAL_STATUS.contains(comittedOrRolledbackRecord.status)) { throw new IllegalArgumentException("The given record " + comittedOrRolledbackRecord + " is not in a state that allows " + "its usage as resource name reduction record."); } final HashSet<String> reducedNames = new HashSet<String>(uniqueNames); if (reducedNames.removeAll(comittedOrRolledbackRecord.getUniqueNames())) return new NioJournalRecord(status, recordLength, time, sequenceNumber, rolledOverFlag, gtrid, unmodifiableSet(reducedNames), valid); else return this; } /** * Returns true if the given buffer has enough capacity to take this record. * * @param buffer the buffer to test. * @return true if the capacity is sufficient for encoding. */ public boolean hasEnoughCapacity(ByteBuffer buffer) { return buffer != null && buffer.capacity() >= getRecordLength(); } /** * {@inheritDoc} */ public int getStatus() { return status; } /** * {@inheritDoc} */ public Uid getGtrid() { return gtrid; } /** * {@inheritDoc} */ public Set<String> getUniqueNames() { return uniqueNames; } /** * Returns the number of unique names. * * @return the number of unique names. */ protected int getUniqueNamesCount() { return uniqueNames.size(); } /** * Returns true if the unique names of this record are all contained in 'other'. * * @param other the other record to check. * @return true if the unique names of this record are all contained in 'other'. */ protected boolean isUniqueNamesContainedInRecord(NioJournalRecord other) { return uniqueNames.isEmpty() || other.uniqueNames.containsAll(uniqueNames); } /** * {@inheritDoc} */ public long getTime() { return time; } public long getSequenceNumber() { return sequenceNumber; } public int getRecordLength() { return recordLength; } public boolean isRolledOverFlag() { return rolledOverFlag; } /** * {@inheritDoc} */ public Map<String, ?> getRecordProperties() { Map<String, Object> props = new LinkedHashMap<String, Object>(4); props.put("recordLength", recordLength); props.put("sequenceNumber", sequenceNumber); props.put("rolledOverFlag", rolledOverFlag); return props; } /** * {@inheritDoc} */ public boolean isValid() { return valid; } /** * {@inheritDoc} */ @Override public String toString() { return "NioJournalRecord{" + "status=" + TRANSACTION_LONG_STATUS_STRINGS.get(Math.min(TRANSACTION_LONG_STATUS_STRINGS.size() - 1, status)) + ", gtrid=" + gtrid + ", uniqueNames=" + uniqueNames + ", time=" + new Date(time) + ", sequenceNumber=" + sequenceNumber + ", recordLength=" + recordLength + ", rolledOverFlag=" + rolledOverFlag + ", valid=" + valid + '}'; } /** * {@inheritDoc} */ @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof NioJournalRecord)) return false; NioJournalRecord that = (NioJournalRecord) o; if (recordLength != that.getRecordLength()) return false; if (sequenceNumber != that.getSequenceNumber()) return false; if (status != that.getStatus()) return false; if (time != that.getTime()) return false; if (valid != that.isValid()) return false; if (gtrid != null ? !gtrid.equals(that.getGtrid()) : that.getGtrid() != null) return false; if (uniqueNames != null ? !uniqueNames.equals(that.getUniqueNames()) : that.getUniqueNames() != null) return false; return true; } /** * {@inheritDoc} */ @Override public int hashCode() { int result = status; result = 31 * result + (gtrid != null ? gtrid.hashCode() : 0); result = 31 * result + (uniqueNames != null ? uniqueNames.hashCode() : 0); result = 31 * result + (int) (time ^ (time >>> 32)); result = 31 * result + (int) (sequenceNumber ^ (sequenceNumber >>> 32)); result = 31 * result + recordLength; result = 31 * result + (valid ? 1 : 0); return result; } }