/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.github.geophile.erdo.transaction;
import com.github.geophile.erdo.AbstractKey;
import com.github.geophile.erdo.Cursor;
import com.github.geophile.erdo.TransactionCallback;
import com.github.geophile.erdo.map.Factory;
import com.github.geophile.erdo.map.transactionalmap.TransactionalMap;
import com.github.geophile.erdo.util.IdentitySet;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
// Transaction times and timestamps:
//
// startTime and commitTime are used to manage the TransactionRegistry.
// timestamp is the timestamp of a committed transactions, indicates serialization order,
// and is used for record versioning. commitTime order must be consistent with timestamp order.
// However, commitTime cannot substitute for timestamp. Manifests store timestamps. Because
// timestamps are dense, they compress better.
//
// commitTime order is consistent with timestamp order because Transaction.commit is called by
// TransactionManager.commitTransactionAsynchronously inside the TransactionManager's monitor.
public class Transaction
{
// Object interface
@Override
public String toString()
{
StringBuilder buffer = new StringBuilder();
buffer.append("txn(");
buffer.append(times.startTime);
buffer.append(')');
if (termination != null) {
switch (termination) {
case COMMITTED:
buffer.append('(');
buffer.append(times.commitTime);
buffer.append(")ts");
buffer.append(times.timestamp);
break;
case ABORTED:
buffer.append('A');
break;
case DEADLOCK_VICTIM:
buffer.append('D');
break;
}
}
if (irrelevant) {
buffer.append('i');
}
return buffer.toString();
}
@Override
public int hashCode()
{
long h = startTime() * 9987001;
return (int) (h >> 32) | (int) h;
}
@Override
public boolean equals(Object o)
{
return o != null && o instanceof Transaction && startTime() == ((Transaction) o).startTime();
}
// Transaction interface
public long startTime()
{
return times.startTime();
}
public long commitTime()
{
return times.commitTime();
}
public long timestamp()
{
return times.timestamp();
}
public synchronized void markDurable()
{
if (callback != null) {
try {
callback.whenDurable(commitInfo);
} finally {
commitInfo = null;
}
}
}
public boolean synchronousCommit()
{
return callback == null;
}
public void markDeadlockVictim()
{
LOG.log(Level.INFO, "Marking {0} as DEADLOCK_VICTIM", this);
assert termination == null : this;
termination = Termination.DEADLOCK_VICTIM;
irrelevant = true;
}
public void markIrrelevant()
{
LOG.log(Level.INFO, "Marking {0} as irrelevant", this);
assert termination != null : this;
assert hasTerminated() : this;
irrelevant = true;
}
public boolean endedTragically()
{
return termination == Termination.ABORTED || termination == Termination.DEADLOCK_VICTIM;
}
public boolean hasAborted()
{
return termination != null && termination != Termination.COMMITTED;
}
public boolean hasCommitted()
{
return termination == Termination.COMMITTED;
}
public boolean hasTerminated()
{
return termination != null;
}
public boolean irrelevant()
{
return irrelevant;
}
public void waitingFor(AbstractKey key)
{
assert waitingFor == null : waitingFor;
waitingFor = key;
}
public void doneWaitingForKey()
{
if (!hasTerminated()) {
lockedForWrite.add(waitingFor);
}
waitingFor = null;
}
public Set<AbstractKey> lockedForWrite()
{
return lockedForWrite;
}
public void registerCursor(Cursor cursor)
{
Cursor replaced = openCursors.add(cursor);
assert replaced == null;
}
public void unregisterCursor(Cursor cursor)
{
if (!ending) {
Cursor removed = openCursors.remove(cursor);
assert removed == cursor;
}
}
// For testing
public IdentitySet<Cursor> openCursors()
{
return openCursors;
}
public void throwExceptionDueToTragicEnding()
throws DeadlockException, TransactionRolledBackException
{
if (termination == Termination.DEADLOCK_VICTIM) {
throw new DeadlockException(this);
} else if (termination == Termination.ABORTED) {
throw new TransactionRolledBackException(this);
} else {
assert false : this;
}
}
public TransactionalMap transactionalMap()
{
return transactionalMap;
}
public void commit(TransactionCallback callback, Object commitInfo)
throws IOException, InterruptedException
{
assert Thread.holdsLock(transactionManager) : this;
closeOpenScans();
assert termination == null : this;
assert waitingFor == null
: String.format("Can''t commit %s because it is waiting for %s", this, waitingFor);
this.termination = Termination.COMMITTED;
this.callback = callback;
this.commitInfo = commitInfo;
this.times.setCommitTimeAndTimestamp();
if (LOG.isLoggable(Level.INFO)) {
if (commitInfo == null) {
LOG.log(Level.INFO, "Committing transaction {0}", this);
} else {
LOG.log(Level.INFO, "Committing transaction {0}: {1}", new Object[]{this, commitInfo});
}
}
}
public void rollback()
{
// DON'T assert Thread.holdsLock(transactionManager). rollback can be initiated by
// another transaction noticing that this transaction can't succeed, e.g. due to a
// lock conflict.
//
// Why test termination: There is a race between:
// - A transaction committing and aborting its waiters, and
// - Another transaction requesting a lock owned by this transaction, seeing
// the commit and aborting itself.
// So termination could already be set to ABORTED.
if (termination == null) {
closeOpenScans();
termination = Termination.ABORTED;
irrelevant = true;
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "Aborting transaction {0}: {1}", new Object[]{this, termination});
}
} else {
assert
termination == Termination.ABORTED || termination == Termination.DEADLOCK_VICTIM
: this;
}
}
public void destroy()
{
LOG.log(Level.INFO, "Destroying {0}", this);
if (transactionalMap() != null) {
transactionalMap.cleanup();
} // else: Tests of transaction management (only) don't use transactionalMaps.
transactionalMap = null;
lockedForWrite = null;
waitingFor = null;
}
public static synchronized void initialize(Factory factory)
{
Transaction.factory = factory;
}
public static Transaction newTransaction(TransactionManager transactionalManager,
TransactionalMap transactionalMap)
{
assert Thread.holdsLock(transactionalManager);
return new Transaction(transactionalManager, transactionalMap);
}
// For use by this class
private void closeOpenScans()
{
ending = true;
for (Cursor cursor : openCursors.values()) {
cursor.close();
}
openCursors = null;
}
private Transaction(TransactionManager transactionManager, TransactionalMap transactionalMap)
{
// transactionalMap is null in some tests
assert transactionManager != null;
this.transactionManager = transactionManager;
this.transactionalMap = transactionalMap;
this.times = new Times(factory.nextTransactionTime());
}
// Class state
// Why UNCOMMITTED_TIMESTAMP is Long.MAX_VALUE: timestamp is initialized to this value, and keeps this
// value until the transaction is committed. This value will cause the uncommitted transaction,
// part of a PrivateMap, to be ranked after all committed timestamps.
public static final long UNCOMMITTED_TIMESTAMP = Long.MAX_VALUE;
private static final Logger LOG = Logger.getLogger(Transaction.class.getName());
private static Factory factory;
// Object state
private final TransactionManager transactionManager;
private TransactionalMap transactionalMap;
private Set<AbstractKey> lockedForWrite = new HashSet<>();
private final Times times;
private volatile TransactionCallback callback;
private volatile Object commitInfo;
private volatile AbstractKey waitingFor;
private volatile Termination termination;
private volatile boolean irrelevant = false;
private IdentitySet<Cursor> openCursors = new IdentitySet<>();
// unregisterCursor is called from CursorImpl.close. If CursorImpl.close is called from Transaction.closeOpenScans,
// then we get a ConcurrentModificationException on openCursors. If we are in closeOpenScans, then openCursors is
// about to become irrelevant. This ending variable is used to skip doing the openCursors maintenance in
// unregisterCursor.
private boolean ending = false;
// Inner classes
private enum Termination
{
COMMITTED,
ABORTED,
DEADLOCK_VICTIM
}
public static class Times
{
public String toString()
{
return String.format("Times(start = %s, commit = %s, timestamp = %s)",
startTime, commitTime, timestamp);
}
public long startTime()
{
return startTime;
}
public long commitTime()
{
return commitTime;
}
public long timestamp()
{
return timestamp;
}
public void setCommitTimeAndTimestamp()
{
assert commitTime == UNCOMMITTED_TIMESTAMP : this;
assert timestamp == UNCOMMITTED_TIMESTAMP : this;
commitTime = factory.nextTransactionTime();
timestamp = factory.nextTransactionTimestamp();
}
public Times(long startTime)
{
this.startTime = startTime;
}
private final long startTime;
private long commitTime = UNCOMMITTED_TIMESTAMP;
private long timestamp = UNCOMMITTED_TIMESTAMP;
}
}