org.h2.mvstore.MVMap Maven / Gradle / Ivy
/*
* 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;
import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import org.h2.mvstore.type.DataType;
import org.h2.mvstore.type.ObjectDataType;
import org.h2.mvstore.type.StringDataType;
/**
* A stored map.
*
* All read and write operations can happen concurrently with all other
* operations, without risk of corruption.
*
* @param the key class
* @param the value class
*/
public class MVMap extends AbstractMap
implements ConcurrentMap
{
/**
* The store.
*/
public final MVStore store;
/**
* Reference to the current root page.
*/
private final AtomicReference root;
private final int id;
private final long createVersion;
private final DataType keyType;
private final DataType valueType;
private final int keysPerPage;
private final boolean singleWriter;
private final K[] keysBuffer;
private final V[] valuesBuffer;
private final Object lock = new Object();
private volatile boolean notificationRequested;
/**
* Whether the map is closed. Volatile so we don't accidentally write to a
* closed map in multithreaded mode.
*/
private volatile boolean closed;
private boolean readOnly;
private boolean isVolatile;
/**
* This designates the "last stored" version for a store which was
* just open for the first time.
*/
static final long INITIAL_VERSION = -1;
protected MVMap(Map config) {
this((MVStore) config.get("store"),
(DataType) config.get("key"),
(DataType) config.get("val"),
DataUtils.readHexInt(config, "id", 0),
DataUtils.readHexLong(config, "createVersion", 0),
new AtomicReference(),
((MVStore) config.get("store")).getKeysPerPage(),
config.containsKey("singleWriter") && (Boolean) config.get("singleWriter")
);
setInitialRoot(createEmptyLeaf(), store.getCurrentVersion());
}
// constructor for cloneIt()
protected MVMap(MVMap source) {
this(source.store, source.keyType, source.valueType, source.id, source.createVersion,
new AtomicReference<>(source.root.get()), source.keysPerPage, source.singleWriter);
}
// meta map constructor
MVMap(MVStore store) {
this(store, StringDataType.INSTANCE,StringDataType.INSTANCE, 0, 0, new AtomicReference(),
store.getKeysPerPage(), false);
setInitialRoot(createEmptyLeaf(), store.getCurrentVersion());
}
@SuppressWarnings("unchecked")
private MVMap(MVStore store, DataType keyType, DataType valueType, int id, long createVersion,
AtomicReference root, int keysPerPage, boolean singleWriter) {
this.store = store;
this.id = id;
this.createVersion = createVersion;
this.keyType = keyType;
this.valueType = valueType;
this.root = root;
this.keysPerPage = keysPerPage;
this.keysBuffer = singleWriter ? (K[]) new Object[keysPerPage] : null;
this.valuesBuffer = singleWriter ? (V[]) new Object[keysPerPage] : null;
this.singleWriter = singleWriter;
}
/**
* Clone the current map.
*
* @return clone of this.
*/
protected MVMap cloneIt() {
return new MVMap<>(this);
}
/**
* Get the metadata key for the root of the given map id.
*
* @param mapId the map id
* @return the metadata key
*/
static String getMapRootKey(int mapId) {
return DataUtils.META_ROOT + Integer.toHexString(mapId);
}
/**
* Get the metadata key for the given map id.
*
* @param mapId the map id
* @return the metadata key
*/
static String getMapKey(int mapId) {
return DataUtils.META_MAP + Integer.toHexString(mapId);
}
/**
* Add or replace a key-value pair.
*
* @param key the key (may not be null)
* @param value the value (may not be null)
* @return the old value if the key existed, or null otherwise
*/
@Override
public V put(K key, V value) {
DataUtils.checkArgument(value != null, "The value may not be null");
return operate(key, value, DecisionMaker.PUT);
}
/**
* Get the first key, or null if the map is empty.
*
* @return the first key, or null
*/
public final K firstKey() {
return getFirstLast(true);
}
/**
* Get the first key of this page.
*
* @param p the page
* @return the key, or null
*/
public final K firstKey(Page p) {
return getFirstLast(p, true);
}
/**
* Get the last key, or null if the map is empty.
*
* @return the last key, or null
*/
public final K lastKey() {
return getFirstLast(false);
}
/**
* Get the last key of this page.
*
* @param p the page
* @return the key, or null
*/
public final K lastKey(Page p) {
return getFirstLast(p, false);
}
/**
* Get the key at the given index.
*
* This is a O(log(size)) operation.
*
* @param index the index
* @return the key
*/
public final K getKey(long index) {
if (index < 0 || index >= sizeAsLong()) {
return null;
}
Page p = getRootPage();
long offset = 0;
while (true) {
if (p.isLeaf()) {
if (index >= offset + p.getKeyCount()) {
return null;
}
@SuppressWarnings("unchecked")
K key = (K) p.getKey((int) (index - offset));
return key;
}
int i = 0, size = getChildPageCount(p);
for (; i < size; i++) {
long c = p.getCounts(i);
if (index < c + offset) {
break;
}
offset += c;
}
if (i == size) {
return null;
}
p = p.getChildPage(i);
}
}
/**
* Get the key list. The list is a read-only representation of all keys.
*
* The get and indexOf methods are O(log(size)) operations. The result of
* indexOf is cast to an int.
*
* @return the key list
*/
public final List keyList() {
return new AbstractList() {
@Override
public K get(int index) {
return getKey(index);
}
@Override
public int size() {
return MVMap.this.size();
}
@Override
@SuppressWarnings("unchecked")
public int indexOf(Object key) {
return (int) getKeyIndex((K) key);
}
};
}
/**
* Get the index of the given key in the map.
*
* This is a O(log(size)) operation.
*
* If the key was found, the returned value is the index in the key array.
* If not found, the returned value is negative, where -1 means the provided
* key is smaller than any keys. See also Arrays.binarySearch.
*
* @param key the key
* @return the index
*/
public final long getKeyIndex(K key) {
Page p = getRootPage();
if (p.getTotalCount() == 0) {
return -1;
}
long offset = 0;
while (true) {
int x = p.binarySearch(key);
if (p.isLeaf()) {
if (x < 0) {
offset = -offset;
}
return offset + x;
}
if (x++ < 0) {
x = -x;
}
for (int i = 0; i < x; i++) {
offset += p.getCounts(i);
}
p = p.getChildPage(x);
}
}
/**
* Get the first (lowest) or last (largest) key.
*
* @param first whether to retrieve the first key
* @return the key, or null if the map is empty
*/
private K getFirstLast(boolean first) {
Page p = getRootPage();
return getFirstLast(p, first);
}
@SuppressWarnings("unchecked")
private K getFirstLast(Page p, boolean first) {
if (p.getTotalCount() == 0) {
return null;
}
while (true) {
if (p.isLeaf()) {
return (K) p.getKey(first ? 0 : p.getKeyCount() - 1);
}
p = p.getChildPage(first ? 0 : getChildPageCount(p) - 1);
}
}
/**
* Get the smallest key that is larger than the given key, or null if no
* such key exists.
*
* @param key the key
* @return the result
*/
public final K higherKey(K key) {
return getMinMax(key, false, true);
}
/**
* Get the smallest key that is larger than the given key, for the given
* root page, or null if no such key exists.
*
* @param p the root page
* @param key the key
* @return the result
*/
public final K higherKey(Page p, K key) {
return getMinMax(p, key, false, true);
}
/**
* Get the smallest key that is larger or equal to this key.
*
* @param key the key
* @return the result
*/
public final K ceilingKey(K key) {
return getMinMax(key, false, false);
}
/**
* Get the smallest key that is larger or equal to this key, for the given root page.
*
* @param p the root page
* @param key the key
* @return the result
*/
public final K ceilingKey(Page p, K key) {
return getMinMax(p, key, false, false);
}
/**
* Get the largest key that is smaller or equal to this key.
*
* @param key the key
* @return the result
*/
public final K floorKey(K key) {
return getMinMax(key, true, false);
}
/**
* Get the largest key that is smaller or equal to this key, for the given root page.
*
* @param p the root page
* @param key the key
* @return the result
*/
public final K floorKey(Page p, K key) {
return getMinMax(p, key, true, false);
}
/**
* Get the largest key that is smaller than the given key, or null if no
* such key exists.
*
* @param key the key
* @return the result
*/
public final K lowerKey(K key) {
return getMinMax(key, true, true);
}
/**
* Get the largest key that is smaller than the given key, for the given
* root page, or null if no such key exists.
*
* @param p the root page
* @param key the key
* @return the result
*/
public final K lowerKey(Page p, K key) {
return getMinMax(p, key, true, true);
}
/**
* Get the smallest or largest key using the given bounds.
*
* @param key the key
* @param min whether to retrieve the smallest key
* @param excluding if the given upper/lower bound is exclusive
* @return the key, or null if no such key exists
*/
private K getMinMax(K key, boolean min, boolean excluding) {
return getMinMax(getRootPage(), key, min, excluding);
}
@SuppressWarnings("unchecked")
private K getMinMax(Page p, K key, boolean min, boolean excluding) {
int x = p.binarySearch(key);
if (p.isLeaf()) {
if (x < 0) {
x = -x - (min ? 2 : 1);
} else if (excluding) {
x += min ? -1 : 1;
}
if (x < 0 || x >= p.getKeyCount()) {
return null;
}
return (K) p.getKey(x);
}
if (x++ < 0) {
x = -x;
}
while (true) {
if (x < 0 || x >= getChildPageCount(p)) {
return null;
}
K k = getMinMax(p.getChildPage(x), key, min, excluding);
if (k != null) {
return k;
}
x += min ? -1 : 1;
}
}
/**
* Get the value for the given key, or null if not found.
*
* @param key the key
* @return the value, or null if not found
* @throws ClassCastException if type of the specified key is not compatible with this map
*/
@Override
public final V get(Object key) {
return get(getRootPage(), key);
}
/**
* Get the value for the given key from a snapshot, or null if not found.
*
* @param p the root of a snapshot
* @param key the key
* @return the value, or null if not found
* @throws ClassCastException if type of the specified key is not compatible with this map
*/
@SuppressWarnings("unchecked")
public V get(Page p, Object key) {
return (V) Page.get(p, key);
}
@Override
public final boolean containsKey(Object key) {
return get(key) != null;
}
/**
* Remove all entries.
*/
@Override
public void clear() {
clearIt();
}
/**
* Remove all entries and return the root reference.
*
* @return the new root reference
*/
RootReference clearIt() {
Page emptyRootPage = createEmptyLeaf();
int attempt = 0;
while (true) {
RootReference rootReference = flushAndGetRoot();
if (rootReference.getTotalCount() == 0) {
return rootReference;
}
boolean locked = rootReference.isLockedByCurrentThread();
if (!locked) {
if (attempt++ == 0) {
beforeWrite();
} else if (attempt > 3 || rootReference.isLocked()) {
rootReference = lockRoot(rootReference, attempt);
locked = true;
}
}
Page rootPage = rootReference.root;
long version = rootReference.version;
try {
if (!locked) {
rootReference = rootReference.updateRootPage(emptyRootPage, attempt);
if (rootReference == null) {
continue;
}
}
store.registerUnsavedMemory(rootPage.removeAllRecursive(version));
rootPage = emptyRootPage;
return rootReference;
} finally {
if(locked) {
unlockRoot(rootPage);
}
}
}
}
/**
* Close the map. Accessing the data is still possible (to allow concurrent
* reads), but it is marked as closed.
*/
final void close() {
closed = true;
}
public final boolean isClosed() {
return closed;
}
/**
* Remove a key-value pair, if the key exists.
*
* @param key the key (may not be null)
* @return the old value if the key existed, or null otherwise
* @throws ClassCastException if type of the specified key is not compatible with this map
*/
@Override
@SuppressWarnings("unchecked")
public V remove(Object key) {
return operate((K)key, null, DecisionMaker.REMOVE);
}
/**
* Add a key-value pair if it does not yet exist.
*
* @param key the key (may not be null)
* @param value the new value
* @return the old value if the key existed, or null otherwise
*/
@Override
public final V putIfAbsent(K key, V value) {
return operate(key, value, DecisionMaker.IF_ABSENT);
}
/**
* Remove a key-value pair if the value matches the stored one.
*
* @param key the key (may not be null)
* @param value the expected value
* @return true if the item was removed
*/
@SuppressWarnings("unchecked")
@Override
public boolean remove(Object key, Object value) {
EqualsDecisionMaker decisionMaker = new EqualsDecisionMaker<>(valueType, (V)value);
operate((K)key, null, decisionMaker);
return decisionMaker.getDecision() != Decision.ABORT;
}
/**
* Check whether the two values are equal.
*
* @param a the first value
* @param b the second value
* @param datatype to use for comparison
* @return true if they are equal
*/
static boolean areValuesEqual(DataType datatype, Object a, Object b) {
return a == b
|| a != null && b != null && datatype.compare(a, b) == 0;
}
/**
* Replace a value for an existing key, if the value matches.
*
* @param key the key (may not be null)
* @param oldValue the expected value
* @param newValue the new value
* @return true if the value was replaced
*/
@Override
public final boolean replace(K key, V oldValue, V newValue) {
EqualsDecisionMaker decisionMaker = new EqualsDecisionMaker<>(valueType, oldValue);
V result = operate(key, newValue, decisionMaker);
boolean res = decisionMaker.getDecision() != Decision.ABORT;
assert !res || areValuesEqual(valueType, oldValue, result) : oldValue + " != " + result;
return res;
}
private boolean rewrite(K key) {
ContainsDecisionMaker decisionMaker = new ContainsDecisionMaker<>();
V result = operate(key, null, decisionMaker);
boolean res = decisionMaker.getDecision() != Decision.ABORT;
assert res == (result != null);
return res;
}
/**
* Replace a value for an existing key.
*
* @param key the key (may not be null)
* @param value the new value
* @return the old value, if the value was replaced, or null
*/
@Override
public final V replace(K key, V value) {
return operate(key, value, DecisionMaker.IF_PRESENT);
}
/**
* Compare two keys.
*
* @param a the first key
* @param b the second key
* @return -1 if the first key is smaller, 1 if bigger, 0 if equal
*/
final int compare(Object a, Object b) {
return keyType.compare(a, b);
}
/**
* Get the key type.
*
* @return the key type
*/
public final DataType getKeyType() {
return keyType;
}
/**
* Get the value type.
*
* @return the value type
*/
public final DataType getValueType() {
return valueType;
}
boolean isSingleWriter() {
return singleWriter;
}
/**
* Read a page.
*
* @param pos the position of the page
* @return the page
*/
final Page readPage(long pos) {
return store.readPage(this, pos);
}
/**
* Set the position of the root page.
* @param rootPos the position, 0 for empty
* @param version to set for this map
*
*/
final void setRootPos(long rootPos, long version) {
Page root = readOrCreateRootPage(rootPos);
setInitialRoot(root, version);
setWriteVersion(store.getCurrentVersion());
}
private Page readOrCreateRootPage(long rootPos) {
Page root = rootPos == 0 ? createEmptyLeaf() : readPage(rootPos);
return root;
}
/**
* Iterate over a number of keys.
*
* @param from the first key to return
* @return the iterator
*/
public final Iterator keyIterator(K from) {
return new Cursor(getRootPage(), from);
}
/**
* Re-write any pages that belong to one of the chunks in the given set.
*
* @param set the set of chunk ids
* @return number of pages actually re-written
*/
final int rewrite(Set set) {
if (!singleWriter) {
return rewrite(getRootPage(), set);
}
RootReference rootReference = lockRoot(getRoot(), 1);
int appendCounter = rootReference.getAppendCounter();
try {
if (appendCounter > 0) {
rootReference = flushAppendBuffer(rootReference, true);
assert rootReference.getAppendCounter() == 0;
}
int res = rewrite(rootReference.root, set);
return res;
} finally {
unlockRoot();
}
}
private int rewrite(Page p, Set set) {
if (p.isLeaf()) {
long pos = p.getPos();
int chunkId = DataUtils.getPageChunkId(pos);
if (!set.contains(chunkId)) {
return 0;
}
assert p.getKeyCount() > 0;
return rewritePage(p) ? 1 : 0;
}
int writtenPageCount = 0;
for (int i = 0; i < getChildPageCount(p); i++) {
long childPos = p.getChildPagePos(i);
if (childPos != 0 && DataUtils.getPageType(childPos) == DataUtils.PAGE_TYPE_LEAF) {
// we would need to load the page, and it's a leaf:
// only do that if it's within the set of chunks we are
// interested in
int chunkId = DataUtils.getPageChunkId(childPos);
if (!set.contains(chunkId)) {
continue;
}
}
writtenPageCount += rewrite(p.getChildPage(i), set);
}
if (writtenPageCount == 0) {
long pos = p.getPos();
int chunkId = DataUtils.getPageChunkId(pos);
if (set.contains(chunkId)) {
// an inner node page that is in one of the chunks,
// but only points to chunks that are not in the set:
// if no child was changed, we need to do that now
// (this is not needed if anyway one of the children
// was changed, as this would have updated this
// page as well)
while (!p.isLeaf()) {
p = p.getChildPage(0);
}
if (rewritePage(p)) {
writtenPageCount = 1;
}
}
}
return writtenPageCount;
}
private boolean rewritePage(Page p) {
@SuppressWarnings("unchecked")
K key = (K) p.getKey(0);
if (!isClosed()) {
return rewrite(key);
}
return true;
}
/**
* Get a cursor to iterate over a number of keys and values.
*
* @param from the first key to return
* @return the cursor
*/
public final Cursor cursor(K from) {
return new Cursor<>(getRootPage(), from);
}
@Override
public final Set> entrySet() {
final Page root = this.getRootPage();
return new AbstractSet>() {
@Override
public Iterator> iterator() {
final Cursor cursor = new Cursor<>(root, null);
return new Iterator>() {
@Override
public boolean hasNext() {
return cursor.hasNext();
}
@Override
public Entry next() {
K k = cursor.next();
return new SimpleImmutableEntry<>(k, cursor.getValue());
}
@Override
public void remove() {
throw DataUtils.newUnsupportedOperationException(
"Removing is not supported");
}
};
}
@Override
public int size() {
return MVMap.this.size();
}
@Override
public boolean contains(Object o) {
return MVMap.this.containsKey(o);
}
};
}
@Override
public Set keySet() {
final Page root = getRootPage();
return new AbstractSet() {
@Override
public Iterator iterator() {
return new Cursor(root, null);
}
@Override
public int size() {
return MVMap.this.size();
}
@Override
public boolean contains(Object o) {
return MVMap.this.containsKey(o);
}
};
}
/**
* Get the map name.
*
* @return the name
*/
public final String getName() {
return store.getMapName(id);
}
public final MVStore getStore() {
return store;
}
protected final boolean isPersistent() {
return store.getFileStore() != null && !isVolatile;
}
/**
* Get the map id. Please note the map id may be different after compacting
* a store.
*
* @return the map id
*/
public final int getId() {
return id;
}
/**
* The current root page (may not be null).
*
* @return the root page
*/
public final Page getRootPage() {
return flushAndGetRoot().root;
}
public RootReference getRoot() {
return root.get();
}
/**
* Get the root reference, flushing any current append buffer.
*
* @return current root reference
*/
public RootReference flushAndGetRoot() {
RootReference rootReference = getRoot();
if (singleWriter && rootReference.getAppendCounter() > 0) {
return flushAppendBuffer(rootReference, true);
}
return rootReference;
}
/**
* Set the initial root.
*
* @param rootPage root page
* @param version initial version
*/
final void setInitialRoot(Page rootPage, long version) {
root.set(new RootReference(rootPage, version));
}
/**
* Compare and set the root reference.
*
* @param expectedRootReference the old (expected)
* @param updatedRootReference the new
* @return whether updating worked
*/
final boolean compareAndSetRoot(RootReference expectedRootReference, RootReference updatedRootReference) {
return root.compareAndSet(expectedRootReference, updatedRootReference);
}
/**
* Rollback to the given version.
*
* @param version the version
*/
final void rollbackTo(long version) {
// check if the map was removed and re-created later ?
if (version > createVersion) {
rollbackRoot(version);
}
}
/**
* Roll the root back to the specified version.
*
* @param version to rollback to
* @return true if rollback was a success, false if there was not enough in-memory history
*/
boolean rollbackRoot(long version)
{
RootReference rootReference = flushAndGetRoot();
RootReference previous;
while (rootReference.version >= version && (previous = rootReference.previous) != null) {
if (root.compareAndSet(rootReference, previous)) {
rootReference = previous;
closed = false;
}
}
setWriteVersion(version);
return rootReference.version < version;
}
/**
* Use the new root page from now on.
* @param expectedRootReference expected current root reference
* @param newRootPage the new root page
* @param attemptUpdateCounter how many attempt (including current)
* were made to update root
* @return new RootReference or null if update failed
*/
protected static boolean updateRoot(RootReference expectedRootReference, Page newRootPage,
int attemptUpdateCounter) {
return expectedRootReference.updateRootPage(newRootPage, attemptUpdateCounter) != null;
}
/**
* Forget those old versions that are no longer needed.
* @param rootReference to inspect
*/
private void removeUnusedOldVersions(RootReference rootReference) {
rootReference.removeUnusedOldVersions(store.getOldestVersionToKeep());
}
public final boolean isReadOnly() {
return readOnly;
}
/**
* Set the volatile flag of the map.
*
* @param isVolatile the volatile flag
*/
public final void setVolatile(boolean isVolatile) {
this.isVolatile = isVolatile;
}
/**
* Whether this is volatile map, meaning that changes
* are not persisted. By default (even if the store is not persisted),
* maps are not volatile.
*
* @return whether this map is volatile
*/
public final boolean isVolatile() {
return isVolatile;
}
/**
* This method is called before writing to the map. The default
* implementation checks whether writing is allowed, and tries
* to detect concurrent modification.
*
* @throws UnsupportedOperationException if the map is read-only,
* or if another thread is concurrently writing
*/
protected final void beforeWrite() {
assert !getRoot().isLockedByCurrentThread() : getRoot();
if (closed) {
int id = getId();
String mapName = store.getMapName(id);
throw DataUtils.newIllegalStateException(
DataUtils.ERROR_CLOSED, "Map {0}({1}) is closed. {2}", mapName, id, store.getPanicException());
}
if (readOnly) {
throw DataUtils.newUnsupportedOperationException(
"This map is read-only");
}
store.beforeWrite(this);
}
@Override
public final int hashCode() {
return id;
}
@Override
public final boolean equals(Object o) {
return this == o;
}
/**
* 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 number of entries, as a long.
*
* @return the number of entries
*/
public final long sizeAsLong() {
return getRoot().getTotalCount();
}
@Override
public boolean isEmpty() {
return sizeAsLong() == 0;
}
public final long getCreateVersion() {
return createVersion;
}
/**
* Open an old version for the given map.
* It will restore map at last known state of the version specified.
* (at the point right before the commit() call, which advanced map to the next version)
* Map is opened in read-only mode.
*
* @param version the version
* @return the map
*/
public final MVMap openVersion(long version) {
if (readOnly) {
throw DataUtils.newUnsupportedOperationException(
"This map is read-only; need to call " +
"the method on the writable map");
}
DataUtils.checkArgument(version >= createVersion,
"Unknown version {0}; this map was created in version is {1}",
version, createVersion);
RootReference rootReference = flushAndGetRoot();
removeUnusedOldVersions(rootReference);
RootReference previous;
while ((previous = rootReference.previous) != null && previous.version >= version) {
rootReference = previous;
}
if (previous == null && version < store.getOldestVersionToKeep()) {
throw DataUtils.newIllegalArgumentException("Unknown version {0}", version);
}
MVMap m = openReadOnly(rootReference.root, version);
assert m.getVersion() <= version : m.getVersion() + " <= " + version;
return m;
}
/**
* Open a copy of the map in read-only mode.
*
* @param rootPos position of the root page
* @param version to open
* @return the opened map
*/
final MVMap openReadOnly(long rootPos, long version) {
Page root = readOrCreateRootPage(rootPos);
return openReadOnly(root, version);
}
private MVMap openReadOnly(Page root, long version) {
MVMap m = cloneIt();
m.readOnly = true;
m.setInitialRoot(root, version);
return m;
}
/**
* Get version of the map, which is the version of the store,
* at the moment when map was modified last time.
*
* @return version
*/
public final long getVersion() {
return getRoot().getVersion();
}
/**
* Does the root have changes since the specified version?
*
* @param version root version
* @return true if has changes
*/
final boolean hasChangesSince(long version) {
return getRoot().hasChangesSince(version);
}
/**
* Get the child page count for this page. This is to allow another map
* implementation to override the default, in case the last child is not to
* be used.
*
* @param p the page
* @return the number of direct children
*/
protected int getChildPageCount(Page p) {
return p.getRawChildPageCount();
}
/**
* Get the map type. When opening an existing map, the map type must match.
*
* @return the map type
*/
public String getType() {
return null;
}
/**
* Get the map metadata as a string.
*
* @param name the map name (or null)
* @return the string
*/
protected String asString(String name) {
StringBuilder buff = new StringBuilder();
if (name != null) {
DataUtils.appendMap(buff, "name", name);
}
if (createVersion != 0) {
DataUtils.appendMap(buff, "createVersion", createVersion);
}
String type = getType();
if (type != null) {
DataUtils.appendMap(buff, "type", type);
}
return buff.toString();
}
final RootReference setWriteVersion(long writeVersion) {
int attempt = 0;
while(true) {
RootReference rootReference = flushAndGetRoot();
if(rootReference.version >= writeVersion) {
return rootReference;
} else if (isClosed()) {
// map was closed a while back and can not possibly be in use by now
// it's time to remove it completely from the store (it was anonymous already)
if (rootReference.getVersion() + 1 < store.getOldestVersionToKeep()) {
store.deregisterMapRoot(id);
return null;
}
}
RootReference lockedRootReference = null;
if (++attempt > 3 || rootReference.isLocked()) {
lockedRootReference = lockRoot(rootReference, attempt);
rootReference = flushAndGetRoot();
}
try {
rootReference = rootReference.tryUnlockAndUpdateVersion(writeVersion, attempt);
if (rootReference != null) {
lockedRootReference = null;
removeUnusedOldVersions(rootReference);
return rootReference;
}
} finally {
if (lockedRootReference != null) {
unlockRoot();
}
}
}
}
/**
* Create empty leaf node page.
*
* @return new page
*/
protected Page createEmptyLeaf() {
return Page.createEmptyLeaf(this);
}
/**
* Create empty internal node page.
*
* @return new page
*/
protected Page createEmptyNode() {
return Page.createEmptyNode(this);
}
/**
* Copy a map. All pages are copied.
*
* @param sourceMap the source map
*/
final void copyFrom(MVMap sourceMap) {
MVStore.TxCounter txCounter = store.registerVersionUsage();
try {
beforeWrite();
copy(sourceMap.getRootPage(), null, 0);
} finally {
store.deregisterVersionUsage(txCounter);
}
}
private void copy(Page source, Page parent, int index) {
Page target = source.copy(this);
if (parent == null) {
setInitialRoot(target, INITIAL_VERSION);
} else {
parent.setChild(index, target);
}
if (!source.isLeaf()) {
for (int i = 0; i < getChildPageCount(target); i++) {
if (source.getChildPagePos(i) != 0) {
// position 0 means no child
// (for example the last entry of an r-tree node)
// (the MVMap is also used for r-trees for compacting)
copy(source.getChildPage(i), target, i);
}
}
target.setComplete();
}
store.registerUnsavedMemory(target.getMemory());
if (store.isSaveNeeded()) {
store.commit();
}
}
/**
* If map was used in append mode, this method will ensure that append buffer
* is flushed - emptied with all entries inserted into map as a new leaf.
* @param rootReference current RootReference
* @param fullFlush whether buffer should be completely flushed,
* otherwise just a single empty slot is required
* @return potentially updated RootReference
*/
private RootReference flushAppendBuffer(RootReference rootReference, boolean fullFlush) {
boolean preLocked = rootReference.isLockedByCurrentThread();
boolean locked = preLocked;
int keysPerPage = store.getKeysPerPage();
try {
IntValueHolder unsavedMemoryHolder = new IntValueHolder();
int attempt = 0;
int keyCount;
int availabilityThreshold = fullFlush ? 0 : keysPerPage - 1;
while ((keyCount = rootReference.getAppendCounter()) > availabilityThreshold) {
if (!locked) {
// instead of just calling lockRoot() we loop here and check if someone else
// already flushed the buffer, then we don't need a lock
rootReference = tryLock(rootReference, ++attempt);
if (rootReference == null) {
rootReference = getRoot();
continue;
}
locked = true;
}
Page rootPage = rootReference.root;
long version = rootReference.version;
CursorPos pos = rootPage.getAppendCursorPos(null);
assert pos != null;
assert pos.index < 0 : pos.index;
int index = -pos.index - 1;
assert index == pos.page.getKeyCount() : index + " != " + pos.page.getKeyCount();
Page p = pos.page;
CursorPos tip = pos;
pos = pos.parent;
int remainingBuffer = 0;
Page page = null;
int available = keysPerPage - p.getKeyCount();
if (available > 0) {
p = p.copy();
if (keyCount <= available) {
p.expand(keyCount, keysBuffer, valuesBuffer);
} else {
p.expand(available, keysBuffer, valuesBuffer);
keyCount -= available;
if (fullFlush) {
Object[] keys = new Object[keyCount];
Object[] values = new Object[keyCount];
System.arraycopy(keysBuffer, available, keys, 0, keyCount);
System.arraycopy(valuesBuffer, available, values, 0, keyCount);
page = Page.createLeaf(this, keys, values, 0);
} else {
System.arraycopy(keysBuffer, available, keysBuffer, 0, keyCount);
System.arraycopy(valuesBuffer, available, valuesBuffer, 0, keyCount);
remainingBuffer = keyCount;
}
}
} else {
tip = tip.parent;
page = Page.createLeaf(this,
Arrays.copyOf(keysBuffer, keyCount),
Arrays.copyOf(valuesBuffer, keyCount),
0);
}
unsavedMemoryHolder.value = 0;
if (page != null) {
assert page.map == this;
assert page.getKeyCount() > 0;
Object key = page.getKey(0);
unsavedMemoryHolder.value += page.getMemory();
while (true) {
if (pos == null) {
if (p.getKeyCount() == 0) {
p = page;
} else {
Object[] keys = new Object[]{key};
Page.PageReference[] children = new Page.PageReference[]{
new Page.PageReference(p),
new Page.PageReference(page)};
unsavedMemoryHolder.value += p.getMemory();
p = Page.createNode(this, keys, children, p.getTotalCount() + page.getTotalCount(), 0);
}
break;
}
Page c = p;
p = pos.page;
index = pos.index;
pos = pos.parent;
p = p.copy();
p.setChild(index, page);
p.insertNode(index, key, c);
keyCount = p.getKeyCount();
int at = keyCount - (p.isLeaf() ? 1 : 2);
if (keyCount <= keysPerPage &&
(p.getMemory() < store.getMaxPageSize() || at <= 0)) {
break;
}
key = p.getKey(at);
page = p.split(at);
unsavedMemoryHolder.value += p.getMemory() + page.getMemory();
}
}
p = replacePage(pos, p, unsavedMemoryHolder);
rootReference = rootReference.updatePageAndLockedStatus(p, preLocked || isPersistent(),
remainingBuffer);
if (rootReference != null) {
// should always be the case, except for spurious failure?
locked = preLocked || isPersistent();
if (isPersistent() && tip != null) {
store.registerUnsavedMemory(unsavedMemoryHolder.value + tip.processRemovalInfo(version));
}
assert rootReference.getAppendCounter() <= availabilityThreshold;
break;
}
rootReference = getRoot();
}
} finally {
if (locked && !preLocked) {
rootReference = unlockRoot();
}
}
return rootReference;
}
private static Page replacePage(CursorPos path, Page replacement, IntValueHolder unsavedMemoryHolder) {
int unsavedMemory = replacement.isSaved() ? 0 : replacement.getMemory();
while (path != null) {
Page parent = path.page;
// condition below should always be true, but older versions (up to 1.4.197)
// may create single-childed (with no keys) internal nodes, which we skip here
if (parent.getKeyCount() > 0) {
Page child = replacement;
replacement = parent.copy();
replacement.setChild(path.index, child);
unsavedMemory += replacement.getMemory();
}
path = path.parent;
}
unsavedMemoryHolder.value += unsavedMemory;
return replacement;
}
/**
* Appends entry to this map. this method is NOT thread safe and can not be used
* neither concurrently, nor in combination with any method that updates this map.
* Non-updating 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) {
if (singleWriter) {
beforeWrite();
RootReference rootReference = lockRoot(getRoot(), 1);
int appendCounter = rootReference.getAppendCounter();
try {
if (appendCounter >= keysPerPage) {
rootReference = flushAppendBuffer(rootReference, false);
appendCounter = rootReference.getAppendCounter();
assert appendCounter < keysPerPage;
}
keysBuffer[appendCounter] = key;
valuesBuffer[appendCounter] = value;
++appendCounter;
} finally {
unlockRoot(appendCounter);
}
} else {
put(key, value);
}
}
/**
* Removes last entry from this map. this method is NOT thread safe and can not be used
* neither concurrently, nor in combination with any method that updates this map.
* Non-updating method may be used concurrently, but latest removal may not be visible.
*/
public void trimLast() {
if (singleWriter) {
RootReference rootReference = getRoot();
int appendCounter = rootReference.getAppendCounter();
boolean useRegularRemove = appendCounter == 0;
if (!useRegularRemove) {
rootReference = lockRoot(rootReference, 1);
try {
appendCounter = rootReference.getAppendCounter();
useRegularRemove = appendCounter == 0;
if (!useRegularRemove) {
--appendCounter;
}
} finally {
unlockRoot(appendCounter);
}
}
if (useRegularRemove) {
Page lastLeaf = rootReference.root.getAppendCursorPos(null).page;
assert lastLeaf.isLeaf();
assert lastLeaf.getKeyCount() > 0;
Object key = lastLeaf.getKey(lastLeaf.getKeyCount() - 1);
remove(key);
}
} else {
remove(lastKey());
}
}
@Override
public final String toString() {
return asString(null);
}
/**
* A builder for maps.
*
* @param the map type
* @param the key type
* @param the value type
*/
public interface MapBuilder, K, V> {
/**
* Create a new map of the given type.
* @param store which will own this map
* @param config configuration
*
* @return the map
*/
M create(MVStore store, Map config);
DataType getKeyType();
DataType getValueType();
void setKeyType(DataType dataType);
void setValueType(DataType dataType);
}
/**
* A builder for this class.
*
* @param the key type
* @param the value type
*/
public abstract static class BasicBuilder, K, V> implements MapBuilder {
private DataType keyType;
private DataType valueType;
/**
* Create a new builder with the default key and value data types.
*/
protected BasicBuilder() {
// ignore
}
@Override
public DataType getKeyType() {
return keyType;
}
@Override
public DataType getValueType() {
return valueType;
}
@Override
public void setKeyType(DataType keyType) {
this.keyType = keyType;
}
@Override
public void setValueType(DataType valueType) {
this.valueType = valueType;
}
/**
* Set the key data type.
*
* @param keyType the key type
* @return this
*/
public BasicBuilder keyType(DataType keyType) {
this.keyType = keyType;
return this;
}
/**
* Set the value data type.
*
* @param valueType the value type
* @return this
*/
public BasicBuilder valueType(DataType valueType) {
this.valueType = valueType;
return this;
}
@Override
public M create(MVStore store, Map config) {
if (getKeyType() == null) {
setKeyType(new ObjectDataType());
}
if (getValueType() == null) {
setValueType(new ObjectDataType());
}
DataType keyType = getKeyType();
DataType valueType = getValueType();
config.put("store", store);
config.put("key", keyType);
config.put("val", valueType);
return create(config);
}
/**
* Create map from config.
* @param config config map
* @return new map
*/
protected abstract M create(Map config);
}
/**
* A builder for this class.
*
* @param the key type
* @param the value type
*/
public static class Builder extends BasicBuilder, K, V> {
private boolean singleWriter;
public Builder() {}
@Override
public Builder keyType(DataType dataType) {
setKeyType(dataType);
return this;
}
@Override
public Builder valueType(DataType dataType) {
setValueType(dataType);
return this;
}
/**
* Set up this Builder to produce MVMap, which can be used in append mode
* by a single thread.
* @see MVMap#append(Object, Object)
* @return this Builder for chained execution
*/
public Builder singleWriter() {
singleWriter = true;
return this;
}
@Override
protected MVMap create(Map config) {
config.put("singleWriter", singleWriter);
Object type = config.get("type");
if(type == null || type.equals("rtree")) {
return new MVMap<>(config);
}
throw new IllegalArgumentException("Incompatible map type");
}
}
public enum Decision { ABORT, REMOVE, PUT, REPEAT }
/**
* Class DecisionMaker provides callback interface (and should become a such in Java 8)
* for MVMap.operate method.
* It provides control logic to make a decision about how to proceed with update
* at the point in execution when proper place and possible existing value
* for insert/update/delete key is found.
* Revised value for insert/update is also provided based on original input value
* and value currently existing in the map.
*
* @param value type of the map
*/
public abstract static class DecisionMaker
{
/**
* Decision maker for transaction rollback.
*/
public static final DecisionMaker