org.h2.mvstore.tx.TransactionMap Maven / Gradle / Ivy
/*
* Copyright 2004-2022 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.AbstractMap;
import java.util.AbstractSet;
import java.util.BitSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import org.h2.engine.IsolationLevel;
import org.h2.mvstore.Cursor;
import org.h2.mvstore.DataUtils;
import org.h2.mvstore.MVMap;
import org.h2.mvstore.MVStoreException;
import org.h2.mvstore.RootReference;
import org.h2.mvstore.type.DataType;
import org.h2.value.VersionedValue;
/**
* A map that supports transactions.
*
*
* Methods of this class may be changed at any time without notice. If
* you use this class directly make sure that your application or library
* requires exactly the same version of MVStore or H2 jar as the version that
* you use during its development and build.
*
*
* @param the key type
* @param the value type
*/
public final class TransactionMap extends AbstractMap {
/**
* The map used for writing (the latest version).
*
* Key: key the key of the data.
* Value: { transactionId, oldVersion, value }
*/
public final MVMap> map;
/**
* The transaction which is used for this map.
*/
private final Transaction transaction;
/**
* Snapshot of this map as of beginning of transaction or
* first usage within transaction or
* beginning of the statement, depending on isolation level
*/
private Snapshot> snapshot;
/**
* Snapshot of this map as of beginning of beginning of the statement
*/
private Snapshot> statementSnapshot;
/**
* Indicates whether underlying map was modified from within related transaction
*/
private boolean hasChanges;
private final TxDecisionMaker txDecisionMaker;
private final TxDecisionMaker ifAbsentDecisionMaker;
private final TxDecisionMaker lockDecisionMaker;
TransactionMap(Transaction transaction, MVMap> map) {
this.transaction = transaction;
this.map = map;
this.txDecisionMaker = new TxDecisionMaker<>(map.getId(), transaction);
this.ifAbsentDecisionMaker = new TxDecisionMaker.PutIfAbsentDecisionMaker<>(map.getId(),
transaction, this::getFromSnapshot);
this.lockDecisionMaker = transaction.allowNonRepeatableRead()
? new TxDecisionMaker.LockDecisionMaker<>(map.getId(), transaction)
: new TxDecisionMaker.RepeatableReadLockDecisionMaker<>(map.getId(), transaction,
map.getValueType(), this::getFromSnapshot);
}
/**
* Get a clone of this map for the given transaction.
*
* @param transaction the transaction
* @return the map
*/
public TransactionMap getInstance(Transaction transaction) {
return transaction.openMapX(map);
}
/**
* Get the number of entries, as a integer. {@link Integer#MAX_VALUE} is
* returned if there are more than this entries.
*
* @return the number of entries, as an integer
* @see #sizeAsLong()
*/
@Override
public int size() {
long size = sizeAsLong();
return size > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) size;
}
/**
* Get the size of the raw map. This includes uncommitted entries, and
* transiently removed entries, so it is the maximum number of entries.
*
* @return the maximum size
*/
public long sizeAsLongMax() {
return map.sizeAsLong();
}
/**
* Get the size of the map as seen by this transaction.
*
* @return the size
*/
public long sizeAsLong() {
IsolationLevel isolationLevel = transaction.getIsolationLevel();
if (!isolationLevel.allowNonRepeatableRead() && hasChanges) {
return sizeAsLongRepeatableReadWithChanges();
}
// getting coherent picture of the map, committing transactions, and undo logs
// either from values stored in transaction (never loops in that case),
// or current values from the transaction store (loops until moment of silence)
Snapshot> snapshot;
RootReference>[] undoLogRootReferences;
do {
snapshot = getSnapshot();
undoLogRootReferences = getTransaction().getUndoLogRootReferences();
} while (!snapshot.equals(getSnapshot()));
RootReference> mapRootReference = snapshot.root;
long size = mapRootReference.getTotalCount();
long undoLogsTotalSize = undoLogRootReferences == null ? size
: TransactionStore.calculateUndoLogsTotalSize(undoLogRootReferences);
// if we are looking at the map without any uncommitted values
if (undoLogsTotalSize == 0) {
return size;
}
return adjustSize(undoLogRootReferences, mapRootReference,
isolationLevel == IsolationLevel.READ_UNCOMMITTED ? null : snapshot.committingTransactions,
size, undoLogsTotalSize);
}
private long adjustSize(RootReference>[] undoLogRootReferences,
RootReference> mapRootReference, BitSet committingTransactions, long size,
long undoLogsTotalSize) {
// Entries describing removals from the map by this transaction and all transactions,
// which are committed but not closed yet,
// and entries about additions to the map by other uncommitted transactions were counted,
// but they should not contribute into total count.
if (2 * undoLogsTotalSize > size) {
// the undo log is larger than half of the map - scan the entries of the map directly
Cursor> cursor = map.cursor(mapRootReference, null, null, false);
while (cursor.hasNext()) {
cursor.next();
VersionedValue> currentValue = cursor.getValue();
assert currentValue != null;
long operationId = currentValue.getOperationId();
if (operationId != 0 && // skip committed entries
isIrrelevant(operationId, currentValue, committingTransactions)) {
--size;
}
}
} else {
assert undoLogRootReferences != null;
// The undo logs are much smaller than the map - scan all undo logs,
// and then lookup relevant map entry.
for (RootReference> undoLogRootReference : undoLogRootReferences) {
if (undoLogRootReference != null) {
Cursor> cursor = undoLogRootReference.root.map.cursor(undoLogRootReference,
null, null, false);
while (cursor.hasNext()) {
cursor.next();
Record,?> op = cursor.getValue();
if (op.mapId == map.getId()) {
@SuppressWarnings("unchecked")
VersionedValue currentValue = map.get(mapRootReference.root, (K)op.key);
// If map entry is not there, then we never counted
// it, in the first place, so skip it.
// This is possible when undo entry exists because
// it belongs to a committed but not yet closed
// transaction, and it was later deleted by some
// other already committed and closed transaction.
if (currentValue != null) {
// only the last undo entry for any given map
// key should be considered
long operationId = cursor.getKey();
assert operationId != 0;
if (currentValue.getOperationId() == operationId &&
isIrrelevant(operationId, currentValue, committingTransactions)) {
--size;
}
}
}
}
}
}
}
return size;
}
private boolean isIrrelevant(long operationId, VersionedValue> currentValue, BitSet committingTransactions) {
Object v;
if (committingTransactions == null) {
v = currentValue.getCurrentValue();
} else {
int txId = TransactionStore.getTransactionId(operationId);
v = txId == transaction.transactionId || committingTransactions.get(txId)
? currentValue.getCurrentValue() : currentValue.getCommittedValue();
}
return v == null;
}
private long sizeAsLongRepeatableReadWithChanges() {
long count = 0L;
RepeatableIterator iterator = new RepeatableIterator<>(this, null, null, false, false);
while (iterator.fetchNext() != null) {
count++;
}
return count;
}
/**
* Remove an entry.
*
* If the row is locked, this method will retry until the row could be
* updated or until a lock timeout.
*
* @param key the key
* @throws MVStoreException if a lock timeout occurs
* @throws ClassCastException if type of the specified key is not compatible with this map
*/
@SuppressWarnings("unchecked")
@Override
public V remove(Object key) {
return set((K)key, (V)null);
}
/**
* Update the value for the given key.
*
* If the row is locked, this method will retry until the row could be
* updated or until a lock timeout.
*
* @param key the key
* @param value the new value (not null)
* @return the old value
* @throws MVStoreException if a lock timeout occurs
*/
@Override
public V put(K key, V value) {
DataUtils.checkArgument(value != null, "The value may not be null");
return set(key, value);
}
/**
* Put the value for the given key if entry for this key does not exist.
* It is atomic equivalent of the following expression:
* contains(key) ? get(k) : put(key, value);
*
* @param key the key
* @param value the new value (not null)
* @return the old value
*/
@Override
public V putIfAbsent(K key, V value) {
DataUtils.checkArgument(value != null, "The value may not be null");
ifAbsentDecisionMaker.initialize(key, value);
V result = set(key, ifAbsentDecisionMaker);
if (ifAbsentDecisionMaker.getDecision() == MVMap.Decision.ABORT) {
result = ifAbsentDecisionMaker.getLastValue();
}
return result;
}
/**
* Appends entry to underlying map. This method may be used concurrently,
* but latest appended values are not guaranteed to be visible.
* @param key should be higher in map's order than any existing key
* @param value to be appended
*/
public void append(K key, V value) {
map.append(key, VersionedValueUncommitted.getInstance(
transaction.log(new Record<>(map.getId(), key, null)), value, null));
hasChanges = true;
}
/**
* Lock row for the given key.
*
* If the row is locked, this method will retry until the row could be
* updated or until a lock timeout.
*
* @param key the key
* @return the locked value
* @throws MVStoreException if a lock timeout occurs
*/
public V lock(K key) {
lockDecisionMaker.initialize(key, null);
return set(key, lockDecisionMaker);
}
/**
* Update the value for the given key, without adding an undo log entry.
*
* @param key the key
* @param value the value
* @return the old value
*/
@SuppressWarnings("UnusedReturnValue")
public V putCommitted(K key, V value) {
DataUtils.checkArgument(value != null, "The value may not be null");
VersionedValue newValue = VersionedValueCommitted.getInstance(value);
VersionedValue oldValue = map.put(key, newValue);
V result = oldValue == null ? null : oldValue.getCurrentValue();
return result;
}
private V set(K key, V value) {
txDecisionMaker.initialize(key, value);
return set(key, txDecisionMaker);
}
private V set(Object key, TxDecisionMaker decisionMaker) {
Transaction blockingTransaction;
VersionedValue result;
String mapName = null;
do {
assert transaction.getBlockerId() == 0;
@SuppressWarnings("unchecked")
K k = (K) key;
// second parameter (value) is not really used,
// since TxDecisionMaker has it embedded
result = map.operate(k, null, decisionMaker);
MVMap.Decision decision = decisionMaker.getDecision();
assert decision != null;
assert decision != MVMap.Decision.REPEAT;
blockingTransaction = decisionMaker.getBlockingTransaction();
if (decision != MVMap.Decision.ABORT || blockingTransaction == null) {
hasChanges |= decision != MVMap.Decision.ABORT;
V res = result == null ? null : result.getCurrentValue();
return res;
}
decisionMaker.reset();
if (mapName == null) {
mapName = map.getName();
}
} while (transaction.waitFor(blockingTransaction, mapName, key));
throw DataUtils.newMVStoreException(DataUtils.ERROR_TRANSACTION_LOCKED,
"Map entry <{0}> with key <{1}> and value {2} is locked by tx {3} and can not be updated by tx {4}"
+ " within allocated time interval {5} ms.",
mapName, key, result, blockingTransaction.transactionId, transaction.transactionId,
transaction.timeoutMillis);
}
/**
* Try to remove the value for the given key.
*
* This will fail if the row is locked by another transaction (that
* means, if another open transaction changed the row).
*
* @param key the key
* @return whether the entry could be removed
*/
public boolean tryRemove(K key) {
return trySet(key, null);
}
/**
* Try to update the value for the given key.
*
* This will fail if the row is locked by another transaction (that
* means, if another open transaction changed the row).
*
* @param key the key
* @param value the new value
* @return whether the entry could be updated
*/
public boolean tryPut(K key, V value) {
DataUtils.checkArgument(value != null, "The value may not be null");
return trySet(key, value);
}
/**
* Try to set or remove the value. When updating only unchanged entries,
* then the value is only changed if it was not changed after opening
* the map.
*
* @param key the key
* @param value the new value (null to remove the value)
* @return true if the value was set, false if there was a concurrent
* update
*/
public boolean trySet(K key, V value) {
try {
// TODO: effective transaction.timeoutMillis should be set to 0 here
// and restored before return
// TODO: eliminate exception usage as part of normal control flaw
set(key, value);
return true;
} catch (MVStoreException e) {
return false;
}
}
/**
* Get the effective value for the given key.
*
* @param key the key
* @return the value or null
* @throws ClassCastException if type of the specified key is not compatible with this map
*/
@SuppressWarnings("unchecked")
@Override
public V get(Object key) {
return getImmediate((K)key);
}
/**
* Get the value for the given key, or null if value does not exist in accordance with transactional rules.
* Value is taken from a snapshot, appropriate for an isolation level of the related transaction
*
* @param key the key
* @return the value, or null if not found
*/
public V getFromSnapshot(K key) {
switch (transaction.isolationLevel) {
case READ_UNCOMMITTED: {
Snapshot> snapshot = getStatementSnapshot();
VersionedValue data = map.get(snapshot.root.root, key);
if (data != null) {
return data.getCurrentValue();
}
return null;
}
case REPEATABLE_READ:
case SNAPSHOT:
case SERIALIZABLE:
if (transaction.hasChanges()) {
Snapshot> snapshot = getStatementSnapshot();
VersionedValue data = map.get(snapshot.root.root, key);
if (data != null) {
long id = data.getOperationId();
if (id != 0L && transaction.transactionId == TransactionStore.getTransactionId(id)) {
return data.getCurrentValue();
}
}
}
//$FALL-THROUGH$
case READ_COMMITTED:
default:
Snapshot> snapshot = getSnapshot();
return getFromSnapshot(snapshot.root, snapshot.committingTransactions, key);
}
}
private V getFromSnapshot(RootReference> rootRef, BitSet committingTransactions, K key) {
VersionedValue data = map.get(rootRef.root, key);
if (data == null) {
// doesn't exist
return null;
}
long id = data.getOperationId();
if (id != 0) {
int tx = TransactionStore.getTransactionId(id);
if (tx != transaction.transactionId && !committingTransactions.get(tx)) {
// added/modified/removed by uncommitted transaction, change should not be visible
return data.getCommittedValue();
}
}
// added/modified/removed by this transaction or another transaction which is committed by now
return data.getCurrentValue();
}
/**
* Get the value for the given key, or null if not found.
* Operation is performed on a snapshot of the map taken during this call.
*
* @param key the key
* @return the value, or null if not found
*/
public V getImmediate(K key) {
return useSnapshot((rootReference, committedTransactions) ->
getFromSnapshot(rootReference, committedTransactions, key));
}
Snapshot> getSnapshot() {
return snapshot == null ? createSnapshot() : snapshot;
}
Snapshot> getStatementSnapshot() {
return statementSnapshot == null ? createSnapshot() : statementSnapshot;
}
void setStatementSnapshot(Snapshot> snapshot) {
statementSnapshot = snapshot;
}
void promoteSnapshot() {
if (snapshot == null) {
snapshot = statementSnapshot;
}
}
/**
* Create a new snapshot for this map.
*
* @return the snapshot
*/
Snapshot> createSnapshot() {
return useSnapshot(Snapshot::new);
}
/**
* Gets a coherent picture of committing transactions and root reference,
* passes it to the specified function, and returns its result.
*
* @param type of the result
*
* @param snapshotConsumer
* function to invoke on a snapshot
* @return function's result
*/
R useSnapshot(BiFunction>, BitSet, R> snapshotConsumer) {
// The purpose of the following loop is to get a coherent picture
// of a state of two independent volatile / atomic variables,
// which they had at some recent moment in time.
// In order to get such a "snapshot", we wait for a moment of silence,
// when neither of the variables concurrently changes it's value.
AtomicReference holder = transaction.store.committingTransactions;
BitSet committingTransactions = holder.get();
while (true) {
BitSet prevCommittingTransactions = committingTransactions;
RootReference> root = map.getRoot();
committingTransactions = holder.get();
if (committingTransactions == prevCommittingTransactions) {
return snapshotConsumer.apply(root, committingTransactions);
}
}
}
/**
* Whether the map contains the key.
*
* @param key the key
* @return true if the map contains an entry for this key
* @throws ClassCastException if type of the specified key is not compatible with this map
*/
@SuppressWarnings("unchecked")
@Override
public boolean containsKey(Object key) {
return getImmediate((K)key) != null;
}
/**
* Check if the row was deleted by this transaction.
*
* @param key the key
* @return {@code true} if it was
*/
public boolean isDeletedByCurrentTransaction(K key) {
VersionedValue data = map.get(key);
if (data != null) {
long id = data.getOperationId();
return id != 0 && TransactionStore.getTransactionId(id) == transaction.transactionId
&& data.getCurrentValue() == null;
}
return false;
}
/**
* Whether the entry for this key was added or removed from this
* session.
*
* @param key the key
* @return true if yes
*/
public boolean isSameTransaction(K key) {
VersionedValue data = map.get(key);
if (data == null) {
// doesn't exist or deleted by a committed transaction
return false;
}
int tx = TransactionStore.getTransactionId(data.getOperationId());
return tx == transaction.transactionId;
}
/**
* Check whether this map is closed.
*
* @return true if closed
*/
public boolean isClosed() {
return map.isClosed();
}
/**
* Clear the map.
*/
@Override
public void clear() {
// TODO truncate transactionally?
map.clear();
hasChanges = true;
}
@Override
public Set> entrySet() {
return new AbstractSet>() {
@Override
public Iterator> iterator() {
return entryIterator(null, null);
}
@Override
public int size() {
return TransactionMap.this.size();
}
@Override
public boolean contains(Object o) {
return TransactionMap.this.containsKey(o);
}
};
}
/**
* Get the first entry.
*
* @return the first entry, or null if empty
*/
public Entry firstEntry() {
return this.>chooseIterator(null, null, false, true).fetchNext();
}
/**
* Get the first key.
*
* @return the first key, or null if empty
*/
public K firstKey() {
return this.chooseIterator(null, null, false, false).fetchNext();
}
/**
* Get the last entry.
*
* @return the last entry, or null if empty
*/
public Entry lastEntry() {
return this.>chooseIterator(null, null, true, true).fetchNext();
}
/**
* Get the last key.
*
* @return the last key, or null if empty
*/
public K lastKey() {
return this.chooseIterator(null, null, true, false).fetchNext();
}
/**
* Get the entry with smallest key that is larger than the given key, or null if no
* such key exists.
*
* @param key the key (may not be null)
* @return the result
*/
public Entry higherEntry(K key) {
return higherLowerEntry(key, false);
}
/**
* Get the smallest key that is larger than the given key, or null if no
* such key exists.
*
* @param key the key (may not be null)
* @return the result
*/
public K higherKey(K key) {
return higherLowerKey(key, false);
}
/**
* Get the entry with smallest key that is larger than or equal to this key,
* or null if no such key exists.
*
* @param key the key (may not be null)
* @return the result
*/
public Entry ceilingEntry(K key) {
return this.>chooseIterator(key, null, false, true).fetchNext();
}
/**
* Get the smallest key that is larger than or equal to this key,
* or null if no such key exists.
*
* @param key the key (may not be null)
* @return the result
*/
public K ceilingKey(K key) {
return this.chooseIterator(key, null, false, false).fetchNext();
}
/**
* Get the entry with largest key that is smaller than or equal to this key,
* or null if no such key exists.
*
* @param key the key (may not be null)
* @return the result
*/
public Entry floorEntry(K key) {
return this.>chooseIterator(key, null, true, true).fetchNext();
}
/**
* Get the largest key that is smaller than or equal to this key,
* or null if no such key exists.
*
* @param key the key (may not be null)
* @return the result
*/
public K floorKey(K key) {
return this.chooseIterator(key, null, true, false).fetchNext();
}
/**
* Get the entry with largest key that is smaller than the given key, or null if no
* such key exists.
*
* @param key the key (may not be null)
* @return the result
*/
public Entry lowerEntry(K key) {
return higherLowerEntry(key, true);
}
/**
* Get the largest key that is smaller than the given key, or null if no
* such key exists.
*
* @param key the key (may not be null)
* @return the result
*/
public K lowerKey(K key) {
return higherLowerKey(key, true);
}
private Entry higherLowerEntry(K key, boolean lower) {
TMIterator> it = chooseIterator(key, null, lower, true);
Entry result = it.fetchNext();
if (result != null && map.getKeyType().compare(key, result.getKey()) == 0) {
result = it.fetchNext();
}
return result;
}
private K higherLowerKey(K key, boolean lower) {
TMIterator it = chooseIterator(key, null, lower, false);
K result = it.fetchNext();
if (result != null && map.getKeyType().compare(key, result) == 0) {
result = it.fetchNext();
}
return result;
}
/**
* Iterate over keys.
*
* @param from the first key to return
* @return the iterator
*/
public Iterator keyIterator(K from) {
return chooseIterator(from, null, false, false);
}
/**
* Iterate over keys in the specified order.
*
* @param from the first key to return
* @param reverse if true, iterate in reverse (descending) order
* @return the iterator
*/
public TMIterator keyIterator(K from, boolean reverse) {
return chooseIterator(from, null, reverse, false);
}
/**
* Iterate over keys.
*
* @param from the first key to return
* @param to the last key to return or null if there is no limit
* @return the iterator
*/
public TMIterator keyIterator(K from, K to) {
return chooseIterator(from, to, false, false);
}
/**
* Iterate over keys, including keys from uncommitted entries.
*
* @param from the first key to return
* @param to the last key to return or null if there is no limit
* @return the iterator
*/
public TMIterator keyIteratorUncommitted(K from, K to) {
return new ValidationIterator<>(this, from, to);
}
/**
* Iterate over entries.
*
* @param from the first key to return
* @param to the last key to return
* @return the iterator
*/
public TMIterator> entryIterator(final K from, final K to) {
return chooseIterator(from, to, false, true);
}
private TMIterator chooseIterator(K from, K to, boolean reverse, boolean forEntries) {
switch (transaction.isolationLevel) {
case READ_UNCOMMITTED:
return new UncommittedIterator<>(this, from, to, reverse, forEntries);
case REPEATABLE_READ:
case SNAPSHOT:
case SERIALIZABLE:
if (hasChanges) {
return new RepeatableIterator<>(this, from, to, reverse, forEntries);
}
//$FALL-THROUGH$
case READ_COMMITTED:
default:
return new CommittedIterator<>(this, from, to, reverse, forEntries);
}
}
public Transaction getTransaction() {
return transaction;
}
public DataType getKeyType() {
return map.getKeyType();
}
/**
* The iterator for read uncommitted isolation level. This iterator is also
* used for unique indexes.
*
* @param
* the type of keys
* @param
* the type of elements
*/
private static class UncommittedIterator extends TMIterator {
UncommittedIterator(TransactionMap transactionMap, K from, K to, boolean reverse, boolean forEntries) {
super(transactionMap, from, to, transactionMap.createSnapshot(), reverse, forEntries);
}
UncommittedIterator(TransactionMap transactionMap, K from, K to, Snapshot> snapshot,
boolean reverse, boolean forEntries) {
super(transactionMap, from, to, snapshot, reverse, forEntries);
}
@Override
public final X fetchNext() {
while (cursor.hasNext()) {
K key = cursor.next();
VersionedValue> data = cursor.getValue();
if (data != null) {
Object currentValue = data.getCurrentValue();
if (currentValue != null || shouldIgnoreRemoval(data)) {
return toElement(key, currentValue);
}
}
}
return null;
}
boolean shouldIgnoreRemoval(VersionedValue> data) {
return false;
}
}
// This iterator should include all entries applicable for unique index validation,
// committed and otherwise, only excluding keys removed by the current transaction
// or by some other already committed (but not closed yet) transactions
private static final class ValidationIterator extends UncommittedIterator {
ValidationIterator(TransactionMap transactionMap, K from, K to) {
super(transactionMap, from, to, transactionMap.createSnapshot(), false, false);
}
@Override
boolean shouldIgnoreRemoval(VersionedValue> data) {
assert data.getCurrentValue() == null;
long id = data.getOperationId();
if (id != 0) {
int tx = TransactionStore.getTransactionId(id);
return transactionId != tx && !committingTransactions.get(tx);
}
return false;
}
}
/**
* The iterator for read committed isolation level. Can also be used on
* higher levels when the transaction doesn't have own changes.
*
* @param
* the type of keys
* @param
* the type of elements
*/
private static final class CommittedIterator extends TMIterator {
CommittedIterator(TransactionMap transactionMap, K from, K to, boolean reverse, boolean forEntries) {
super(transactionMap, from, to, transactionMap.getSnapshot(), reverse, forEntries);
}
@Override
public X fetchNext() {
while (cursor.hasNext()) {
K key = cursor.next();
VersionedValue> data = cursor.getValue();
// If value doesn't exist or it was deleted by a committed transaction,
// or if value is a committed one, just return it.
if (data != null) {
long id = data.getOperationId();
if (id != 0) {
int tx = TransactionStore.getTransactionId(id);
if (tx != transactionId && !committingTransactions.get(tx)) {
// current value comes from another uncommitted transaction
// take committed value instead
Object committedValue = data.getCommittedValue();
if (committedValue == null) {
continue;
}
return toElement(key, committedValue);
}
}
Object currentValue = data.getCurrentValue();
if (currentValue != null) {
return toElement(key, currentValue);
}
}
}
return null;
}
}
/**
* The iterator for repeatable read and serializable isolation levels.
*
* @param
* the type of keys
* @param
* the type of elements
*/
private static final class RepeatableIterator extends TMIterator {
private final DataType keyType;
private K snapshotKey;
private Object snapshotValue;
private final Cursor> uncommittedCursor;
private K uncommittedKey;
private V uncommittedValue;
RepeatableIterator(TransactionMap transactionMap, K from, K to, boolean reverse, boolean forEntries) {
super(transactionMap, from, to, transactionMap.getSnapshot(), reverse, forEntries);
keyType = transactionMap.map.getKeyType();
Snapshot> snapshot = transactionMap.getStatementSnapshot();
uncommittedCursor = transactionMap.map.cursor(snapshot.root, from, to, reverse);
}
@Override
public X fetchNext() {
X next = null;
do {
if (snapshotKey == null) {
fetchSnapshot();
}
if (uncommittedKey == null) {
fetchUncommitted();
}
if (snapshotKey == null && uncommittedKey == null) {
break;
}
int cmp = snapshotKey == null ? 1 :
uncommittedKey == null ? -1 :
keyType.compare(snapshotKey, uncommittedKey);
if (cmp < 0) {
next = toElement(snapshotKey, snapshotValue);
snapshotKey = null;
break;
}
if (uncommittedValue != null) {
// This entry was added / updated by this transaction, use the new value
next = toElement(uncommittedKey, uncommittedValue);
}
if (cmp == 0) { // This entry was updated / deleted
snapshotKey = null;
}
uncommittedKey = null;
} while (next == null);
return next;
}
private void fetchSnapshot() {
while (cursor.hasNext()) {
K key = cursor.next();
VersionedValue> data = cursor.getValue();
// If value doesn't exist or it was deleted by a committed transaction,
// or if value is a committed one, just return it.
if (data != null) {
Object value = data.getCommittedValue();
long id = data.getOperationId();
if (id != 0) {
int tx = TransactionStore.getTransactionId(id);
if (tx == transactionId || committingTransactions.get(tx)) {
// value comes from this transaction or another committed transaction
// take current value instead instead of committed one
value = data.getCurrentValue();
}
}
if (value != null) {
snapshotKey = key;
snapshotValue = value;
return;
}
}
}
}
private void fetchUncommitted() {
while (uncommittedCursor.hasNext()) {
K key = uncommittedCursor.next();
VersionedValue data = uncommittedCursor.getValue();
if (data != null) {
long id = data.getOperationId();
if (id != 0L && transactionId == TransactionStore.getTransactionId(id)) {
uncommittedKey = key;
uncommittedValue = data.getCurrentValue();
return;
}
}
}
}
}
public abstract static class TMIterator implements Iterator {
final int transactionId;
final BitSet committingTransactions;
protected final Cursor> cursor;
private final boolean forEntries;
X current;
TMIterator(TransactionMap transactionMap, K from, K to, Snapshot> snapshot,
boolean reverse, boolean forEntries) {
Transaction transaction = transactionMap.getTransaction();
this.transactionId = transaction.transactionId;
this.forEntries = forEntries;
this.cursor = transactionMap.map.cursor(snapshot.root, from, to, reverse);
this.committingTransactions = snapshot.committingTransactions;
}
@SuppressWarnings("unchecked")
final X toElement(K key, Object value) {
return (X) (forEntries ? new AbstractMap.SimpleImmutableEntry<>(key, value) : key);
}
/**
* Fetches a next entry.
*
* This method cannot be used together with {@link #hasNext()} and
* {@link #next()}.
*
* @return the next entry or {@code null}
*/
public abstract X fetchNext();
@Override
public final boolean hasNext() {
return current != null || (current = fetchNext()) != null;
}
@Override
public final X next() {
X result = current;
if (result == null) {
if ((result = fetchNext()) == null) {
throw new NoSuchElementException();
}
} else {
current = null;
}
return result;
}
}
}