org.glowroot.shaded.h2.mvstore.MVMap Maven / Gradle / Ivy
/*
* Copyright 2004-2013 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.glowroot.shaded.h2.mvstore;
import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import org.glowroot.shaded.h2.mvstore.type.DataType;
import org.glowroot.shaded.h2.mvstore.type.ObjectDataType;
import org.glowroot.shaded.h2.util.New;
/**
* A stored map.
*
* @param the key class
* @param the value class
*/
public class MVMap extends AbstractMap
implements ConcurrentMap {
/**
* The store.
*/
protected MVStore store;
/**
* The current root page (may not be null).
*/
protected volatile Page root;
/**
* The version used for writing.
*/
protected volatile long writeVersion;
/**
* This version is set during a write operation.
*/
protected volatile long currentWriteVersion = -1;
private int id;
private long createVersion;
private final DataType keyType;
private final DataType valueType;
private ArrayList oldRoots = new ArrayList();
private boolean closed;
private boolean readOnly;
protected MVMap(DataType keyType, DataType valueType) {
this.keyType = keyType;
this.valueType = valueType;
this.root = Page.createEmpty(this, -1);
}
/**
* Open this map with the given store and configuration.
*
* @param store the store
* @param config the configuration
*/
protected void init(MVStore store, HashMap config) {
this.store = store;
this.id = DataUtils.readHexInt(config, "id", 0);
this.createVersion = DataUtils.readHexLong(config, "createVersion", 0);
this.writeVersion = store.getCurrentVersion();
}
/**
* Create a copy of a page, if the write version is higher than the current
* version. If a copy is created, the old page is marked as deleted.
*
* @param p the page
* @param writeVersion the write version
* @return a page with the given write version
*/
protected Page copyOnWrite(Page p, long writeVersion) {
if (p.getVersion() == writeVersion) {
return p;
}
return p.copy(writeVersion);
}
/**
* 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
@SuppressWarnings("unchecked")
public V put(K key, V value) {
DataUtils.checkArgument(value != null, "The value may not be null");
beforeWrite();
try {
long v = writeVersion;
Page p = copyOnWrite(root, v);
p = splitRootIfNeeded(p, v);
Object result = put(p, v, key, value);
newRoot(p);
return (V) result;
} finally {
afterWrite();
}
}
/**
* Split the root page if necessary.
*
* @param p the page
* @param writeVersion the write version
* @return the new sibling
*/
protected Page splitRootIfNeeded(Page p, long writeVersion) {
if (p.getMemory() <= store.getPageSplitSize() || p.getKeyCount() <= 1) {
return p;
}
int at = p.getKeyCount() / 2;
long totalCount = p.getTotalCount();
Object k = p.getKey(at);
Page split = p.split(at);
Object[] keys = { k };
long[] children = { p.getPos(), split.getPos() };
Page[] childrenPages = { p, split };
long[] counts = { p.getTotalCount(), split.getTotalCount() };
p = Page.create(this, writeVersion,
1, keys, null,
2, children, childrenPages, counts,
totalCount, 0, 0);
return p;
}
/**
* Add or update a key-value pair.
*
* @param p the page
* @param writeVersion the write version
* @param key the key (may not be null)
* @param value the value (may not be null)
* @return the old value, or null
*/
protected Object put(Page p, long writeVersion, Object key, Object value) {
int index = p.binarySearch(key);
if (p.isLeaf()) {
if (index < 0) {
index = -index - 1;
p.insertLeaf(index, key, value);
return null;
}
return p.setValue(index, value);
}
// p is a node
if (index < 0) {
index = -index - 1;
} else {
index++;
}
Page c = copyOnWrite(p.getChildPage(index), writeVersion);
if (c.getMemory() > store.getPageSplitSize() && c.getKeyCount() > 1) {
// split on the way down
int at = c.getKeyCount() / 2;
Object k = c.getKey(at);
Page split = c.split(at);
p.setChild(index, split);
p.setCounts(index, split);
p.insertNode(index, k, c);
// now we are not sure where to add
return put(p, writeVersion, key, value);
}
p.setChild(index, c);
Object result = put(c, writeVersion, key, value);
p.setCounts(index, c);
return result;
}
/**
* Get the first key, or null if the map is empty.
*
* @return the first key, or null
*/
public K firstKey() {
return getFirstLast(true);
}
/**
* Get the last key, or null if the map is empty.
*
* @return the last key, or null
*/
public K lastKey() {
return getFirstLast(false);
}
/**
* Get the key at the given index.
*
* This is a O(log(size)) operation.
*
* @param index the index
* @return the key
*/
@SuppressWarnings("unchecked")
public K getKey(long index) {
if (index < 0 || index >= size()) {
return null;
}
Page p = root;
long offset = 0;
while (true) {
if (p.isLeaf()) {
if (index >= offset + p.getKeyCount()) {
return null;
}
return (K) p.getKey((int) (index - offset));
}
int i = 0, size = p.getChildPageCount();
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 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 long getKeyIndex(K key) {
if (size() == 0) {
return -1;
}
Page p = root;
long offset = 0;
while (true) {
int x = p.binarySearch(key);
if (p.isLeaf()) {
if (x < 0) {
return -offset + x;
}
return offset + x;
}
if (x < 0) {
x = -x - 1;
} else {
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
*/
@SuppressWarnings("unchecked")
protected K getFirstLast(boolean first) {
if (size() == 0) {
return null;
}
Page p = root;
while (true) {
if (p.isLeaf()) {
return (K) p.getKey(first ? 0 : p.getKeyCount() - 1);
}
p = p.getChildPage(first ? 0 : p.getChildPageCount() - 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 K higherKey(K key) {
return getMinMax(key, false, true);
}
/**
* Get the smallest key that is larger or equal to this key.
*
* @param key the key
* @return the result
*/
public K ceilingKey(K key) {
return getMinMax(key, false, false);
}
/**
* Get the largest key that is smaller or equal to this key.
*
* @param key the key
* @return the result
*/
public K floorKey(K key) {
return getMinMax(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 K lowerKey(K key) {
return getMinMax(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
*/
protected K getMinMax(K key, boolean min, boolean excluding) {
return getMinMax(root, key, min, excluding);
}
@SuppressWarnings("unchecked")
private K getMinMax(Page p, K key, boolean min, boolean excluding) {
if (p.isLeaf()) {
int x = p.binarySearch(key);
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);
}
int x = p.binarySearch(key);
if (x < 0) {
x = -x - 1;
} else {
x++;
}
while (true) {
if (x < 0 || x >= p.getChildPageCount()) {
return null;
}
K k = getMinMax(p.getChildPage(x), key, min, excluding);
if (k != null) {
return k;
}
x += min ? -1 : 1;
}
}
/**
* Get a value.
*
* @param key the key
* @return the value, or null if not found
*/
@Override
@SuppressWarnings("unchecked")
public V get(Object key) {
return (V) binarySearch(root, key);
}
/**
* Get the value for the given key, or null if not found.
*
* @param p the page
* @param key the key
* @return the value or null
*/
protected Object binarySearch(Page p, Object key) {
int x = p.binarySearch(key);
if (!p.isLeaf()) {
if (x < 0) {
x = -x - 1;
} else {
x++;
}
p = p.getChildPage(x);
return binarySearch(p, key);
}
if (x >= 0) {
return p.getValue(x);
}
return null;
}
@Override
public boolean containsKey(Object key) {
return get(key) != null;
}
/**
* Get the page for the given value.
*
* @param key the key
* @return the value, or null if not found
*/
protected Page getPage(K key) {
return binarySearchPage(root, key);
}
/**
* Get the value for the given key, or null if not found.
*
* @param p the parent page
* @param key the key
* @return the page or null
*/
protected Page binarySearchPage(Page p, Object key) {
int x = p.binarySearch(key);
if (!p.isLeaf()) {
if (x < 0) {
x = -x - 1;
} else {
x++;
}
p = p.getChildPage(x);
return binarySearchPage(p, key);
}
if (x >= 0) {
return p;
}
return null;
}
/**
* Remove all entries.
*/
@Override
public void clear() {
beforeWrite();
try {
root.removeAllRecursive();
newRoot(Page.createEmpty(this, writeVersion));
} finally {
afterWrite();
}
}
/**
* Close the map. Accessing the data is still possible (to allow concurrent
* reads), but it is marked as closed.
*/
void close() {
closed = true;
}
public 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
*/
@Override
public V remove(Object key) {
beforeWrite();
try {
long v = writeVersion;
Page p = copyOnWrite(root, v);
@SuppressWarnings("unchecked")
V result = (V) remove(p, v, key);
if (!p.isLeaf() && p.getTotalCount() == 0) {
p.removePage();
p = Page.createEmpty(this, p.getVersion());
}
newRoot(p);
return result;
} finally {
afterWrite();
}
}
/**
* 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 synchronized V putIfAbsent(K key, V value) {
V old = get(key);
if (old == null) {
put(key, value);
}
return old;
}
/**
* 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
*/
@Override
public synchronized boolean remove(Object key, Object value) {
V old = get(key);
if (areValuesEqual(old, value)) {
remove(key);
return true;
}
return false;
}
/**
* Check whether the two values are equal.
*
* @param a the first value
* @param b the second value
* @return true if they are equal
*/
public boolean areValuesEqual(Object a, Object b) {
if (a == b) {
return true;
} else if (a == null || b == null) {
return false;
}
return valueType.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 synchronized boolean replace(K key, V oldValue, V newValue) {
V old = get(key);
if (areValuesEqual(old, oldValue)) {
put(key, newValue);
return true;
}
return false;
}
/**
* 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 synchronized V replace(K key, V value) {
V old = get(key);
if (old != null) {
put(key, value);
return old;
}
return null;
}
/**
* Remove a key-value pair.
*
* @param p the page (may not be null)
* @param writeVersion the write version
* @param key the key
* @return the old value, or null if the key did not exist
*/
protected Object remove(Page p, long writeVersion, Object key) {
int index = p.binarySearch(key);
Object result = null;
if (p.isLeaf()) {
if (index >= 0) {
result = p.getValue(index);
p.remove(index);
}
return result;
}
// node
if (index < 0) {
index = -index - 1;
} else {
index++;
}
Page cOld = p.getChildPage(index);
Page c = copyOnWrite(cOld, writeVersion);
result = remove(c, writeVersion, key);
if (result == null || c.getTotalCount() != 0) {
// no change, or
// there are more nodes
p.setChild(index, c);
p.setCounts(index, c);
} else {
// this child was deleted
if (p.getKeyCount() == 0) {
p.setChild(index, c);
p.setCounts(index, c);
c.removePage();
} else {
p.remove(index);
}
}
return result;
}
/**
* Use the new root page from now on.
*
* @param newRoot the new root page
*/
protected void newRoot(Page newRoot) {
if (root != newRoot) {
removeUnusedOldVersions();
if (root.getVersion() != newRoot.getVersion()) {
ArrayList list = oldRoots;
if (list.size() > 0) {
Page last = list.get(list.size() - 1);
if (last.getVersion() != root.getVersion()) {
list.add(root);
}
} else {
list.add(root);
}
}
root = newRoot;
}
}
/**
* 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
*/
int compare(Object a, Object b) {
return keyType.compare(a, b);
}
/**
* Get the key type.
*
* @return the key type
*/
public DataType getKeyType() {
return keyType;
}
/**
* Get the value type.
*
* @return the value type
*/
public DataType getValueType() {
return valueType;
}
/**
* Read a page.
*
* @param pos the position of the page
* @return the page
*/
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 the version of the root
*/
void setRootPos(long rootPos, long version) {
root = rootPos == 0 ? Page.createEmpty(this, -1) : readPage(rootPos);
root.setVersion(version);
}
/**
* Iterate over a number of keys.
*
* @param from the first key to return
* @return the iterator
*/
public Iterator keyIterator(K from) {
return new Cursor(this, root, from);
}
/**
* Get a cursor to iterate over a number of keys and values.
*
* @param from the first key to return
* @return the cursor
*/
public Cursor cursor(K from) {
return new Cursor(this, root, from);
}
@Override
public Set> entrySet() {
final MVMap map = this;
final Page root = this.root;
return new AbstractSet>() {
@Override
public Iterator> iterator() {
final Cursor cursor = new Cursor(map, root, null);
return new Iterator>() {
@Override
public boolean hasNext() {
return cursor.hasNext();
}
@Override
public Entry next() {
K k = cursor.next();
return new DataUtils.MapEntry(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 MVMap map = this;
final Page root = this.root;
return new AbstractSet() {
@Override
public Iterator iterator() {
return new Cursor(map, root, null);
}
@Override
public int size() {
return MVMap.this.size();
}
@Override
public boolean contains(Object o) {
return MVMap.this.containsKey(o);
}
};
}
/**
* Get the root page.
*
* @return the root page
*/
public Page getRoot() {
return root;
}
/**
* Get the map name.
*
* @return the name
*/
public String getName() {
return store.getMapName(id);
}
public MVStore getStore() {
return store;
}
public int getId() {
return id;
}
/**
* Rollback to the given version.
*
* @param version the version
*/
void rollbackTo(long version) {
beforeWrite();
try {
removeUnusedOldVersions();
if (version <= createVersion) {
// the map is removed later
} else if (root.getVersion() >= version) {
// iterating in descending order -
// this is not terribly efficient if there are many versions
ArrayList list = oldRoots;
while (list.size() > 0) {
int i = list.size() - 1;
Page p = list.get(i);
root = p;
list.remove(i);
if (p.getVersion() < version) {
break;
}
}
}
} finally {
afterWrite();
}
}
/**
* Forget those old versions that are no longer needed.
*/
void removeUnusedOldVersions() {
long oldest = store.getOldestVersionToKeep();
if (oldest == -1) {
return;
}
int i = searchRoot(oldest);
if (i < 0) {
i = -i - 1;
}
i--;
if (i <= 0) {
return;
}
// create a new instance
// because another thread might iterate over it
int size = oldRoots.size() - i;
ArrayList list = new ArrayList(size);
list.addAll(oldRoots.subList(i, oldRoots.size()));
oldRoots = list;
}
public boolean isReadOnly() {
return readOnly;
}
/**
* 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 void beforeWrite() {
if (closed) {
throw DataUtils.newIllegalStateException(
DataUtils.ERROR_CLOSED, "This map is closed");
}
if (readOnly) {
throw DataUtils.newUnsupportedOperationException(
"This map is read-only");
}
checkConcurrentWrite();
store.beforeWrite();
currentWriteVersion = writeVersion;
}
/**
* Check that no write operation is in progress.
*/
protected void checkConcurrentWrite() {
if (currentWriteVersion != -1) {
// try to detect concurrent modification
// on a best-effort basis
throw DataUtils.newConcurrentModificationException(getName());
}
}
/**
* This method is called after writing to the map (whether or not the write
* operation was successful).
*/
protected void afterWrite() {
currentWriteVersion = -1;
}
/**
* If there is a concurrent update to the given version, wait until it is
* finished.
*
* @param version the read version
*/
protected void waitUntilWritten(long version) {
if (readOnly) {
throw DataUtils.newIllegalStateException(
DataUtils.ERROR_INTERNAL,
"Waiting for writes to a read-only map");
}
while (currentWriteVersion == version) {
Thread.yield();
}
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object o) {
return this == o;
}
/**
* Get the number of entries, as a integer. Integer.MAX_VALUE is returned if
* there are more than this entries.
*
* @return the number of entries, as an integer
*/
@Override
public 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 long sizeAsLong() {
return root.getTotalCount();
}
@Override
public boolean isEmpty() {
// could also use (sizeAsLong() == 0)
return root.isLeaf() && root.getKeyCount() == 0;
}
public long getCreateVersion() {
return createVersion;
}
/**
* Remove the given page (make the space available).
*
* @param pos the position of the page to remove
* @param memory the number of bytes used for this page
*/
protected void removePage(long pos, int memory) {
store.removePage(this, pos, memory);
}
/**
* Open an old version for the given map.
*
* @param version the version
* @return the map
*/
public 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);
Page newest = null;
// need to copy because it can change
Page r = root;
if (version >= r.getVersion() &&
(version == writeVersion ||
r.getVersion() >= 0 ||
version <= createVersion ||
store.getFileStore() == null)) {
newest = r;
} else {
// find the newest page that has a getVersion() <= version
int i = searchRoot(version);
if (i < 0) {
// not found
if (i == -1) {
// smaller than all in-memory versions
return store.openMapVersion(version, id, this);
}
i = -i - 2;
}
newest = oldRoots.get(i);
}
MVMap m = openReadOnly();
m.root = newest;
return m;
}
/**
* Open a copy of the map in read-only mode.
*
* @return the opened map
*/
MVMap openReadOnly() {
MVMap m = new MVMap(keyType, valueType);
m.readOnly = true;
HashMap config = New.hashMap();
config.put("id", id);
config.put("createVersion", createVersion);
m.init(store, config);
m.root = root;
return m;
}
private int searchRoot(long version) {
int low = 0, high = oldRoots.size() - 1;
while (low <= high) {
int x = (low + high) >>> 1;
long v = oldRoots.get(x).getVersion();
if (v < version) {
low = x + 1;
} else if (version < v) {
high = x - 1;
} else {
return x;
}
}
return -(low + 1);
}
public long getVersion() {
return root.getVersion();
}
/**
* 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.getChildPageCount();
}
/**
* 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
*/
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();
}
void setWriteVersion(long writeVersion) {
this.writeVersion = writeVersion;
}
@Override
public 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.
*
* @return the map
*/
M create();
}
/**
* A builder for this class.
*
* @param the key type
* @param the value type
*/
public static class Builder implements MapBuilder, K, V> {
protected DataType keyType;
protected DataType valueType;
/**
* Create a new builder with the default key and value data types.
*/
public Builder() {
// ignore
}
/**
* Set the key data type.
*
* @param keyType the key type
* @return this
*/
public Builder keyType(DataType keyType) {
this.keyType = keyType;
return this;
}
public DataType getKeyType() {
return keyType;
}
public DataType getValueType() {
return valueType;
}
/**
* Set the value data type.
*
* @param valueType the value type
* @return this
*/
public Builder valueType(DataType valueType) {
this.valueType = valueType;
return this;
}
@Override
public MVMap create() {
if (keyType == null) {
keyType = new ObjectDataType();
}
if (valueType == null) {
valueType = new ObjectDataType();
}
return new MVMap(keyType, valueType);
}
}
}