All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.h2.mvstore.tx.TransactionMap Maven / Gradle / Ivy

There is a newer version: 1.0.0-beta2
Show newest version
/*
 * Copyright 2004-2019 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 org.h2.engine.IsolationLevel;
import org.h2.mvstore.Cursor;
import org.h2.mvstore.DataUtils;
import org.h2.mvstore.MVMap;
import org.h2.mvstore.Page;
import org.h2.mvstore.RootReference;
import org.h2.mvstore.type.DataType;
import org.h2.value.VersionedValue;

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;

/**
 * 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 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; TransactionMap(Transaction transaction, MVMap map) { this.transaction = transaction; this.map = map; } /** * Get a clone of this map for the given transaction. * * @param transaction the transaction * @return the map */ public TransactionMap getInstance(Transaction transaction) { return new TransactionMap<>(transaction, 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 final 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() { if (transaction.isolationLevel != IsolationLevel.READ_COMMITTED) { return sizeAsLongSlow(); } // 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; BitSet committingTransactions = snapshot.committingTransactions; Page mapRootPage = mapRootReference.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; } // 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 = new Cursor<>(mapRootPage, null); 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 = new Cursor<>(undoLogRootReference.root, null); while (cursor.hasNext()) { cursor.next(); Object[] op = cursor.getValue(); if ((int) op[0] == map.getId()) { VersionedValue currentValue = map.get(mapRootPage, op[1]); // 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 long sizeAsLongSlow() { long count = 0L; Iterator iterator = keyIterator(null, null); while (iterator.hasNext()) { iterator.next(); count++; } return count; } private boolean isIrrelevant(long operationId, VersionedValue currentValue, BitSet committingTransactions) { int txId = TransactionStore.getTransactionId(operationId); boolean isVisible = txId == transaction.transactionId || committingTransactions.get(txId); Object v = isVisible ? currentValue.getCurrentValue() : currentValue.getCommittedValue(); return v == null; } /** * 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 IllegalStateException if a lock timeout occurs * @throws ClassCastException if type of the specified key is not compatible with this map */ @Override public V remove(Object key) { return set(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 IllegalStateException 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 */ // Do not add @Override, code should be compatible with Java 7 public V putIfAbsent(K key, V value) { DataUtils.checkArgument(value != null, "The value may not be null"); TxDecisionMaker decisionMaker = new TxDecisionMaker.PutIfAbsentDecisionMaker(map.getId(), key, value, transaction); return set(key, decisionMaker); } /** * 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(map.getId(), key, null), value, null)); } /** * 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 IllegalStateException if a lock timeout occurs */ public V lock(K key) { TxDecisionMaker decisionMaker = new TxDecisionMaker.LockDecisionMaker(map.getId(), key, transaction); return set(key, decisionMaker); } /** * 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 */ 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); @SuppressWarnings("unchecked") V result = (V) (oldValue == null ? null : oldValue.getCurrentValue()); return result; } private V set(Object key, V value) { TxDecisionMaker decisionMaker = new TxDecisionMaker(map.getId(), key, value, transaction); return set(key, decisionMaker); } private V set(Object key, TxDecisionMaker decisionMaker) { TransactionStore store = transaction.store; Transaction blockingTransaction; long sequenceNumWhenStarted; VersionedValue result; do { sequenceNumWhenStarted = store.openTransactions.get().getVersion(); assert transaction.getBlockerId() == 0; // although second parameter (value) is not really used, // since TxDecisionMaker has it embedded, // MVRTreeMap has weird traversal logic based on it, // and any non-null value will do @SuppressWarnings("unchecked") K k = (K) key; result = map.operate(k, VersionedValue.DUMMY, decisionMaker); MVMap.Decision decision = decisionMaker.getDecision(); assert decision != null; assert decision != MVMap.Decision.REPEAT; blockingTransaction = decisionMaker.getBlockingTransaction(); if (decision != MVMap.Decision.ABORT || blockingTransaction == null) { @SuppressWarnings("unchecked") V res = result == null ? null : (V) result.getCurrentValue(); return res; } decisionMaker.reset(); } while (blockingTransaction.sequenceNum > sequenceNumWhenStarted || transaction.waitFor(blockingTransaction, map, key)); throw DataUtils.newIllegalStateException(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.", map.getName(), 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 (IllegalStateException 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 */ @Override public V get(Object key) { return getImmediate(key); } /** * Get the value for the given key from a snapshot, or null if not found. * * @param key the key * @return the value, or null if not found */ @SuppressWarnings("unchecked") public V getFromSnapshot(Object key) { switch (transaction.isolationLevel) { case READ_UNCOMMITTED: { Snapshot snapshot = getStatementSnapshot(); VersionedValue data = map.get(snapshot.root.root, key); if (data != null) { return (V) 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 (V) data.getCurrentValue(); } } } //$FALL-THROUGH$ case READ_COMMITTED: default: Snapshot snapshot = getSnapshot(); VersionedValue data = map.get(snapshot.root.root, key); if (data == null) { // doesn't exist or deleted by a committed transaction return null; } long id = data.getOperationId(); if (id != 0) { int tx = TransactionStore.getTransactionId(id); if (tx != transaction.transactionId && !snapshot.committingTransactions.get(tx)) { return (V) data.getCommittedValue(); } } // added by this transaction or another transaction which is committed by now return (V) data.getCurrentValue(); } } /** * Get the value for the given key, or null if not found. * * @param key the key * @return the value, or null if not found */ @SuppressWarnings("unchecked") public V getImmediate(Object key) { VersionedValue data = map.get(key); if (data == null) { // doesn't exist or deleted by a committed transaction return null; } long id = data.getOperationId(); if (id == 0) { // it is committed return (V)data.getCurrentValue(); } int tx = TransactionStore.getTransactionId(id); if (tx == transaction.transactionId || transaction.store.committingTransactions.get().get(tx)) { // added by this transaction or another transaction which is committed by now return (V) data.getCurrentValue(); } else { return (V) data.getCommittedValue(); } } Snapshot getSnapshot() { return transaction.getSnapshot(map.getId()); } Snapshot getStatementSnapshot() { return transaction.getStatementSnapshot(map.getId()); } /** * Create a new snapshot for this map. * * @return the snapshot */ Snapshot createSnapshot() { return transaction.createSnapshot(map.getId()); } /** * 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 */ @Override public boolean containsKey(Object key) { return getImmediate(key) != null; } /** * 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(); } @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 key. * * @return the first key, or null if empty */ public K firstKey() { Iterator it = keyIterator(null); return it.hasNext() ? it.next() : null; } /** * Get the last key. * * @return the last key, or null if empty */ public K lastKey() { RootReference rootReference = getSnapshot().root; K k = map.lastKey(rootReference.root); while (k != null && getFromSnapshot(k) == null) { k = map.lowerKey(rootReference.root, k); } return k; } /** * 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) { RootReference rootReference = getSnapshot().root; do { key = map.higherKey(rootReference.root, key); } while (key != null && getFromSnapshot(key) == null); return key; } /** * 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) { Iterator it = keyIterator(key); return it.hasNext() ? it.next() : null; } /** * 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) { RootReference rootReference = getSnapshot().root; key = map.floorKey(rootReference.root, key); while (key != null && getFromSnapshot(key) == null) { // Use lowerKey() for the next attempts, otherwise we'll get an infinite loop key = map.lowerKey(rootReference.root, key); } return key; } /** * 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) { RootReference rootReference = getSnapshot().root; do { key = map.lowerKey(rootReference.root, key); } while (key != null && getFromSnapshot(key) == null); return key; } /** * Iterate over keys. * * @param from the first key to return * @return the iterator */ public Iterator keyIterator(K from) { return keyIterator(from, null); } /** * 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 Iterator keyIterator(K from, K to) { return chooseIterator(from, to, 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 Iterator 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 Iterator> entryIterator(final K from, final K to) { return chooseIterator(from, to, true); } private Iterator chooseIterator(K from, K to, boolean forEntries) { switch (transaction.isolationLevel) { case READ_UNCOMMITTED: return new UncommittedIterator<>(this, from, to, forEntries); case REPEATABLE_READ: case SNAPSHOT: case SERIALIZABLE: if (transaction.hasChanges()) { return new RepeatableIterator<>(this, from, to, forEntries); } //$FALL-THROUGH$ case READ_COMMITTED: default: return new CommittedIterator<>(this, from, to, 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 forEntries) { super(transactionMap, from, to, transactionMap.getStatementSnapshot(), forEntries); fetchNext(); } UncommittedIterator(TransactionMap transactionMap, K from, K to, Snapshot snapshot, boolean forEntries) { super(transactionMap, from, to, snapshot, forEntries); fetchNext(); } @Override final void fetchNext() { while (cursor.hasNext()) { K key = cursor.next(); VersionedValue data = cursor.getValue(); if (data != null) { Object currentValue = data.getCurrentValue(); if (currentValue != null || isApplicable(data)) { registerCurrent(key, currentValue); return; } } } current = null; } boolean isApplicable(VersionedValue data) { return false; } } private static final class ValidationIterator extends UncommittedIterator { ValidationIterator(TransactionMap transactionMap, K from, K to) { super(transactionMap, from, to, transactionMap.createSnapshot(), false); } @Override boolean isApplicable(VersionedValue data) { // Include all uncommitted entries for unique index validation 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 forEntries) { super(transactionMap, from, to, transactionMap.getSnapshot(), forEntries); fetchNext(); } @Override void 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; } registerCurrent(key, committedValue); return; } } Object currentValue = data.getCurrentValue(); if (currentValue != null) { registerCurrent(key, currentValue); return; } } } current = 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 Object uncommittedValue; RepeatableIterator(TransactionMap transactionMap, K from, K to, boolean forEntries) { super(transactionMap, from, to, transactionMap.getSnapshot(), forEntries); keyType = transactionMap.map.getKeyType(); Snapshot snapshot = transactionMap.getStatementSnapshot(); uncommittedCursor = new Cursor<>(snapshot.root.root, from, to); fetchNext(); } @Override void fetchNext() { current = 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) { registerCurrent(snapshotKey, snapshotValue); snapshotKey = null; break; } if (uncommittedValue != null) { // This entry was added / updated by this transaction, use the new value registerCurrent(uncommittedKey, uncommittedValue); } if (cmp == 0) { // This entry was updated / deleted snapshotKey = null; } uncommittedKey = null; } while (current == null); } 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; } } } } } private 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 forEntries) { Transaction transaction = transactionMap.getTransaction(); this.transactionId = transaction.transactionId; this.forEntries = forEntries; this.cursor = new Cursor<>(snapshot.root.root, from, to); this.committingTransactions = snapshot.committingTransactions; } @SuppressWarnings("unchecked") final void registerCurrent(K key, Object value) { current = (X) (forEntries ? new AbstractMap.SimpleImmutableEntry<>(key, value) : key); } abstract void fetchNext(); @Override public final boolean hasNext() { return current != null; } @Override public final X next() { if (current == null) { throw new NoSuchElementException(); } X result = current; fetchNext(); return result; } @Override public final void remove() { throw DataUtils.newUnsupportedOperationException( "Removal is not supported"); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy