提交 09769f17 authored 作者: Thomas Mueller's avatar Thomas Mueller

MVStore: larger stores (multiple GB) are now much faster.

上级 f8db2f80
...@@ -18,7 +18,8 @@ Change Log ...@@ -18,7 +18,8 @@ Change Log
<h1>Change Log</h1> <h1>Change Log</h1>
<h2>Next Version (unreleased)</h2> <h2>Next Version (unreleased)</h2>
<ul><li>When using local temporary tables and not dropping them manually before closing the session, <ul><li>MVStore: larger stores (multiple GB) are now much faster.
</li><li>When using local temporary tables and not dropping them manually before closing the session,
and then killing the process could result in a database that couldn't be opened (except when using and then killing the process could result in a database that couldn't be opened (except when using
the recover tool). the recover tool).
</li><li>Support TRUNC(timestamp) for improved Oracle compatiblity. </li><li>Support TRUNC(timestamp) for improved Oracle compatiblity.
......
/*
* 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.h2.mvstore;
import java.util.Iterator;
/**
* A cursor to iterate over all keys in new pages.
*
* @param <K> the key type
* @param <V> the value type
*/
public class ChangeCursor<K, V> implements Iterator<K> {
private final MVMap<K, V> map;
private final Page root1, root2;
/**
* The state of this cursor.
* 0: not initialized
* 1: reading from root1
* 2: reading from root2
* 3: closed
*/
private int state;
private CursorPos pos1, pos2;
private K current;
ChangeCursor(MVMap<K, V> map, Page root1, Page root2) {
this.map = map;
this.root1 = root1;
this.root2 = root2;
}
public K next() {
K c = current;
fetchNext();
return c;
}
public boolean hasNext() {
if (state == 0) {
pos1 = new CursorPos(root1, 0, null);
pos1 = min(pos1);
state = 1;
fetchNext();
}
return current != null;
}
public void remove() {
throw DataUtils.newUnsupportedOperationException(
"Removing is not supported");
}
private void fetchNext() {
while (fetchNextKey()) {
if (pos1 == null || pos2 == null) {
break;
}
@SuppressWarnings("unchecked")
V v1 = (V) map.binarySearch(root1, current);
@SuppressWarnings("unchecked")
V v2 = (V) map.binarySearch(root2, current);
if (!v1.equals(v2)) {
break;
}
}
}
private boolean fetchNextKey() {
while (true) {
if (state == 3) {
return false;
}
if (state == 1) {
// read from root1
pos1 = fetchNext(pos1);
if (pos1 == null) {
// reached the end of pos1
state = 2;
pos2 = null;
continue;
}
pos2 = find(root2, current);
if (pos2 == null) {
// not found in root2
return true;
}
if (!pos1.page.equals(pos2.page)) {
// the page is different,
// so the entry has possibly changed
return true;
}
while (true) {
pos1 = pos1.parent;
if (pos1 == null) {
// reached end of pos1
state = 2;
pos2 = null;
break;
}
pos2 = pos2.parent;
if (pos2 == null || !pos1.page.equals(pos2.page)) {
if (pos1.index + 1 < map.getChildPageCount(pos1.page)) {
pos1 = new CursorPos(pos1.page.getChildPage(++pos1.index), 0, pos1);
pos1 = min(pos1);
break;
}
}
}
}
if (state == 2) {
if (pos2 == null) {
// init reading from root2
pos2 = new CursorPos(root2, 0, null);
pos2 = min(pos2);
}
// read from root2
pos2 = fetchNext(pos2);
if (pos2 == null) {
// reached the end of pos2
state = 3;
current = null;
continue;
}
pos1 = find(root1, current);
if (pos1 != null) {
// found a corresponding record
// so it was not deleted
// but now we may need to skip pages
if (!pos1.page.equals(pos2.page)) {
// the page is different
pos1 = null;
continue;
}
while (true) {
pos2 = pos2.parent;
if (pos2 == null) {
// reached end of pos1
state = 3;
current = null;
pos1 = null;
break;
}
pos1 = pos1.parent;
if (pos1 == null || !pos2.page.equals(pos1.page)) {
if (pos2.index + 1 < map.getChildPageCount(pos2.page)) {
pos2 = new CursorPos(pos2.page.getChildPage(++pos2.index), 0, pos2);
pos2 = min(pos2);
break;
}
}
}
pos1 = null;
continue;
}
// found no corresponding record
// so it was deleted
return true;
}
}
}
private CursorPos find(Page p, K key) {
// TODO combine with RangeCursor.min
// possibly move to MVMap
CursorPos pos = null;
while (true) {
if (p.isLeaf()) {
int x = key == null ? 0 : p.binarySearch(key);
if (x < 0) {
return null;
}
return new CursorPos(p, x, pos);
}
int x = key == null ? -1 : p.binarySearch(key);
if (x < 0) {
x = -x - 1;
} else {
x++;
}
pos = new CursorPos(p, x, pos);
p = p.getChildPage(x);
}
}
@SuppressWarnings("unchecked")
private CursorPos fetchNext(CursorPos p) {
while (p != null) {
if (p.index < p.page.getKeyCount()) {
current = (K) p.page.getKey(p.index++);
return p;
}
p = p.parent;
if (p == null) {
break;
}
if (p.index + 1 < map.getChildPageCount(p.page)) {
p = new CursorPos(p.page.getChildPage(++p.index), 0, p);
p = min(p);
}
}
current = null;
return p;
}
private static CursorPos min(CursorPos p) {
while (true) {
if (p.page.isLeaf()) {
return p;
}
Page c = p.page.getChildPage(0);
p = new CursorPos(c, 0, p);
}
}
}
...@@ -42,9 +42,14 @@ public class Chunk { ...@@ -42,9 +42,14 @@ public class Chunk {
int length; int length;
/** /**
* The number of pages. * The total number of pages in this chunk.
*/ */
int pageCount; int pageCount;
/**
* The number of pages still alive.
*/
int pageCountLive;
/** /**
* The sum of the max length of all pages. * The sum of the max length of all pages.
...@@ -101,6 +106,7 @@ public class Chunk { ...@@ -101,6 +106,7 @@ public class Chunk {
Chunk c = new Chunk(chunkId); Chunk c = new Chunk(chunkId);
c.length = length; c.length = length;
c.pageCount = pageCount; c.pageCount = pageCount;
c.pageCountLive = pageCount;
c.start = start; c.start = start;
c.metaRootPos = metaRootPos; c.metaRootPos = metaRootPos;
c.maxLength = maxLength; c.maxLength = maxLength;
...@@ -136,6 +142,7 @@ public class Chunk { ...@@ -136,6 +142,7 @@ public class Chunk {
c.start = Long.parseLong(map.get("start")); c.start = Long.parseLong(map.get("start"));
c.length = Integer.parseInt(map.get("length")); c.length = Integer.parseInt(map.get("length"));
c.pageCount = Integer.parseInt(map.get("pageCount")); c.pageCount = Integer.parseInt(map.get("pageCount"));
c.pageCountLive = Integer.parseInt(map.get("pageCountLive"));
c.maxLength = Long.parseLong(map.get("maxLength")); c.maxLength = Long.parseLong(map.get("maxLength"));
c.maxLengthLive = Long.parseLong(map.get("maxLengthLive")); c.maxLengthLive = Long.parseLong(map.get("maxLengthLive"));
c.metaRootPos = Long.parseLong(map.get("metaRoot")); c.metaRootPos = Long.parseLong(map.get("metaRoot"));
...@@ -169,6 +176,7 @@ public class Chunk { ...@@ -169,6 +176,7 @@ public class Chunk {
"maxLengthLive:" + maxLengthLive + "," + "maxLengthLive:" + maxLengthLive + "," +
"metaRoot:" + metaRootPos + "," + "metaRoot:" + metaRootPos + "," +
"pageCount:" + pageCount + "," + "pageCount:" + pageCount + "," +
"pageCountLive:" + pageCountLive + "," +
"start:" + start + "," + "start:" + start + "," +
"time:" + time + "," + "time:" + time + "," +
"version:" + version; "version:" + version;
......
/*
* 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.h2.mvstore; package org.h2.mvstore;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/**
* A list that maintains ranges of free space (in pages) in a file.
*/
public class FreeSpaceList { public class FreeSpaceList {
/** the first 2 pages are occupied by the file header. */ /**
* The first 2 pages are occupied by the file header.
*/
private static final int FIRST_FREE_PAGE = 2; private static final int FIRST_FREE_PAGE = 2;
/** I use max_value/2 to avoid overflow errors during arithmetic. */
private static final int MAX_NO_PAGES = Integer.MAX_VALUE / 2; /**
* The maximum number of pages. Smaller than than MAX_VALUE to avoid
* overflow errors during arithmetic operations.
*/
private static final int MAX_PAGE_COUNT = Integer.MAX_VALUE / 2;
private List<PageRange> freeSpaceList = new ArrayList<PageRange>(); private List<PageRange> freeSpaceList = new ArrayList<PageRange>();
public FreeSpaceList() { public FreeSpaceList() {
freeSpaceList.add(new PageRange(FIRST_FREE_PAGE, MAX_NO_PAGES)); clear();
} }
private static final class PageRange { /**
/** the start point, in pages */ * Reset the list.
public int start; */
/** the length, in pages */ public synchronized void clear() {
public int length; freeSpaceList.clear();
freeSpaceList.add(new PageRange(FIRST_FREE_PAGE, MAX_PAGE_COUNT));
public PageRange(int start, int length) {
this.start = start;
this.length = length;
}
@Override
public String toString() {
return "start:" + start + " length:" + length;
}
} }
/** /**
* @return position in pages * Allocate a number of pages.
*
* @param length the number of bytes to allocate
* @return the position in pages
*/ */
public synchronized int allocatePages(long length) { public synchronized int allocatePages(long length) {
int required = (int) (length / MVStore.BLOCK_SIZE) + 1; int required = (int) (length / MVStore.BLOCK_SIZE) + 1;
...@@ -43,9 +52,15 @@ public class FreeSpaceList { ...@@ -43,9 +52,15 @@ public class FreeSpaceList {
return pr.start; return pr.start;
} }
} }
throw DataUtils.newIllegalStateException("could not find a free page to allocate"); throw DataUtils.newIllegalStateException(
"Could not find a free page to allocate");
} }
/**
* Mark a chunk as used.
*
* @param c the chunk
*/
public synchronized void markUsed(Chunk c) { public synchronized void markUsed(Chunk c) {
int chunkStart = (int) (c.start / MVStore.BLOCK_SIZE); int chunkStart = (int) (c.start / MVStore.BLOCK_SIZE);
int required = (int) ((c.start + c.length) / MVStore.BLOCK_SIZE) + 2 - chunkStart; int required = (int) ((c.start + c.length) / MVStore.BLOCK_SIZE) + 2 - chunkStart;
...@@ -59,11 +74,12 @@ public class FreeSpaceList { ...@@ -59,11 +74,12 @@ public class FreeSpaceList {
i++; i++;
} }
if (found == null) { if (found == null) {
throw DataUtils.newIllegalStateException("cannot find spot to mark chunk as used in free list, " throw DataUtils.newIllegalStateException(
+ c.toString()); "Cannot find spot to mark chunk as used in free list: {0}", c);
} }
if (chunkStart + required > found.start + found.length) { if (chunkStart + required > found.start + found.length) {
throw DataUtils.newIllegalStateException("chunk runs over edge of free space, " + c.toString()); throw DataUtils.newIllegalStateException(
"Chunk runs over edge of free space: {0}", c);
} }
if (found.start == chunkStart) { if (found.start == chunkStart) {
// if the used-chunk is at the beginning of a free-space-range // if the used-chunk is at the beginning of a free-space-range
...@@ -92,6 +108,11 @@ public class FreeSpaceList { ...@@ -92,6 +108,11 @@ public class FreeSpaceList {
} }
} }
/**
* Mark the chunk as free.
*
* @param c the chunk
*/
public synchronized void markFree(Chunk c) { public synchronized void markFree(Chunk c) {
int chunkStart = (int) (c.start / MVStore.BLOCK_SIZE); int chunkStart = (int) (c.start / MVStore.BLOCK_SIZE);
int required = (c.length / MVStore.BLOCK_SIZE) + 1; int required = (c.length / MVStore.BLOCK_SIZE) + 1;
...@@ -105,15 +126,15 @@ public class FreeSpaceList { ...@@ -105,15 +126,15 @@ public class FreeSpaceList {
i++; i++;
} }
if (found == null) { if (found == null) {
throw DataUtils.newIllegalStateException("cannot find spot to mark chunk as unused in free list, " throw DataUtils.newIllegalStateException(
+ c.toString()); "Cannot find spot to mark chunk as unused in free list: {0}", c);
} }
if (chunkStart + required + 1 == found.start) { if (chunkStart + required + 1 == found.start) {
// if the used-chunk is adjacent to the beginning of a // if the used-chunk is adjacent to the beginning of a
// free-space-range // free-space-range
found.start = chunkStart; found.start = chunkStart;
found.length += required; found.length += required;
// compactify-free-list: merge the previous entry into this one if // compact: merge the previous entry into this one if
// they are now adjacent // they are now adjacent
if (i > 0) { if (i > 0) {
PageRange previous = freeSpaceList.get(i - 1); PageRange previous = freeSpaceList.get(i - 1);
...@@ -129,7 +150,7 @@ public class FreeSpaceList { ...@@ -129,7 +150,7 @@ public class FreeSpaceList {
PageRange previous = freeSpaceList.get(i - 1); PageRange previous = freeSpaceList.get(i - 1);
if (previous.start + previous.length + 1 == chunkStart) { if (previous.start + previous.length + 1 == chunkStart) {
previous.length += required; previous.length += required;
// compactify-free-list: merge the next entry into this one if // compact: merge the next entry into this one if
// they are now adjacent // they are now adjacent
if (previous.start + previous.length + 1 == found.start) { if (previous.start + previous.length + 1 == found.start) {
previous.length += found.length; previous.length += found.length;
...@@ -144,23 +165,44 @@ public class FreeSpaceList { ...@@ -144,23 +165,44 @@ public class FreeSpaceList {
freeSpaceList.add(i, newRange); freeSpaceList.add(i, newRange);
} }
public synchronized void clear() { public String toString() {
freeSpaceList.clear(); StringBuilder buff = new StringBuilder();
freeSpaceList.add(new PageRange(FIRST_FREE_PAGE, MAX_NO_PAGES));
}
/** debug method */
public void dumpFreeList() {
StringBuilder buf = new StringBuilder("free list : ");
boolean first = true; boolean first = true;
for (PageRange pr : freeSpaceList) { for (PageRange r : freeSpaceList) {
if (first) { if (first) {
first = false; first = false;
} else { } else {
buf.append(", "); buff.append(", ");
} }
buf.append(pr.start + "-" + (pr.start + pr.length - 1)); buff.append(r.start + "-" + (r.start + r.length - 1));
} }
System.out.println(buf.toString()); return buff.toString();
} }
/**
* A range of free pages.
*/
private static final class PageRange {
/**
* The starting point, in pages.
*/
public int start;
/**
* The length, in pages.
*/
public int length;
public PageRange(int start, int length) {
this.start = start;
this.length = length;
}
@Override
public String toString() {
return "start:" + start + " length:" + length;
}
}
} }
...@@ -773,18 +773,6 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -773,18 +773,6 @@ public class MVMap<K, V> extends AbstractMap<K, V>
return new Cursor<K>(this, root, from); return new Cursor<K>(this, root, from);
} }
/**
* Iterate over all keys in changed pages.
*
* @param version the old version
* @return the iterator
*/
public Iterator<K> changeIterator(long version) {
checkOpen();
MVMap<K, V> old = openVersion(version);
return new ChangeCursor<K, V>(this, root, old.root);
}
public Set<Map.Entry<K, V>> entrySet() { public Set<Map.Entry<K, V>> entrySet() {
HashMap<K, V> map = new HashMap<K, V>(); HashMap<K, V> map = new HashMap<K, V>();
for (K k : keySet()) { for (K k : keySet()) {
......
...@@ -44,8 +44,6 @@ H:3,... ...@@ -44,8 +44,6 @@ H:3,...
TODO: TODO:
- rolling docs review: at convert "Features" to top-level (linked) entries - rolling docs review: at convert "Features" to top-level (linked) entries
- mvcc with multiple transactions
- test LZ4
- additional test async write / read algorithm for speed and errors - additional test async write / read algorithm for speed and errors
- move setters to the builder, except for setRetainVersion, setReuseSpace, - move setters to the builder, except for setRetainVersion, setReuseSpace,
and settings that are persistent (setStoreVersion) and settings that are persistent (setStoreVersion)
...@@ -96,7 +94,9 @@ TODO: ...@@ -96,7 +94,9 @@ TODO:
-- use a transaction log where only the deltas are stored -- use a transaction log where only the deltas are stored
- serialization for lists, sets, sets, sorted sets, maps, sorted maps - serialization for lists, sets, sets, sorted sets, maps, sorted maps
- maybe rename 'rollback' to 'revert' - maybe rename 'rollback' to 'revert'
- support other compression algorithms (deflate,...) - support other compression algorithms (deflate, LZ4,...)
- only retain the last version, unless explicitly set (setRetainVersion)
- unit test for the FreeSpaceList; maybe find a simpler implementation
*/ */
...@@ -155,12 +155,11 @@ public class MVStore { ...@@ -155,12 +155,11 @@ public class MVStore {
/** /**
* The map of temporarily freed entries in the chunks. The key is the * The map of temporarily removed pages. The key is the unsaved version, the
* unsaved version, the value is the map of chunks. The maps of chunks * value is the map of chunks. The maps of chunks contains the number of
* contains the number of freed entries per chunk. * freed entries per chunk. Access is synchronized.
* Access is synchronized.
*/ */
private final HashMap<Long, HashMap<Integer, Chunk>> freedChunks = New.hashMap(); private final HashMap<Long, HashMap<Integer, Chunk>> freedPages = New.hashMap();
private MVMapConcurrent<String, String> meta; private MVMapConcurrent<String, String> meta;
...@@ -227,8 +226,8 @@ public class MVStore { ...@@ -227,8 +226,8 @@ public class MVStore {
MVStore(HashMap<String, Object> config) { MVStore(HashMap<String, Object> config) {
String f = (String) config.get("fileName"); String f = (String) config.get("fileName");
if (f != null && !f.startsWith("nio:")) { if (f != null && f.indexOf(':') < 0) {
// NIO is used by default // NIO is used, unless a different file system is specified
// the following line is to ensure the NIO file system is compiled // the following line is to ensure the NIO file system is compiled
FilePathNio.class.getName(); FilePathNio.class.getName();
f = "nio:" + f; f = "nio:" + f;
...@@ -551,23 +550,32 @@ public class MVStore { ...@@ -551,23 +550,32 @@ public class MVStore {
lastChunkId = header.id; lastChunkId = header.id;
chunks.put(header.id, header); chunks.put(header.id, header);
meta.setRootPos(header.metaRootPos, -1); meta.setRootPos(header.metaRootPos, -1);
Iterator<String> it = meta.keyIterator("chunk."); // load chunks in reverse order, because data about previous chunks
while (it.hasNext()) { // might only be available in later chunks
String s = it.next(); // if this is a performance problem when there are many
if (!s.startsWith("chunk.")) { // chunks, the id of the previous / next chunk might need to
break; // be maintained
for (int id = lastChunkId; id >= 0; id--) {
String s = meta.get("chunk." + id);
if (s == null) {
continue;
} }
Chunk c = Chunk.fromString(meta.get(s)); Chunk c = Chunk.fromString(s);
if (c.id == header.id) { if (c.id == header.id) {
c.start = header.start; c.start = header.start;
c.length = header.length; c.length = header.length;
c.metaRootPos = header.metaRootPos; c.metaRootPos = header.metaRootPos;
c.maxLengthLive = header.maxLengthLive;
c.pageCount = header.pageCount; c.pageCount = header.pageCount;
c.pageCountLive = header.pageCountLive;
c.maxLength = header.maxLength; c.maxLength = header.maxLength;
c.maxLengthLive = header.maxLengthLive;
} }
lastChunkId = Math.max(c.id, lastChunkId); lastChunkId = Math.max(c.id, lastChunkId);
chunks.put(c.id, c); chunks.put(c.id, c);
if (c.pageCountLive == 0) {
// remove this chunk in the next save operation
registerFreePage(currentVersion, c.id, 0, 0);
}
} }
// rebuild the free space list // rebuild the free space list
freeSpaceList.clear(); freeSpaceList.clear();
...@@ -835,7 +843,6 @@ public class MVStore { ...@@ -835,7 +843,6 @@ public class MVStore {
c.maxLengthLive = Long.MAX_VALUE; c.maxLengthLive = Long.MAX_VALUE;
c.start = Long.MAX_VALUE; c.start = Long.MAX_VALUE;
c.length = Integer.MAX_VALUE; c.length = Integer.MAX_VALUE;
time = Math.max(0, time - creationTime);
c.time = time; c.time = time;
c.version = version; c.version = version;
chunks.put(c.id, c); chunks.put(c.id, c);
...@@ -848,7 +855,9 @@ public class MVStore { ...@@ -848,7 +855,9 @@ public class MVStore {
if (v >= 0 && m.getVersion() >= lastStoredVersion) { if (v >= 0 && m.getVersion() >= lastStoredVersion) {
MVMap<?, ?> r = m.openVersion(storeVersion); MVMap<?, ?> r = m.openVersion(storeVersion);
r.waitUntilWritten(r.getRoot()); r.waitUntilWritten(r.getRoot());
changed.add(r); if (r.getRoot().getPos() == 0) {
changed.add(r);
}
} }
} }
} }
...@@ -859,23 +868,8 @@ public class MVStore { ...@@ -859,23 +868,8 @@ public class MVStore {
} else { } else {
meta.put("root." + m.getId(), String.valueOf(Integer.MAX_VALUE)); meta.put("root." + m.getId(), String.valueOf(Integer.MAX_VALUE));
} }
} }
Set<Chunk> removedChunks = applyFreedPages(storeVersion, time);
applyFreedChunks(storeVersion);
Set<Integer> removedChunks = New.hashSet();
// do it twice, because changing the meta table
// could cause a chunk to become empty
for (int i = 0; i < 2; i++) {
for (Chunk x : chunks.values()) {
if (x.maxLengthLive == 0 && canOverwriteChunk(x, time)) {
meta.remove("chunk." + x.id);
removedChunks.add(x.id);
} else {
meta.put("chunk." + x.id, x.asString());
}
applyFreedChunks(storeVersion);
}
}
ByteBuffer buff; ByteBuffer buff;
if (writeBuffer != null) { if (writeBuffer != null) {
buff = writeBuffer; buff = writeBuffer;
...@@ -899,7 +893,7 @@ public class MVStore { ...@@ -899,7 +893,7 @@ public class MVStore {
meta.put("chunk." + c.id, c.asString()); meta.put("chunk." + c.id, c.asString());
if (ASSERT) { if (ASSERT) {
if (freedChunks.size() > 0) { if (freedPages.size() > 0) {
throw DataUtils.newIllegalStateException("Temporary freed chunks"); throw DataUtils.newIllegalStateException("Temporary freed chunks");
} }
} }
...@@ -920,13 +914,9 @@ public class MVStore { ...@@ -920,13 +914,9 @@ public class MVStore {
long filePos = reuseSpace ? allocateChunk(length) : fileLength; long filePos = reuseSpace ? allocateChunk(length) : fileLength;
boolean storeAtEndOfFile = filePos + length >= fileLength; boolean storeAtEndOfFile = filePos + length >= fileLength;
// need to keep old chunks // free up the space of unused chunks now
// until they are are no longer referenced for (Chunk x : removedChunks) {
// by an old version freeSpaceList.markFree(x);
// so empty space is not reused too early
for (int x : removedChunks) {
freeSpaceList.markFree(chunks.get(x));
chunks.remove(x);
} }
c.start = filePos; c.start = filePos;
...@@ -971,7 +961,6 @@ public class MVStore { ...@@ -971,7 +961,6 @@ public class MVStore {
currentStoreVersion = -1; currentStoreVersion = -1;
metaChanged = false; metaChanged = false;
lastStoredVersion = storeVersion; lastStoredVersion = storeVersion;
return version; return version;
} }
...@@ -990,25 +979,64 @@ public class MVStore { ...@@ -990,25 +979,64 @@ public class MVStore {
return System.currentTimeMillis() - creationTime; return System.currentTimeMillis() - creationTime;
} }
private void applyFreedChunks(long storeVersion) { /**
synchronized (freedChunks) { * Apply the freed pages to the chunk metadata. The metadata is updated, but
for (Iterator<Long> it = freedChunks.keySet().iterator(); it.hasNext();) { * freed chunks are not removed yet.
long v = it.next(); *
if (v > storeVersion) { * @param storeVersion apply up to the given version
continue; * @return the set of freed chunks (might be empty)
*/
private Set<Chunk> applyFreedPages(long storeVersion, long time) {
Set<Chunk> removedChunks = New.hashSet();
synchronized (freedPages) {
while (true) {
ArrayList<Chunk> modified = New.arrayList();
for (Iterator<Long> it = freedPages.keySet().iterator(); it.hasNext();) {
long v = it.next();
if (v > storeVersion) {
continue;
}
Map<Integer, Chunk> freed = freedPages.get(v);
for (Chunk f : freed.values()) {
Chunk c = chunks.get(f.id);
c.maxLengthLive += f.maxLengthLive;
c.pageCountLive += f.pageCountLive;
if (c.pageCountLive < 0) {
throw DataUtils.newIllegalStateException(
"Corrupt page count {0}", c.pageCountLive);
}
if (c.maxLengthLive < 0) {
throw DataUtils.newIllegalStateException(
"Corrupt max length {0}", c.maxLengthLive);
}
if (c.pageCount == 0 && c.maxLengthLive > 0) {
throw DataUtils.newIllegalStateException(
"Corrupt max length {0}", c.maxLengthLive);
}
modified.add(c);
}
it.remove();
} }
Map<Integer, Chunk> freed = freedChunks.get(v); for (Chunk c : modified) {
for (Chunk f : freed.values()) { if (c.maxLengthLive == 0) {
Chunk c = chunks.get(f.id); if (canOverwriteChunk(c, time)) {
c.maxLengthLive += f.maxLengthLive; removedChunks.add(c);
if (c.maxLengthLive < 0) { chunks.remove(c.id);
throw DataUtils.newIllegalStateException( meta.remove("chunk." + c.id);
"Corrupt max length {0}", c.maxLengthLive); } else {
// remove this chunk in the next save operation
registerFreePage(storeVersion + 1, c.id, 0, 0);
}
} else {
meta.put("chunk." + c.id, c.asString());
} }
} }
it.remove(); if (modified.size() == 0) {
break;
}
} }
} }
return removedChunks;
} }
/** /**
...@@ -1267,18 +1295,23 @@ public class MVStore { ...@@ -1267,18 +1295,23 @@ public class MVStore {
// can be re-used // can be re-used
version = currentStoreVersion; version = currentStoreVersion;
} }
synchronized (freedChunks) { registerFreePage(version, c.id, DataUtils.getPageMaxLength(pos), 1);
HashMap<Integer, Chunk>freed = freedChunks.get(version); }
private void registerFreePage(long version, int chunkId, long maxLengthLive, int pageCount) {
synchronized (freedPages) {
HashMap<Integer, Chunk>freed = freedPages.get(version);
if (freed == null) { if (freed == null) {
freed = New.hashMap(); freed = New.hashMap();
freedChunks.put(version, freed); freedPages.put(version, freed);
} }
Chunk f = freed.get(c.id); Chunk f = freed.get(chunkId);
if (f == null) { if (f == null) {
f = new Chunk(c.id); f = new Chunk(chunkId);
freed.put(c.id, f); freed.put(chunkId, f);
} }
f.maxLengthLive -= DataUtils.getPageMaxLength(pos); f.maxLengthLive -= maxLengthLive;
f.pageCountLive -= pageCount;
} }
} }
...@@ -1492,8 +1525,8 @@ public class MVStore { ...@@ -1492,8 +1525,8 @@ public class MVStore {
chunks.clear(); chunks.clear();
freeSpaceList.clear(); freeSpaceList.clear();
maps.clear(); maps.clear();
synchronized (freedChunks) { synchronized (freedPages) {
freedChunks.clear(); freedPages.clear();
} }
currentVersion = version; currentVersion = version;
metaChanged = false; metaChanged = false;
...@@ -1506,10 +1539,10 @@ public class MVStore { ...@@ -1506,10 +1539,10 @@ public class MVStore {
m.rollbackTo(version); m.rollbackTo(version);
} }
for (long v = currentVersion; v >= version; v--) { for (long v = currentVersion; v >= version; v--) {
if (freedChunks.size() == 0) { if (freedPages.size() == 0) {
break; break;
} }
freedChunks.remove(v); freedPages.remove(v);
} }
meta.rollbackTo(version); meta.rollbackTo(version);
metaChanged = false; metaChanged = false;
...@@ -1556,8 +1589,8 @@ public class MVStore { ...@@ -1556,8 +1589,8 @@ public class MVStore {
} }
private void revertTemp(long storeVersion) { private void revertTemp(long storeVersion) {
synchronized (freedChunks) { synchronized (freedPages) {
for (Iterator<Long> it = freedChunks.keySet().iterator(); it.hasNext();) { for (Iterator<Long> it = freedPages.keySet().iterator(); it.hasNext();) {
long v = it.next(); long v = it.next();
if (v > storeVersion) { if (v > storeVersion) {
continue; continue;
......
...@@ -814,11 +814,15 @@ public class Page { ...@@ -814,11 +814,15 @@ public class Page {
^ DataUtils.getCheckValue(start) ^ DataUtils.getCheckValue(start)
^ DataUtils.getCheckValue(pageLength); ^ DataUtils.getCheckValue(pageLength);
buff.putShort(start + 4, (short) check); buff.putShort(start + 4, (short) check);
this.pos = DataUtils.getPagePos(chunkId, start, pageLength, type); if (pos != 0) {
throw DataUtils.newIllegalStateException("Page already stored");
}
pos = DataUtils.getPagePos(chunkId, start, pageLength, type);
long max = DataUtils.getPageMaxLength(pos); long max = DataUtils.getPageMaxLength(pos);
chunk.maxLength += max; chunk.maxLength += max;
chunk.maxLengthLive += max; chunk.maxLengthLive += max;
chunk.pageCount++; chunk.pageCount++;
chunk.pageCountLive++;
return buff; return buff;
} }
......
...@@ -90,6 +90,7 @@ public class TransactionStore { ...@@ -90,6 +90,7 @@ public class TransactionStore {
MVMapConcurrent.Builder<long[], Object[]> builder = MVMapConcurrent.Builder<long[], Object[]> builder =
new MVMapConcurrent.Builder<long[], Object[]>(). new MVMapConcurrent.Builder<long[], Object[]>().
valueType(valueType); valueType(valueType);
// TODO escape other map names, to avoid conflicts
undoLog = store.openMap("undoLog", builder); undoLog = store.openMap("undoLog", builder);
init(); init();
} }
...@@ -220,8 +221,9 @@ public class TransactionStore { ...@@ -220,8 +221,9 @@ public class TransactionStore {
void commit(Transaction t, long maxLogId) { void commit(Transaction t, long maxLogId) {
store.incrementVersion(); store.incrementVersion();
for (long logId = 0; logId < maxLogId; logId++) { for (long logId = 0; logId < maxLogId; logId++) {
Object[] op = undoLog.get(new long[] { long[] undoKey = new long[] {
t.getId(), logId }); t.getId(), logId };
Object[] op = undoLog.get(undoKey);
int opType = (Integer) op[0]; int opType = (Integer) op[0];
if (opType == Transaction.OP_REMOVE) { if (opType == Transaction.OP_REMOVE) {
int mapId = (Integer) op[1]; int mapId = (Integer) op[1];
...@@ -238,7 +240,7 @@ public class TransactionStore { ...@@ -238,7 +240,7 @@ public class TransactionStore {
map.remove(key); map.remove(key);
} }
} }
undoLog.remove(logId); undoLog.remove(undoKey);
} }
openTransactions.remove(t.getId()); openTransactions.remove(t.getId());
openTransactionMap.remove(t.getId()); openTransactionMap.remove(t.getId());
...@@ -858,6 +860,7 @@ public class TransactionStore { ...@@ -858,6 +860,7 @@ public class TransactionStore {
} }
public void removeMap() { public void removeMap() {
// TODO remove in a transaction
mapWrite.removeMap(); mapWrite.removeMap();
} }
......
...@@ -116,7 +116,8 @@ public class MVTableEngine implements TableEngine { ...@@ -116,7 +116,8 @@ public class MVTableEngine implements TableEngine {
* @param store the store * @param store the store
*/ */
static void store(MVStore store) { static void store(MVStore store) {
store.compact(50); int test;
// store.compact(50);
store.store(); store.store();
} }
......
...@@ -203,6 +203,9 @@ java org.pitest.mutationtest.MutationCoverageReport ...@@ -203,6 +203,9 @@ java org.pitest.mutationtest.MutationCoverageReport
--targetTests org.h2.test.store.TestStreamStore --targetTests org.h2.test.store.TestStreamStore
--sourceDirs src/test,src/tools --sourceDirs src/test,src/tools
Dump heap on out of memory:
-XX:+HeapDumpOnOutOfMemoryError
Random test: Random test:
java15 java15
cd h2database/h2/bin cd h2database/h2/bin
...@@ -536,6 +539,7 @@ kill -9 `jps -l | grep "org.h2.test." | cut -d " " -f 1` ...@@ -536,6 +539,7 @@ kill -9 `jps -l | grep "org.h2.test." | cut -d " " -f 1`
ssl = false; ssl = false;
traceLevelFile = 0; traceLevelFile = 0;
test(); test();
testUnit();
big = false; big = false;
cipher = "AES"; cipher = "AES";
......
...@@ -600,9 +600,9 @@ public abstract class TestBase { ...@@ -600,9 +600,9 @@ public abstract class TestBase {
*/ */
public void assertEquals(java.util.Date expected, java.util.Date actual) { public void assertEquals(java.util.Date expected, java.util.Date actual) {
if (expected != actual && !expected.equals(actual)) { if (expected != actual && !expected.equals(actual)) {
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
final SimpleTimeZone GMT_TIMEZONE = new SimpleTimeZone(0, "Z"); SimpleTimeZone gmt = new SimpleTimeZone(0, "Z");
df.setTimeZone(GMT_TIMEZONE); df.setTimeZone(gmt);
fail("Expected: " + df.format(expected) + " actual: " + df.format(actual)); fail("Expected: " + df.format(expected) + " actual: " + df.format(actual));
} }
} }
......
...@@ -8,7 +8,6 @@ package org.h2.test.store; ...@@ -8,7 +8,6 @@ package org.h2.test.store;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
...@@ -22,7 +21,6 @@ import org.h2.mvstore.type.StringDataType; ...@@ -22,7 +21,6 @@ import org.h2.mvstore.type.StringDataType;
import org.h2.store.fs.FilePath; import org.h2.store.fs.FilePath;
import org.h2.store.fs.FileUtils; import org.h2.store.fs.FileUtils;
import org.h2.test.TestBase; import org.h2.test.TestBase;
import org.h2.util.New;
/** /**
* Tests the MVStore. * Tests the MVStore.
...@@ -35,7 +33,10 @@ public class TestMVStore extends TestBase { ...@@ -35,7 +33,10 @@ public class TestMVStore extends TestBase {
* @param a ignored * @param a ignored
*/ */
public static void main(String... a) throws Exception { public static void main(String... a) throws Exception {
TestBase.createCaller().init().test(); TestBase test = TestBase.createCaller().init();
test.config.traceTest = true;
test.config.big = true;
test.test();
} }
public void test() throws Exception { public void test() throws Exception {
...@@ -61,7 +62,6 @@ public class TestMVStore extends TestBase { ...@@ -61,7 +62,6 @@ public class TestMVStore extends TestBase {
testIterateOldVersion(); testIterateOldVersion();
testObjects(); testObjects();
testExample(); testExample();
testIterateOverChanges();
testOpenStoreCloseLoop(); testOpenStoreCloseLoop();
testVersion(); testVersion();
testTruncateFile(); testTruncateFile();
...@@ -79,10 +79,10 @@ public class TestMVStore extends TestBase { ...@@ -79,10 +79,10 @@ public class TestMVStore extends TestBase {
testIterate(); testIterate();
testCloseTwice(); testCloseTwice();
testSimple(); testSimple();
// this test will run out of memory on a 32-bit VM before it gets to the part we need it to test
if ("64".equals(System.getProperty("sun.arch.data.model"))) { // longer running tests
testLargerThan2G();
} testLargerThan2G();
} }
private void testAtomicOperations() { private void testAtomicOperations() {
...@@ -705,39 +705,6 @@ public class TestMVStore extends TestBase { ...@@ -705,39 +705,6 @@ public class TestMVStore extends TestBase {
} }
} }
private void testIterateOverChanges() {
String fileName = getBaseDir() + "/testIterate.h3";
FileUtils.delete(fileName);
MVStore s = openStore(fileName);
s.setPageSize(6);
MVMap<Integer, String> m = s.openMap("data");
for (int i = 0; i < 100; i++) {
m.put(i, "Hi");
}
s.incrementVersion();
s.store();
for (int i = 20; i < 40; i++) {
assertEquals("Hi", m.put(i, "Hello"));
}
long old = s.getCurrentVersion();
s.incrementVersion();
for (int i = 10; i < 15; i++) {
m.put(i, "Hallo");
}
m.put(50, "Hallo");
for (int i = 90; i < 93; i++) {
assertEquals("Hi", m.remove(i));
}
assertEquals(null, m.put(100, "Hallo"));
Iterator<Integer> it = m.changeIterator(old);
ArrayList<Integer> list = New.arrayList();
while (it.hasNext()) {
list.add(it.next());
}
assertEquals("[10, 11, 12, 13, 14, 50, 100, 90, 91, 92]", list.toString());
s.close();
}
private void testOldVersion() { private void testOldVersion() {
MVStore s; MVStore s;
for (int op = 0; op <= 1; op++) { for (int op = 0; op <= 1; op++) {
...@@ -1031,8 +998,8 @@ public class TestMVStore extends TestBase { ...@@ -1031,8 +998,8 @@ public class TestMVStore extends TestBase {
assertEquals("name:data", m.get("map." + id)); assertEquals("name:data", m.get("map." + id));
assertTrue(m.get("root.1").length() > 0); assertTrue(m.get("root.1").length() > 0);
assertTrue(m.containsKey("chunk.1")); assertTrue(m.containsKey("chunk.1"));
assertEquals("id:1,length:246,maxLength:224,maxLengthLive:0," + assertEquals("id:1,length:263,maxLength:288,maxLengthLive:288," +
"metaRoot:274877910922,pageCount:2," + "metaRoot:274877910924,pageCount:2,pageCountLive:2," +
"start:8192,time:0,version:1", m.get("chunk.1")); "start:8192,time:0,version:1", m.get("chunk.1"));
assertTrue(m.containsKey("chunk.2")); assertTrue(m.containsKey("chunk.2"));
...@@ -1397,30 +1364,36 @@ public class TestMVStore extends TestBase { ...@@ -1397,30 +1364,36 @@ public class TestMVStore extends TestBase {
s.close(); s.close();
} }
private void testLargerThan2G() { private void testLargerThan2G() throws IOException {
if (!config.big) {
return;
}
String fileName = getBaseDir() + "/testLargerThan2G.h3"; String fileName = getBaseDir() + "/testLargerThan2G.h3";
FileUtils.delete(fileName); FileUtils.delete(fileName);
MVStore store = new MVStore.Builder().cacheSize(16).
MVStore store = new MVStore.Builder().cacheSize(800).fileName(fileName).writeBufferSize(0).writeDelay(-1) fileName(fileName).open();
.open(); MVMap<Integer, String> map = store.openMap("test");
long last = System.currentTimeMillis();
MVMap<String, String> map = store.openMap("test"); String data = new String(new char[2500]).replace((char) 0, 'x');
int totalWrite = 15000000; for (int i = 0;; i++) {
int lineStored = 0; map.put(i, data);
while (lineStored <= totalWrite) { if (i % 10000 == 0) {
lineStored++; long version = store.commit();
String actualKey = lineStored + " just for length length length " + lineStored; store.setRetainVersion(version);
String value = "Just a a string that is kinda long long long but not too much much much much much much much much much " long time = System.currentTimeMillis();
+ lineStored; if (time - last > 2000) {
long mb = store.getFile().size() / 1024 / 1024;
map.put(actualKey, value); trace(mb + "/4500");
if (mb > 4500) {
if (lineStored % 1000000 == 0) { break;
store.store(); }
last = time;
}
} }
} }
store.store(); store.commit();
store.close(); store.close();
FileUtils.delete(fileName);
} }
/** /**
......
...@@ -36,7 +36,7 @@ public class TestMVTableEngine extends TestBase { ...@@ -36,7 +36,7 @@ public class TestMVTableEngine extends TestBase {
public void test() throws Exception { public void test() throws Exception {
testEncryption(); testEncryption();
testReadOnly(); testReadOnly();
// testReuseDiskSpace(); testReuseDiskSpace();
testDataTypes(); testDataTypes();
testLocking(); testLocking();
testSimple(); testSimple();
...@@ -87,8 +87,8 @@ public class TestMVTableEngine extends TestBase { ...@@ -87,8 +87,8 @@ public class TestMVTableEngine extends TestBase {
";DEFAULT_TABLE_ENGINE=org.h2.mvstore.db.MVTableEngine"; ";DEFAULT_TABLE_ENGINE=org.h2.mvstore.db.MVTableEngine";
Connection conn; Connection conn;
Statement stat; Statement stat;
long maxSizeHalf = 0; long maxSize = 0;
for (int i = 0; i < 10; i++) { for (int i = 0; i < 20; i++) {
conn = getConnection(dbName); conn = getConnection(dbName);
for (MVTableEngine.Store s : MVTableEngine.getStores()) { for (MVTableEngine.Store s : MVTableEngine.getStores()) {
s.getStore().setRetentionTime(0); s.getStore().setRetentionTime(0);
...@@ -100,10 +100,10 @@ public class TestMVTableEngine extends TestBase { ...@@ -100,10 +100,10 @@ public class TestMVTableEngine extends TestBase {
conn.close(); conn.close();
long size = FileUtils.size(getBaseDir() + "/mvstore" long size = FileUtils.size(getBaseDir() + "/mvstore"
+ Constants.SUFFIX_MV_FILE); + Constants.SUFFIX_MV_FILE);
if (i < 6) { if (i < 10) {
maxSizeHalf = Math.max(size, maxSizeHalf); maxSize = (int) (Math.max(size, maxSize) * 1.1);
} else if (size > maxSizeHalf) { } else if (size > maxSize) {
fail(i + " size: " + size + " max: " + maxSizeHalf); fail(i + " size: " + size + " max: " + maxSize);
} }
} }
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论