org.h2.mvstore.tx.Transaction Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of h2-mvstore Show documentation
Show all versions of h2-mvstore Show documentation
Fork of h2database to maintain Java 8 compatibility
The newest version!
/*
* Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (https://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.mvstore.tx;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import org.h2.engine.IsolationLevel;
import org.h2.mvstore.DataUtils;
import org.h2.mvstore.MVMap;
import org.h2.mvstore.MVStore;
import org.h2.mvstore.RootReference;
import org.h2.mvstore.type.DataType;
import org.h2.value.VersionedValue;
/**
* A transaction.
*/
public final class Transaction {
/**
* The status of a closed transaction (committed or rolled back).
*/
public static final int STATUS_CLOSED = 0;
/**
* The status of an open transaction.
*/
public static final int STATUS_OPEN = 1;
/**
* The status of a prepared transaction.
*/
public static final int STATUS_PREPARED = 2;
/**
* The status of a transaction that has been logically committed or rather
* marked as committed, because it might be still listed among prepared,
* if it was prepared for commit. Undo log entries might still exists for it
* and not all of it's changes within map's are re-written as committed yet.
* Nevertheless, those changes should be already viewed by other
* transactions as committed.
* This transaction's id can not be re-used until all of the above is completed
* and transaction is closed.
* A transactions can be observed in this state when the store was
* closed while the transaction was not closed yet.
* When opening a store, such transactions will automatically
* be processed and closed as committed.
*/
public static final int STATUS_COMMITTED = 3;
/**
* The status of a transaction that currently in a process of rolling back
* to a savepoint.
*/
private static final int STATUS_ROLLING_BACK = 4;
/**
* The status of a transaction that has been rolled back completely,
* but undo operations are not finished yet.
*/
private static final int STATUS_ROLLED_BACK = 5;
private static final String[] STATUS_NAMES = {
"CLOSED", "OPEN", "PREPARED", "COMMITTED", "ROLLING_BACK", "ROLLED_BACK"
};
/**
* How many bits of the "operation id" we store in the transaction belong to the
* log id (the rest belong to the transaction id).
*/
static final int LOG_ID_BITS = 40;
private static final int LOG_ID_BITS1 = LOG_ID_BITS + 1;
private static final long LOG_ID_LIMIT = 1L << LOG_ID_BITS;
private static final long LOG_ID_MASK = (1L << LOG_ID_BITS1) - 1;
private static final int STATUS_BITS = 4;
private static final int STATUS_MASK = (1 << STATUS_BITS) - 1;
/**
* The transaction store.
*/
final TransactionStore store;
/**
* Listener for this transaction's rollback changes.
*/
final TransactionStore.RollbackListener listener;
/**
* The transaction id.
* More appropriate name for this field would be "slotId"
*/
final int transactionId;
/**
* This is really a transaction identity, because it's not re-used.
*/
final long sequenceNum;
/*
* Transaction state is an atomic composite field:
* bit 45 : flag whether transaction had rollback(s)
* bits 44-41 : status
* bits 40 : overflow control bit, 1 indicates overflow
* bits 39-0 : log id of the last entry in the undo log map
*/
private final AtomicLong statusAndLogId;
/**
* Reference to a counter for an earliest store version used by this transaction.
* Referenced version and all newer ones can not be discarded
* at least until this transaction ends.
*/
private MVStore.TxCounter txCounter;
/**
* Transaction name.
*/
private String name;
/**
* Indicates whether this transaction was stored in preparedTransactions map
*/
boolean wasStored;
/**
* How long to wait for blocking transaction to commit or rollback.
*/
int timeoutMillis;
/**
* Identification of the owner of this transaction,
* usually the owner is a database session.
*/
private final int ownerId;
/**
* Blocking transaction, if any
*/
private volatile Transaction blockingTransaction;
/**
* Map on which this transaction is blocked.
*/
private String blockingMapName;
/**
* Key in blockingMap on which this transaction is blocked.
*/
private Object blockingKey;
/**
* Whether other transaction(s) are waiting for this to close.
*/
private volatile boolean notificationRequested;
/**
* RootReferences for undo log snapshots
*/
private RootReference>[] undoLogRootReferences;
/**
* Map of transactional maps for this transaction
*/
private final Map> transactionMaps = new HashMap<>();
/**
* The current isolation level.
*/
final IsolationLevel isolationLevel;
Transaction(TransactionStore store, int transactionId, long sequenceNum, int status,
String name, long logId, int timeoutMillis, int ownerId,
IsolationLevel isolationLevel, TransactionStore.RollbackListener listener) {
this.store = store;
this.transactionId = transactionId;
this.sequenceNum = sequenceNum;
this.statusAndLogId = new AtomicLong(composeState(status, logId, false));
this.name = name;
setTimeoutMillis(timeoutMillis);
this.ownerId = ownerId;
this.isolationLevel = isolationLevel;
this.listener = listener;
}
public int getId() {
return transactionId;
}
public long getSequenceNum() {
return sequenceNum;
}
public int getStatus() {
return getStatus(statusAndLogId.get());
}
RootReference>[] getUndoLogRootReferences() {
return undoLogRootReferences;
}
/**
* Changes transaction status to a specified value
* @param status to be set
* @return transaction state as it was before status change
*/
private long setStatus(int status) {
while (true) {
long currentState = statusAndLogId.get();
long logId = getLogId(currentState);
int currentStatus = getStatus(currentState);
boolean valid;
switch (status) {
case STATUS_ROLLING_BACK:
valid = currentStatus == STATUS_OPEN;
break;
case STATUS_PREPARED:
valid = currentStatus == STATUS_OPEN;
break;
case STATUS_COMMITTED:
valid = currentStatus == STATUS_OPEN ||
currentStatus == STATUS_PREPARED ||
// this case is only possible if called
// from endLeftoverTransactions()
currentStatus == STATUS_COMMITTED;
break;
case STATUS_ROLLED_BACK:
valid = currentStatus == STATUS_OPEN ||
currentStatus == STATUS_PREPARED ||
currentStatus == STATUS_ROLLING_BACK;
break;
case STATUS_CLOSED:
valid = currentStatus == STATUS_COMMITTED ||
currentStatus == STATUS_ROLLED_BACK;
break;
case STATUS_OPEN:
default:
valid = false;
break;
}
if (!valid) {
throw DataUtils.newMVStoreException(
DataUtils.ERROR_TRANSACTION_ILLEGAL_STATE,
"Transaction was illegally transitioned from {0} to {1}",
getStatusName(currentStatus), getStatusName(status));
}
long newState = composeState(status, logId, hasRollback(currentState));
if (statusAndLogId.compareAndSet(currentState, newState)) {
return currentState;
}
}
}
/**
* Determine if any database changes were made as part of this transaction.
*
* @return true if there are changes to commit, false otherwise
*/
public boolean hasChanges() {
return hasChanges(statusAndLogId.get());
}
public void setName(String name) {
checkNotClosed();
this.name = name;
store.storeTransaction(this);
}
public String getName() {
return name;
}
public int getBlockerId() {
Transaction blocker = this.blockingTransaction;
return blocker == null ? 0 : blocker.ownerId;
}
/**
* Create a new savepoint.
*
* @return the savepoint id
*/
public long setSavepoint() {
return getLogId();
}
/**
* Returns whether statement dependencies are currently set.
*
* @return whether statement dependencies are currently set
*/
public boolean hasStatementDependencies() {
return !transactionMaps.isEmpty();
}
/**
* Returns the isolation level of this transaction.
*
* @return the isolation level of this transaction
*/
public IsolationLevel getIsolationLevel() {
return isolationLevel;
}
boolean isReadCommitted() {
return isolationLevel == IsolationLevel.READ_COMMITTED;
}
/**
* Whether this transaction has isolation level READ_COMMITTED or below.
* @return true if isolation level is READ_COMMITTED or READ_UNCOMMITTED
*/
public boolean allowNonRepeatableRead() {
return isolationLevel.allowNonRepeatableRead();
}
/**
* Mark an entry into a new SQL statement execution within this transaction.
*
* @param maps
* set of maps used by transaction or statement is about to be executed
*/
@SuppressWarnings({"unchecked","rawtypes"})
public void markStatementStart(HashSet>> maps) {
markStatementEnd();
if (txCounter == null && store.store.isVersioningRequired()) {
txCounter = store.store.registerVersionUsage();
}
if (maps != null && !maps.isEmpty()) {
// The purpose of the following loop is to get a coherent picture
// In order to get such a "snapshot", we wait for a moment of silence,
// when no new transaction were committed / closed.
BitSet committingTransactions;
do {
committingTransactions = store.committingTransactions.get();
for (MVMap