/* This file is part of VoltDB.
* Copyright (C) 2008-2010 VoltDB L.L.C.
*
* VoltDB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VoltDB 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb;
import java.util.Calendar;
import java.util.Date;
import org.apache.log4j.Logger;
import edu.brown.hstore.HStore;
/**
* <p>The TransactionIdManager creates Transaction ids that
* get assigned to VoltDB timestamps. A transaction id contains
* three fields, the time of creation, a counter to ensure local
* ordering, and the siteid of the generating site.</p>
*
* <p>This class also contains methods to examine the embedded values of
* transaction ids.</p>
*
* <p>If the clocks of two different machines are reasonably in sync,
* txn ids created at the same time on different machines will be reasonably
* close in value. Thus transaction ids can be used for a global ordering.</p>
*
*/
public class TransactionIdManager {
private static final Logger LOG = Logger.getLogger(TransactionIdManager.class);
// bit sizes for each of the fields in the 64-bit id
// note, these add up to 63 bits to make dealing with
// signed / unsigned conversions easier.
static final long TIMESTAMP_BITS = 40;
static final long COUNTER_BITS = 13;
static final long INITIATORID_BITS = 10;
static final int DRIFT_CHECK = 5; // ms
// VOLT_EPOCH holds the time in millis since 1/1/2008 at 12am.
// The current time - VOLT_EPOCH should fit nicely in 40 bits
// of memory.
static final long VOLT_EPOCH = getEpoch();
public static long getEpoch() {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(0);
c.set(2010, 0, 1, 0, 0, 0);
c.set(Calendar.MILLISECOND, 0);
c.set(Calendar.ZONE_OFFSET, 0);
c.set(Calendar.DST_OFFSET, 0);
long retval = c.getTimeInMillis();
return retval;
}
// maximum values for the fields
// used for bit-shifts and error checking
static final long TIMESTAMP_MAX_VALUE = (1L << TIMESTAMP_BITS) - 1L;
static final long COUNTER_MAX_VALUE = (1L << COUNTER_BITS) - 1L;
static final long INITIATORID_MAX_VALUE = (1L << INITIATORID_BITS) - 1L;
// the local siteid
long initiatorId;
// the time of the previous txn id generation
long lastUsedTime = -1;
// the number of txns generated during the same value
// for System.currentTimeMillis()
long counterValue = 0;
// remembers the last txn generated
Long lastTxnId = 0l;
long time_delta = 0L;
/**
* Initialize the TransactionIdManager for this site
* @param initiatorId The siteId of the current site.
*/
public TransactionIdManager(int initiatorId) {
this.initiatorId = initiatorId;
}
/**
* Generate a unique id that contains a timestamp, a counter
* and a siteid packed into a 64-bit long value. Subsequent calls
* to this method will return strictly larger long values.
* @return The newly generated transaction id.
*/
public Long getNextUniqueTransactionId() {
long currentTime = 0;
long currentCounter = 0;
synchronized (this) {
currentTime = System.currentTimeMillis();
if (currentTime == this.lastUsedTime) {
// increment the counter for this millisecond
currentCounter = ++this.counterValue;
// handle the case where we've run out of counter values
// for this particular millisecond (feels unlikely)
if (this.counterValue > COUNTER_MAX_VALUE) {
LOG.warn("TOO MANY TXNS! SPIN LOCK!!!");
// spin until the next millisecond
while (currentTime == this.lastUsedTime)
currentTime = System.currentTimeMillis();
// reset the counter and lastUsedTime for the new millisecond
this.lastUsedTime = currentTime;
currentCounter = this.counterValue = 0;
}
}
else {
// reset the counter and lastUsedTime for the new millisecond
if (currentTime < this.lastUsedTime) {
LOG.warn(String.format("Initiator time moved backwards from %d to %d by %d ms!!!",
this.lastUsedTime, currentTime, (this.lastUsedTime - currentTime)));
// if the diff is less than 5 ms, wait a bit
if ((this.lastUsedTime - currentTime) < DRIFT_CHECK) {
int count = 1000;
// note, the loop should stop once lastUsedTime is PASSED, not current
while ((currentTime <= this.lastUsedTime) && (count-- > 0)) {
try {
Thread.sleep(this.lastUsedTime - currentTime + 1);
} catch (InterruptedException e) {}
currentTime = System.currentTimeMillis();
}
// if the loop above ended because it ran too much
if (count < 0) {
LOG.error("H-Store was unable to recover after the system time was externally negatively adusted. " +
"It is possible that there is a serious system time or NTP error. ");
HStore.crashDB();
}
}
// crash immediately if time has gone backwards by too much
else {
HStore.crashDB();
}
}
this.lastUsedTime = currentTime;
currentCounter = this.counterValue = 0;
}
} // SYNCH
Long newTxnId = new Long(makeIdFromComponents(currentTime + this.time_delta,
currentCounter,
this.initiatorId));
this.lastTxnId = newTxnId;
return (newTxnId);
}
public static long makeIdFromComponents(long ts, long seqNo, long initiatorId) {
// compute the time in millis since VOLT_EPOCH
long txnId = ts - VOLT_EPOCH;
// verify all fields are the right size
assert(txnId <= TIMESTAMP_MAX_VALUE);
assert(seqNo <= COUNTER_MAX_VALUE);
assert(initiatorId <= INITIATORID_MAX_VALUE);
// put this time value in the right offset
txnId = txnId << (COUNTER_BITS + INITIATORID_BITS);
// add the counter value at the right offset
txnId |= seqNo << INITIATORID_BITS;
// finally add the siteid at the end
txnId |= initiatorId;
return txnId;
}
/**
* Given a transaction id, return the time of its creation
* by examining the embedded timestamp.
* @param txnId The transaction id value to examine.
* @return The Date object representing the time this transaction
* id was created.
*/
public static Date getDateFromTransactionId(long txnId) {
long time = txnId >> (COUNTER_BITS + INITIATORID_BITS);
time += VOLT_EPOCH;
return new Date(time);
}
/**
* Given a transaction id, return the time of its creation
* by examining the embedded timestamp.
* @param txnId The transaction id value to examine.
* @return The integer representing the time this transaction
* id was created.
*/
public static long getTimestampFromTransactionId(long txnId) {
long time = txnId >> (COUNTER_BITS + INITIATORID_BITS);
time += VOLT_EPOCH;
return time;
}
/**
* Given a transaction id, return the embedded site id.
* @param txnId The transaction id value to examine.
* @return The site id embedded within the transaction id.
*/
public static long getInitiatorIdFromTransactionId(long txnId) {
return txnId & INITIATORID_MAX_VALUE;
}
public static long getSequenceNumberFromTransactionId(long txnId) {
long seq = txnId >> INITIATORID_BITS;
seq = seq & COUNTER_MAX_VALUE;
return seq;
}
/**
* Get the last txn id generated.
* @return The last txn id generated.
*/
public Long getLastTxnId() {
return lastTxnId;
}
public long getLastUsedTime() {
return lastUsedTime;
}
/**
* This should not be invoked directly by anybody else at runtime
* @param delta
*/
public void setTimeDelta(long delta) {
this.time_delta = delta;
}
/**
* Get a string representation of the TxnId
*/
public static String toString(long txnId) {
final StringBuilder sb = new StringBuilder(128);
sb.append("Timestamp: ").append(getTimestampFromTransactionId(txnId));
sb.append(".").append(getSequenceNumberFromTransactionId(txnId));
sb.append(" InititatorId: ").append(getInitiatorIdFromTransactionId(txnId));
return sb.toString();
}
public static String toBitString(long txnId) {
String retval = "";
long mask = 0x8000000000000000L;
for(int i = 0; i < 64; i++) {
if ((txnId & mask) == 0) retval += "0";
else retval += "1";
mask >>>= 1;
}
return retval;
}
}