提交 d3d3a11f authored 作者: Thomas Mueller's avatar Thomas Mueller

A persistent multi-version map: open the file in exclusive or shared mode; allow…

A persistent multi-version map: open the file in exclusive or shared mode; allow to read and update the file header.
上级 d6ffbca2
......@@ -405,7 +405,7 @@ class FileDisk extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
return file.getChannel().tryLock();
return file.getChannel().tryLock(position, size, shared);
}
public void implCloseChannel() throws IOException {
......
......@@ -216,7 +216,7 @@ class FilePathMemLZF extends FilePathMem {
*/
class FileMem extends FileBase {
private final FileMemData data;
final FileMemData data;
private final boolean readOnly;
private long pos;
......@@ -282,8 +282,29 @@ class FileMem extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
if (shared) {
if (!data.lockShared()) {
return null;
}
} else {
if (!data.lockExclusive()) {
return null;
}
}
FileLock lock = new FileLock(null, position, size, shared) {
@Override
public boolean isValid() {
return true;
}
@Override
public void release() throws IOException {
data.unlock();
}
};
return lock;
}
public String toString() {
return data.getName();
......@@ -314,6 +335,8 @@ class FileMemData {
private byte[][] data;
private long lastModified;
private boolean isReadOnly;
private boolean isLockedExclusive;
private int sharedLockCount;
static {
byte[] n = new byte[BLOCK_SIZE];
......@@ -322,6 +345,37 @@ class FileMemData {
System.arraycopy(BUFFER, 0, COMPRESSED_EMPTY_BLOCK, 0, len);
}
FileMemData(String name, boolean compress) {
this.name = name;
this.compress = compress;
data = new byte[0][];
lastModified = System.currentTimeMillis();
}
synchronized boolean lockExclusive() {
if (sharedLockCount > 0 || isLockedExclusive) {
return false;
}
isLockedExclusive = true;
return true;
}
synchronized boolean lockShared() {
if (isLockedExclusive) {
return false;
}
sharedLockCount++;
return true;
}
synchronized void unlock() {
if (isLockedExclusive) {
isLockedExclusive = false;
} else {
sharedLockCount = Math.max(0, sharedLockCount - 1);
}
}
/**
* This small cache compresses the data if an element leaves the cache.
*/
......@@ -371,13 +425,7 @@ class FileMemData {
}
return false;
}
}
FileMemData(String name, boolean compress) {
this.name = name;
this.compress = compress;
data = new byte[0][];
lastModified = System.currentTimeMillis();
}
private static void compressLater(byte[][] data, int page) {
......
......@@ -98,7 +98,7 @@ class FileNio extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
return channel.tryLock();
return channel.tryLock(position, size, shared);
}
public String toString() {
......
......@@ -233,7 +233,7 @@ class FileNioMapped extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
return file.getChannel().tryLock();
return file.getChannel().tryLock(position, size, shared);
}
}
......@@ -172,7 +172,7 @@ class FileRec extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
return channel.tryLock();
return channel.tryLock(position, size, shared);
}
}
......@@ -370,7 +370,7 @@ class FileSplit extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
return list[0].tryLock();
return list[0].tryLock(position, size, shared);
}
public String toString() {
......
......@@ -316,6 +316,19 @@ class FileZip extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
if (shared) {
return new FileLock(null, position, size, shared) {
@Override
public boolean isValid() {
return true;
}
@Override
public void release() throws IOException {
// ignore
}};
}
return null;
}
......
......@@ -10,6 +10,7 @@ import java.util.Map;
import java.util.Random;
import org.h2.dev.store.btree.MVMap;
import org.h2.dev.store.btree.MVStore;
import org.h2.dev.store.btree.MVStoreBuilder;
import org.h2.test.TestBase;
import org.h2.util.Task;
......@@ -34,7 +35,7 @@ public class TestConcurrent extends TestMVStore {
}
private void testConcurrentIterate() {
MVStore s = MVStore.open(null, new TestMapFactory());
MVStore s = MVStoreBuilder.inMemory().with(new TestMapFactory()).open();
s.setMaxPageSize(3);
final MVMap<Integer, Integer> map = s.openMap("test");
final int len = 10;
......
......@@ -8,6 +8,7 @@ package org.h2.test.store;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import org.h2.dev.store.btree.DataUtils;
import org.h2.test.TestBase;
......@@ -27,6 +28,7 @@ public class TestDataUtils extends TestBase {
}
public void test() throws Exception {
testFletcher();
testMap();
testMaxShortVarIntVarLong();
testVarIntVarLong();
......@@ -35,6 +37,29 @@ public class TestDataUtils extends TestBase {
testEncodeLength();
}
private void testFletcher() throws Exception {
byte[] data = new byte[10000];
for (int i = 0; i < 10000; i += 1000) {
assertEquals(-1, DataUtils.getFletcher32(data, i));
}
Arrays.fill(data, (byte) 255);
for (int i = 0; i < 10000; i += 1000) {
assertEquals(-1, DataUtils.getFletcher32(data, i));
}
long last = 0;
for (int i = 1; i < 255; i++) {
Arrays.fill(data, (byte) i);
for (int j = 0; j < 10; j += 2) {
int x = DataUtils.getFletcher32(data, j);
assertTrue(x != last);
last = x;
}
}
Arrays.fill(data, (byte) 10);
assertEquals(0x1e1e1414, DataUtils.getFletcher32(data, 10000));
assertEquals(0x1e3fa7ed, DataUtils.getFletcher32("Fletcher32".getBytes(), 10));
}
private void testMap() {
StringBuilder buff = new StringBuilder();
DataUtils.appendMap(buff, "", "");
......
......@@ -64,7 +64,7 @@ public class TestMVRTree extends TestMVStore {
SpatialKey k = new SpatialKey(i, x - p, x + p, y - p, y + p);
r.add(k, "" + i);
if (i > 0 && (i % len / 10) == 0) {
s.save();
s.store();
}
if (i > 0 && (i % 10000) == 0) {
render(r, getBaseDir() + "/test.png");
......@@ -72,7 +72,7 @@ public class TestMVRTree extends TestMVStore {
}
// System.out.println(prof.getTop(5));
// System.out.println("add: " + (System.currentTimeMillis() - t));
s.save();
s.store();
s.close();
s = openStore(fileName);
r = s.openMap("data", "r", "s2", "");
......
......@@ -5,6 +5,9 @@
*/
package org.h2.test.store;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
......@@ -13,6 +16,8 @@ import java.util.TreeMap;
import org.h2.dev.store.btree.Cursor;
import org.h2.dev.store.btree.MVMap;
import org.h2.dev.store.btree.MVStore;
import org.h2.dev.store.btree.MVStoreBuilder;
import org.h2.store.fs.FilePath;
import org.h2.store.fs.FileUtils;
import org.h2.test.TestBase;
import org.h2.util.New;
......@@ -31,10 +36,14 @@ public class TestMVStore extends TestBase {
TestBase.createCaller().init().test();
}
public void test() throws InterruptedException {
public void test() throws Exception {
FileUtils.deleteRecursive(getBaseDir(), true);
testConcurrentOpen();
testFileHeader();
testFileHeaderCorruption();
testIndexSkip();
testMinMaxNextKey();
testSetting();
testStoreVersion();
testIterateOldVersion();
testObjects();
testExample();
......@@ -58,6 +67,80 @@ public class TestMVStore extends TestBase {
testSimple();
}
private void testConcurrentOpen() {
String fileName = getBaseDir() + "/testConcurrentOpen.h3";
MVStore s = MVStoreBuilder.fileBased(fileName).open();
try {
MVStore s1 = MVStoreBuilder.fileBased(fileName).open();
s1.close();
fail();
} catch (Exception e) {
// expected
}
try {
MVStore s1 = MVStoreBuilder.fileBased(fileName).readOnly().open();
s1.close();
fail();
} catch (Exception e) {
// expected
}
s.close();
}
private void testFileHeader() {
String fileName = getBaseDir() + "/testFileHeader.h3";
MVStore s = openStore(fileName);
assertEquals("3", s.getFileHeader().get("H"));
s.getFileHeader().put("test", "123");
MVMap<Integer, Integer> map = s.openMap("test");
map.put(10, 100);
s.store();
s.close();
s = openStore(fileName);
assertEquals("123", s.getFileHeader().get("test"));
s.close();
}
private void testFileHeaderCorruption() throws IOException {
String fileName = getBaseDir() + "/testFileHeader.h3";
MVStore s = openStore(fileName);
MVMap<Integer, Integer> map = s.openMap("test");
map.put(10, 100);
FilePath f = FilePath.get(s.getFileName());
s.close();
int blockSize = 4 * 1024;
// test corrupt file headers
for (int i = 0; i <= blockSize; i += blockSize) {
FileChannel fc = f.open("rw");
ByteBuffer buff = ByteBuffer.allocate(4 * 1024);
fc.read(buff, i);
String h = new String(buff.array(), "UTF-8").trim();
int idx = h.indexOf("fletcher:");
int old = Character.digit(h.charAt(idx + "fletcher:".length()), 16);
int bad = (old + 1) & 15;
buff.put(idx + "fletcher:".length(),
(byte) Character.forDigit(bad, 16));
buff.rewind();
fc.write(buff, i);
fc.close();
if (i == 0) {
// if the first header is corrupt, the second
// header should be used
s = openStore(fileName);
map = s.openMap("test");
s.close();
} else {
// both headers are corrupt
try {
s = openStore(fileName);
fail();
} catch (Exception e) {
// expected
}
}
}
}
private void testIndexSkip() {
MVStore s = openStore(null);
s.setMaxPageSize(4);
......@@ -167,37 +250,30 @@ public class TestMVStore extends TestBase {
}
}
private void testSetting() {
String fileName = getBaseDir() + "/testSetting.h3";
private void testStoreVersion() {
String fileName = getBaseDir() + "/testStoreVersion.h3";
FileUtils.delete(fileName);
MVStore s = MVStore.open(fileName);
assertEquals(0, s.getCurrentVersion());
assertEquals(0, s.getStoreVersion());
s.setStoreVersion(1);
assertEquals(null, s.getSetting("hello"));
s.setSetting("test", "Hello");
assertEquals("Hello", s.getSetting("test"));
s.close();
s = MVStore.open(fileName);
assertEquals(0, s.getCurrentVersion());
assertEquals(0, s.getStoreVersion());
s.setStoreVersion(1);
assertEquals(null, s.getSetting("hello"));
s.setSetting("test", "Hello");
assertEquals("Hello", s.getSetting("test"));
s.save();
s.store();
s.close();
s = MVStore.open(fileName);
assertEquals(1, s.getCurrentVersion());
assertEquals(1, s.getStoreVersion());
assertEquals("Hello", s.getSetting("test"));
s.close();
}
private void testIterateOldVersion() {
MVStore s;
Map<Integer, Integer> map;
s = MVStore.open(null, new TestMapFactory());
s = MVStoreBuilder.inMemory().with(new TestMapFactory()).open();
map = s.openMap("test");
int len = 100;
for (int i = 0; i < len; i++) {
......@@ -222,14 +298,14 @@ public class TestMVStore extends TestBase {
FileUtils.delete(fileName);
MVStore s;
Map<Object, Object> map;
s = MVStore.open(fileName, new TestMapFactory());
s = MVStoreBuilder.fileBased(fileName).with(new TestMapFactory()).open();
map = s.openMap("test");
map.put(1, "Hello");
map.put("2", 200);
map.put(new Object[1], new Object[]{1, "2"});
s.save();
s.store();
s.close();
s = MVStore.open(fileName, new TestMapFactory());
s = MVStoreBuilder.fileBased(fileName).with(new TestMapFactory()).open();
map = s.openMap("test");
assertEquals("Hello", map.get(1).toString());
assertEquals(200, ((Integer) map.get("2")).intValue());
......@@ -273,7 +349,7 @@ public class TestMVStore extends TestBase {
map.openVersion(oldVersion);
// store the newest data to disk
s.save();
s.store();
// print the old version (can be done
// concurrently with further modifications)
......@@ -301,7 +377,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 3; i++) {
Integer x = m.get("value");
m.put("value", x == null ? 0 : x + 1);
s.save();
s.store();
}
s.close();
}
......@@ -320,7 +396,7 @@ public class TestMVStore extends TestBase {
m.put(i, "Hi");
}
s.incrementVersion();
s.save();
s.store();
for (int i = 20; i < 40; i++) {
assertEquals("Hi", m.put(i, "Hello"));
}
......@@ -373,7 +449,7 @@ public class TestMVStore extends TestBase {
assertTrue(mOld.isReadOnly());
s.getCurrentVersion();
s.setRetainChunk(0);
long old2 = s.save();
long old2 = s.store();
// the old version is still available
assertEquals("Hello", mOld.get("1"));
......@@ -381,7 +457,7 @@ public class TestMVStore extends TestBase {
m.put("1", "Hi");
assertEquals("Welt", m.remove("2"));
s.save();
s.store();
s.close();
s = openStore(fileName);
......@@ -404,13 +480,13 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 1000; i++) {
m.put(i, "Hello World");
}
s.save();
s.store();
s.close();
long len = FileUtils.size(fileName);
s = openStore(fileName);
m = s.openMap("data", Integer.class, String.class);
m.clear();
s.save();
s.store();
s.compact(100);
s.close();
long len2 = FileUtils.size(fileName);
......@@ -431,18 +507,18 @@ public class TestMVStore extends TestBase {
}
assertEquals(1000, m.size());
assertEquals(281, s.getUnsavedPageCount());
s.save();
assertEquals(3, s.getWriteCount());
s.store();
assertEquals(3, s.getFileWriteCount());
s.close();
s = openStore(fileName);
m = s.openMap("data", Integer.class, String.class);
m.clear();
assertEquals(0, m.size());
s.save();
s.store();
// ensure only nodes are read, but not leaves
assertEquals(34, s.getReadCount());
assertEquals(2, s.getWriteCount());
assertEquals(34, s.getFileReadCount());
assertEquals(2, s.getFileWriteCount());
s.close();
}
......@@ -463,7 +539,7 @@ public class TestMVStore extends TestBase {
assertEquals(1, s.incrementVersion());
s.rollbackTo(1);
assertEquals("Hello", m.get("1"));
long v2 = s.save();
long v2 = s.store();
assertEquals(2, v2);
assertEquals(2, s.getCurrentVersion());
assertFalse(s.hasUnsavedChanges());
......@@ -487,7 +563,7 @@ public class TestMVStore extends TestBase {
assertNull(meta.get("map.data1"));
assertNull(m0.get("1"));
assertEquals("Hello", m.get("1"));
assertEquals(2, s.save());
assertEquals(2, s.store());
s.close();
s = openStore(fileName);
......@@ -505,7 +581,7 @@ public class TestMVStore extends TestBase {
m.put("1", "Hallo");
s.incrementVersion();
assertEquals(3, s.getCurrentVersion());
long v4 = s.save();
long v4 = s.store();
assertEquals(4, v4);
assertEquals(4, s.getCurrentVersion());
s.close();
......@@ -514,7 +590,7 @@ public class TestMVStore extends TestBase {
s.setRetainChunk(0);
m = s.openMap("data", String.class, String.class);
m.put("1", "Hello");
s.save();
s.store();
s.close();
s = openStore(fileName);
......@@ -583,11 +659,11 @@ public class TestMVStore extends TestBase {
MVMap<String, String> data = s.openMap("data", String.class, String.class);
data.put("1", "Hello");
data.put("2", "World");
s.save();
s.store();
assertEquals("1/0///", m.get("map.data"));
assertTrue(m.containsKey("chunk.1"));
assertEquals("Hello", data.put("1", "Hallo"));
s.save();
s.store();
assertEquals("1/0///", m.get("map.data"));
assertTrue(m.get("root.1").length() > 0);
assertTrue(m.containsKey("chunk.1"));
......@@ -649,10 +725,10 @@ public class TestMVStore extends TestBase {
m.put(i, o);
i++;
if (i % 10000 == 0) {
s.save();
s.store();
}
}
s.save();
s.store();
s.close();
// System.out.println(prof.getTop(5));
// System.out.println("store time " + (System.currentTimeMillis() - t));
......@@ -680,7 +756,7 @@ public class TestMVStore extends TestBase {
// p = new Profiler();
//p.startCollecting();
// t = System.currentTimeMillis();
s.save();
s.store();
// System.out.println("store: " + (System.currentTimeMillis() - t));
// System.out.println(p.getTop(5));
assertEquals("hello 0", m.remove(0));
......@@ -688,7 +764,7 @@ public class TestMVStore extends TestBase {
for (int i = 1; i < count; i++) {
assertEquals("hello " + i, m.get(i));
}
s.save();
s.store();
s.close();
s = openStore(fileName);
m = s.openMap("data", Integer.class, String.class);
......@@ -699,7 +775,7 @@ public class TestMVStore extends TestBase {
for (int i = 1; i < count; i++) {
m.remove(i);
}
s.save();
s.store();
assertNull(m.get(0));
for (int i = 0; i < count; i++) {
assertNull(m.get(i));
......@@ -717,7 +793,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 100; i++) {
m.put(j + i, "Hello " + j);
}
s.save();
s.store();
s.compact(80);
s.close();
long len = FileUtils.size(fileName);
......@@ -735,7 +811,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 100; i++) {
m.remove(i);
}
s.save();
s.store();
s.compact(80);
s.close();
// len = FileUtils.size(fileName);
......@@ -758,12 +834,12 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 10; i++) {
m.put(i, "Hello");
}
s.save();
s.store();
for (int i = 0; i < 10; i++) {
assertEquals("Hello", m.get(i));
assertEquals("Hello", m.remove(i));
}
s.save();
s.store();
s.close();
long len = FileUtils.size(fileName);
if (initialLength == 0) {
......@@ -866,7 +942,7 @@ public class TestMVStore extends TestBase {
si.put("Test", 10);
MVMap<String, String> ss = s.openMap("stringString", String.class, String.class);
ss.put("Hello", "World");
s.save();
s.store();
s.close();
s = openStore(fileName);
is = s.openMap("intString", Integer.class, String.class);
......@@ -890,7 +966,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 10; i++) {
m.put(i, "hello " + i);
}
s.save();
s.store();
it = m.keyIterator(null);
it.next();
assertThrows(UnsupportedOperationException.class, it).remove();
......@@ -921,7 +997,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 3; i++) {
m.put(i, "hello " + i);
}
s.save();
s.store();
// closing twice should be fine
s.close();
s.close();
......@@ -935,14 +1011,14 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 3; i++) {
m.put(i, "hello " + i);
}
s.save();
s.store();
assertEquals("hello 0", m.remove(0));
assertNull(m.get(0));
for (int i = 1; i < 3; i++) {
assertEquals("hello " + i, m.get(i));
}
s.save();
s.store();
s.close();
s = openStore(fileName);
......@@ -961,11 +1037,12 @@ public class TestMVStore extends TestBase {
* @return the store
*/
protected static MVStore openStore(String fileName) {
MVStore store = MVStore.open(fileName, new TestMapFactory());
MVStore store = MVStoreBuilder.fileBased(fileName).with(new TestMapFactory()).open();
store.setMaxPageSize(1000);
return store;
}
/**
* Log the message.
*
......
......@@ -5,8 +5,6 @@
*/
package org.h2.test.store;
import org.h2.compress.CompressLZF;
import org.h2.compress.Compressor;
import org.h2.dev.store.btree.MVMap;
import org.h2.dev.store.btree.MVStore;
import org.h2.dev.store.btree.DataType;
......@@ -59,9 +57,4 @@ public class TestMapFactory implements MapFactory {
throw new RuntimeException("Unsupported object class " + objectClass.toString());
}
@Override
public Compressor buildCompressor() {
return new CompressLZF();
}
}
......@@ -293,7 +293,7 @@ class FileDebug extends FileBase {
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
debug("tryLock");
return channel.tryLock();
return channel.tryLock(position, size, shared);
}
}
......@@ -209,7 +209,7 @@ class FileUnstable extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
return channel.tryLock();
return channel.tryLock(position, size, shared);
}
}
\ No newline at end of file
......@@ -188,7 +188,7 @@ class FileCrypt extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
return file.tryLock();
return file.tryLock(position, size, shared);
}
public void implCloseChannel() throws IOException {
......
......@@ -397,6 +397,19 @@ class FileZip2 extends FileBase {
}
public synchronized FileLock tryLock(long position, long size, boolean shared) throws IOException {
if (shared) {
return new FileLock(null, position, size, shared) {
@Override
public boolean isValid() {
return true;
}
@Override
public void release() throws IOException {
// ignore
}};
}
return null;
}
......
......@@ -11,6 +11,8 @@ import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import org.h2.util.New;
......@@ -425,7 +427,23 @@ public class DataUtils {
}
/**
* Append a key-value pair to the string buffer. Keys may not contain a
* Append a map to the string builder, sorted by key.
*
* @param buff the target buffer
* @param map the map
* @return the string builder
*/
public static StringBuilder appendMap(StringBuilder buff, HashMap<String, ?> map) {
ArrayList<String> list = New.arrayList(map.keySet());
Collections.sort(list);
for (String k : list) {
appendMap(buff, k, map.get(k));
}
return buff;
}
/**
* Append a key-value pair to the string builder. Keys may not contain a
* colon. Values that contain a comma or a double quote are enclosed in
* double quotes, with special characters escaped using a backslash.
*
......@@ -490,4 +508,26 @@ public class DataUtils {
return map;
}
/**
* Calculate the Fletcher32 checksum.
*
* @param bytes the bytes
* @param length the message length (must be a multiple of 2)
* @return the checksum
*/
public static int getFletcher32(byte[] bytes, int length) {
int s1 = 0xffff, s2 = 0xffff;
for (int i = 0; i < length;) {
for (int end = Math.min(i + 718, length); i < end;) {
int x = ((bytes[i++] & 0xff) << 8) | (bytes[i++] & 0xff);
s2 += s1 += x;
}
s1 = (s1 & 0xffff) + (s1 >>> 16);
s2 = (s2 & 0xffff) + (s2 >>> 16);
}
s1 = (s1 & 0xffff) + (s1 >>> 16);
s2 = (s2 & 0xffff) + (s2 >>> 16);
return (s2 << 16) | s1;
}
}
......@@ -9,12 +9,14 @@ package org.h2.dev.store.btree;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.h2.compress.CompressLZF;
import org.h2.compress.Compressor;
import org.h2.dev.store.FilePathCache;
......@@ -32,7 +34,7 @@ header: (blockSize) bytes
[ chunk ] *
(there are two headers for security)
header:
H:3,blockSize=4096,...
H:3,...
TODO:
- support custom fields in the file header (auto-server ip address,...)
......@@ -62,6 +64,13 @@ TODO:
- compact: avoid processing pages using a counting bloom filter
- defragment (re-creating maps, specially those with small pages)
- write using ByteArrayOutputStream; remove DataType.getMaxLength
- file header: check versionRead and versionWrite (and possibly rename; or rename version)
- chunk header: store changed chunk data as row; maybe after the root
- chunk checksum (header, last page, 2 bytes per page?)
- allow renaming maps
- file locking: solve problem that locks are shared for a VM
- online backup
- MapFactory is the wrong name (StorePlugin?) or is too flexible
*/
......@@ -80,6 +89,8 @@ public class MVStore {
*/
public static final StringType STRING_TYPE = new StringType();
static final int BLOCK_SIZE = 4 * 1024;
private final String fileName;
private final MapFactory mapFactory;
......@@ -88,8 +99,8 @@ public class MVStore {
private int maxPageSize = 4 * 1024;
private FileChannel file;
private FileLock fileLock;
private long fileSize;
private final int blockSize = 4 * 1024;
private long rootChunkStart;
private final CacheLongKeyLIRS<Page> cache = CacheLongKeyLIRS.newInstance(
......@@ -112,6 +123,11 @@ public class MVStore {
* The set of maps with potentially unsaved changes.
*/
private final HashMap<Integer, MVMap<?, ?>> mapsChanged = New.hashMap();
private HashMap<String, String> fileHeader = New.hashMap();
private final boolean readOnly;
private int lastMapId;
private boolean reuseSpace = true;
......@@ -121,37 +137,27 @@ public class MVStore {
private Compressor compressor;
private long currentVersion;
private int readCount;
private int writeCount;
private int fileReadCount;
private int fileWriteCount;
private int unsavedPageCount;
private MVStore(String fileName, MapFactory mapFactory) {
this.fileName = fileName;
this.mapFactory = mapFactory;
this.compressor = mapFactory == null ?
new CompressLZF() :
mapFactory.buildCompressor();
}
/**
* Open a tree store.
*
* @param fileName the file name
* @return the store
*/
public static MVStore open(String fileName) {
return open(fileName, null);
MVStore(HashMap<String, Object> config) {
this.fileName = (String) config.get("fileName");
this.mapFactory = (MapFactory) config.get("mapFactory");
this.readOnly = "r".equals(config.get("openMode"));
this.compressor = "0".equals(config.get("compression")) ? null : new CompressLZF();
}
/**
* Open a tree store.
* Open a store in exclusive mode.
*
* @param fileName the file name (null for in-memory)
* @param mapFactory the map factory
* @return the store
*/
public static MVStore open(String fileName, MapFactory mapFactory) {
MVStore s = new MVStore(fileName, mapFactory);
public static MVStore open(String fileName) {
HashMap<String, Object> config = New.hashMap();
config.put("fileName", fileName);
MVStore s = new MVStore(config);
s.open();
return s;
}
......@@ -270,7 +276,11 @@ public class MVStore {
}
/**
* Get the metadata map. It contains the following entries:
* Get the metadata map. This data is for informational purposes only. The
* data is subject to change in future versions. The data should not be
* modified (doing so may corrupt the store).
* <p>
* It contains the following entries:
*
* <pre>
* map.{name} = {mapId}/{keyType}/{valueType}
......@@ -351,7 +361,7 @@ public class MVStore {
mapsChanged.put(map.getId(), map);
}
private void open() {
void open() {
meta = new MVMap<String, String>(this, 0, "meta", STRING_TYPE, STRING_TYPE, 0);
if (fileName == null) {
return;
......@@ -360,14 +370,26 @@ public class MVStore {
try {
log("file open");
file = FilePathCache.wrap(FilePath.get(fileName).open("rw"));
if (file.tryLock() == null) {
if (readOnly) {
fileLock = file.tryLock(0, Long.MAX_VALUE, true);
if (fileLock == null) {
throw new RuntimeException("The file is locked: " + fileName);
}
} else {
fileLock = file.tryLock();
if (fileLock == null) {
throw new RuntimeException("The file is locked: " + fileName);
}
}
fileSize = file.size();
if (fileSize == 0) {
writeHeader();
fileHeader.put("H", "3");
fileHeader.put("blockSize", "" + BLOCK_SIZE);
fileHeader.put("format", "1");
fileHeader.put("formatRead", "1");
writeFileHeader();
} else {
readHeader();
readFileHeader();
if (rootChunkStart > 0) {
readMeta();
}
......@@ -400,38 +422,54 @@ public class MVStore {
}
}
private void writeHeader() {
private void readFileHeader() {
try {
ByteBuffer header = ByteBuffer.allocate(blockSize);
String h = "H:3," +
"versionRead:1," +
"versionWrite:1," +
"blockSize:" + blockSize + "," +
"rootChunk:" + rootChunkStart + "," +
"lastMapId:" + lastMapId + "," +
"version:" + currentVersion;
header.put(h.getBytes("UTF-8"));
header.rewind();
writeCount++;
DataUtils.writeFully(file, 0, header);
header.rewind();
DataUtils.writeFully(file, blockSize, header);
fileSize = Math.max(fileSize, 2 * blockSize);
byte[] headers = new byte[2 * BLOCK_SIZE];
fileReadCount++;
DataUtils.readFully(file, 0, ByteBuffer.wrap(headers));
for (int i = 0; i <= BLOCK_SIZE; i += BLOCK_SIZE) {
String s = new String(headers, i, BLOCK_SIZE, "UTF-8").trim();
fileHeader = DataUtils.parseMap(s);
rootChunkStart = Long.parseLong(fileHeader.get("rootChunk"));
currentVersion = Long.parseLong(fileHeader.get("version"));
lastMapId = Integer.parseInt(fileHeader.get("lastMapId"));
int check = (int) Long.parseLong(fileHeader.get("fletcher"), 16);
s = s.substring(0, s.lastIndexOf("fletcher") - 1) + " ";
byte[] bytes = s.getBytes("UTF-8");
int checksum = DataUtils.getFletcher32(bytes,
bytes.length / 2 * 2);
if (check == checksum) {
return;
}
}
throw new RuntimeException("File header is corrupt");
} catch (Exception e) {
throw convert(e);
}
}
private void readHeader() {
private void writeFileHeader() {
try {
byte[] headers = new byte[blockSize * 2];
readCount++;
file.read(ByteBuffer.wrap(headers), 0);
String s = new String(headers, 0, blockSize, "UTF-8").trim();
HashMap<String, String> map = DataUtils.parseMap(s);
rootChunkStart = Long.parseLong(map.get("rootChunk"));
currentVersion = Long.parseLong(map.get("version"));
lastMapId = Integer.parseInt(map.get("lastMapId"));
StringBuilder buff = new StringBuilder();
fileHeader.put("lastMapId", "" + lastMapId);
fileHeader.put("rootChunk", "" + rootChunkStart);
fileHeader.put("version", "" + currentVersion);
DataUtils.appendMap(buff, fileHeader);
byte[] bytes = (buff.toString() + " ").getBytes("UTF-8");
int checksum = DataUtils.getFletcher32(bytes, bytes.length / 2 * 2);
DataUtils.appendMap(buff, "fletcher", Integer.toHexString(checksum));
bytes = buff.toString().getBytes("UTF-8");
if (bytes.length > BLOCK_SIZE) {
throw new IllegalArgumentException("File header too large: " + buff);
}
ByteBuffer header = ByteBuffer.allocate(2 * BLOCK_SIZE);
header.put(bytes);
header.position(BLOCK_SIZE);
header.put(bytes);
header.rewind();
fileWriteCount++;
DataUtils.writeFully(file, 0, header);
fileSize = Math.max(fileSize, 2 * BLOCK_SIZE);
} catch (Exception e) {
throw convert(e);
}
......@@ -449,6 +487,10 @@ public class MVStore {
try {
shrinkFileIfPossible(0);
log("file close");
if (fileLock != null) {
fileLock.release();
fileLock = null;
}
file.close();
for (MVMap<?, ?> m : New.arrayList(maps.values())) {
m.close();
......@@ -477,16 +519,6 @@ public class MVStore {
return chunks.get(DataUtils.getPageChunkId(pos));
}
private long getFilePosition(long pos) {
Chunk c = getChunk(pos);
if (c == null) {
throw new RuntimeException("Chunk " + DataUtils.getPageChunkId(pos) + " not found");
}
long filePos = c.start;
filePos += DataUtils.getPageOffset(pos);
return filePos;
}
/**
* Increment the current version.
*
......@@ -503,7 +535,7 @@ public class MVStore {
*
* @return the new version (incremented if there were changes)
*/
public long save() {
public long store() {
if (!hasUnsavedChanges()) {
return currentVersion;
}
......@@ -592,7 +624,7 @@ public class MVStore {
c.writeHeader(buff);
buff.rewind();
try {
writeCount++;
fileWriteCount++;
DataUtils.writeFully(file, filePos, buff);
} catch (IOException e) {
throw new RuntimeException(e);
......@@ -603,7 +635,7 @@ public class MVStore {
long version = incrementVersion();
// write the new version (after the commit)
writeHeader();
writeFileHeader();
shrinkFileIfPossible(1);
unsavedPageCount = 0;
return version;
......@@ -634,7 +666,7 @@ public class MVStore {
if (used >= fileSize) {
return;
}
if (minPercent > 0 && fileSize - used < blockSize) {
if (minPercent > 0 && fileSize - used < BLOCK_SIZE) {
return;
}
int savedPercent = (int) (100 - (used * 100 / fileSize));
......@@ -654,10 +686,10 @@ public class MVStore {
if (c.start == Long.MAX_VALUE) {
continue;
}
int last = (int) ((c.start + c.length) / blockSize);
int last = (int) ((c.start + c.length) / BLOCK_SIZE);
min = Math.max(min, last + 1);
}
return min * blockSize;
return min * BLOCK_SIZE;
}
private long allocateChunk(long length) {
......@@ -671,11 +703,11 @@ public class MVStore {
if (c.start == Long.MAX_VALUE) {
continue;
}
int first = (int) (c.start / blockSize);
int last = (int) ((c.start + c.length) / blockSize);
int first = (int) (c.start / BLOCK_SIZE);
int last = (int) ((c.start + c.length) / BLOCK_SIZE);
set.set(first, last +1);
}
int required = (int) (length / blockSize) + 1;
int required = (int) (length / BLOCK_SIZE) + 1;
for (int i = 0; i < set.size(); i++) {
if (!set.get(i)) {
boolean ok = true;
......@@ -686,11 +718,11 @@ public class MVStore {
}
}
if (ok) {
return i * blockSize;
return i * BLOCK_SIZE;
}
}
}
return set.size() * blockSize;
return set.size() * BLOCK_SIZE;
}
/**
......@@ -712,8 +744,8 @@ public class MVStore {
private Chunk readChunkHeader(long start) {
try {
readCount++;
ByteBuffer buff = ByteBuffer.wrap(new byte[32]);
fileReadCount++;
ByteBuffer buff = ByteBuffer.allocate(32);
DataUtils.readFully(file, start, buff);
buff.rewind();
return Chunk.fromHeader(buff, start);
......@@ -801,7 +833,7 @@ public class MVStore {
copyLive(c, old);
}
save();
store();
return true;
}
......@@ -871,8 +903,13 @@ public class MVStore {
Page readPage(MVMap<?, ?> map, long pos) {
Page p = cache.get(pos);
if (p == null) {
long filePos = getFilePosition(pos);
readCount++;
Chunk c = getChunk(pos);
if (c == null) {
throw new RuntimeException("Chunk " + DataUtils.getPageChunkId(pos) + " not found");
}
long filePos = c.start;
filePos += DataUtils.getPageOffset(pos);
fileReadCount++;
p = Page.read(file, map, pos, filePos, fileSize);
cache.put(pos, p);
}
......@@ -1038,21 +1075,26 @@ public class MVStore {
unsavedPageCount++;
}
/**
* Get the store version. The store version is usually used to upgrade the
* structure of the store after upgrading the application. Initially the
* store version is 0, until it is changed.
*
* @return the store version
*/
public int getStoreVersion() {
String x = getSetting("storeVersion");
String x = meta.get("setting.storeVersion");
return x == null ? 0 : Integer.parseInt(x);
}
/**
* Update the store version. The setting is immediately
* persisted.
*
* @param version the new store version
*/
public void setStoreVersion(int version) {
setSetting("storeVersion", Integer.toString(version));
}
public String getSetting(String key) {
return meta.get("setting." + key);
}
public void setSetting(String key, String value) {
meta.put("setting." + key, value);
meta.put("setting.storeVersion", Integer.toString(version));
}
/**
......@@ -1092,8 +1134,8 @@ public class MVStore {
last = chunks.get(lastChunkId);
}
rootChunkStart = last.start;
writeHeader();
readHeader();
writeFileHeader();
readFileHeader();
readMeta();
}
}
......@@ -1131,21 +1173,41 @@ public class MVStore {
}
/**
* Get the number of write operations since this store was opened.
* Get the number of file write operations since this store was opened.
*
* @return the number of write operations
*/
public int getWriteCount() {
return writeCount;
public int getFileWriteCount() {
return fileWriteCount;
}
/**
* Get the number of read operations since this store was opened.
* Get the number of file read operations since this store was opened.
*
* @return the number of read operations
*/
public int getReadCount() {
return readCount;
public int getFileReadCount() {
return fileReadCount;
}
/**
* Get the file name, or null for in-memory stores.
*
* @return the file name
*/
public String getFileName() {
return fileName;
}
/**
* Get the file header. This data is for informational purposes only. The
* data is subject to change in future versions. The data should not be
* modified (doing so may corrupt the store).
*
* @return the file header
*/
public Map<String, String> getFileHeader() {
return fileHeader;
}
}
/*
* Copyright 2004-2011 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.dev.store.btree;
import java.util.HashMap;
import org.h2.util.New;
/**
* A builder for an MVStore.
*/
public class MVStoreBuilder {
private HashMap<String, Object> config = New.hashMap();
/**
* Use the following file name. If the file does not exist, it is
* automatically created.
*
* @param fileName the file name
* @return this
*/
public static MVStoreBuilder fileBased(String fileName) {
return new MVStoreBuilder().fileName(fileName);
}
/**
* Open the store in-memory. In this case, no data may be saved.
*
* @return the store
*/
public static MVStoreBuilder inMemory() {
return new MVStoreBuilder();
}
private MVStoreBuilder set(String key, Object value) {
if (config.containsKey(key)) {
throw new IllegalArgumentException("Parameter " + config.get(key) + " is already set");
}
config.put(key, value);
return this;
}
private MVStoreBuilder fileName(String fileName) {
return set("fileName", fileName);
}
/**
* Open the file in read-only mode. In this case, a shared lock will be
* acquired to ensure the file is not concurrently opened in write mode.
* <p>
* If this option is not used, the file is locked exclusively.
* <p>
* Please note a store may only be opened once in every JVM (no matter
* whether it is opened in read-only or read-write mode), because each file
* may be locked only once in a process.
*
* @return this
*/
public MVStoreBuilder readOnly() {
return set("openMode", "r");
}
public MVStoreBuilder with(MapFactory mapFactory) {
int todoRemove;
return set("mapFactory", mapFactory);
}
/**
* Open the store.
*
* @return the opened store
*/
public MVStore open() {
MVStore s = new MVStore(config);
s.open();
return s;
}
public String toString() {
return DataUtils.appendMap(new StringBuilder(), config).toString();
}
public static MVStoreBuilder fromString(String s) {
HashMap<String, String> config = DataUtils.parseMap(s);
MVStoreBuilder builder = new MVStoreBuilder();
builder.config.putAll(config);
return builder;
}
}
......@@ -17,61 +17,58 @@ import org.h2.store.fs.FilePath;
import org.h2.store.fs.FileUtils;
/**
* Convert a database file to a human-readable text dump.
* Utility methods used in combination with the MVStore.
*/
public class Dump {
private static int blockSize = 4 * 1024;
public class MVStoreUtils {
/**
* Runs this tool.
* Options are case sensitive. Supported options are:
* <table>
* <tr><td>[-file]</td>
* <td>The database file name</td></tr>
* <tr><td>[-dump &lt;dir&gt;]</td>
* <td>Dump the contends of the file</td></tr>
* </table>
*
* @param args the command line arguments
*/
public static void main(String... args) {
String fileName = "test.h3";
public static void main(String... args) throws IOException {
for (int i = 0; i < args.length; i++) {
if ("-file".equals(args[i])) {
fileName = args[++i];
if ("-dump".equals(args[i])) {
String fileName = args[++i];
dump(fileName, new PrintWriter(System.out));
}
}
dump(fileName, new PrintWriter(System.out));
}
/**
* Dump the contents of the file.
* Read the contents of the file and display them in a human-readable
* format.
*
* @param fileName the name of the file
* @param writer the print writer
*/
public static void dump(String fileName, PrintWriter writer) {
public static void dump(String fileName, PrintWriter writer) throws IOException {
if (!FileUtils.exists(fileName)) {
writer.println("File not found: " + fileName);
return;
}
FileChannel file = null;
int blockSize = MVStore.BLOCK_SIZE;
try {
file = FilePath.get(fileName).open("r");
long fileLength = file.size();
file.position(0);
byte[] header = new byte[blockSize];
file.read(ByteBuffer.wrap(header));
file.read(ByteBuffer.wrap(header), 0);
Properties prop = new Properties();
prop.load(new ByteArrayInputStream(header));
prop.load(new StringReader(new String(header, "UTF-8")));
writer.println("file " + fileName);
writer.println(" length " + fileLength);
writer.println(" " + prop);
ByteBuffer block = ByteBuffer.wrap(new byte[32]);
ByteBuffer block = ByteBuffer.allocate(32);
for (long pos = 0; pos < fileLength;) {
file.position(pos);
block.rewind();
FileUtils.readFully(file, block);
DataUtils.readFully(file, pos, block);
block.rewind();
if (block.get() != 'c') {
pos += blockSize;
......@@ -89,8 +86,7 @@ public class Dump {
" root " + metaRootPos +
" maxLengthLive " + maxLengthLive);
ByteBuffer chunk = ByteBuffer.allocate(chunkLength);
file.position(pos);
FileUtils.readFully(file, chunk);
DataUtils.readFully(file, pos, chunk);
int p = block.position();
pos = (pos + chunkLength + blockSize) / blockSize * blockSize;
chunkLength -= p;
......@@ -114,6 +110,7 @@ public class Dump {
}
} catch (IOException e) {
writer.println("ERROR: " + e);
throw e;
} finally {
if (file != null) {
try {
......
......@@ -6,7 +6,6 @@
*/
package org.h2.dev.store.btree;
import org.h2.compress.Compressor;
/**
* A factory for maps and data types.
......@@ -37,13 +36,6 @@ public interface MapFactory {
*/
DataType buildDataType(String dataType);
/**
* Create a new compressor.
*
* @return the compressor
*/
Compressor buildCompressor();
/**
* Get the data type object for the given class.
*
......
......@@ -130,12 +130,12 @@ public class Page {
maxLength = (int) Math.min(fileSize - filePos, maxLength);
int length = maxLength;
if (maxLength == Integer.MAX_VALUE) {
buff = ByteBuffer.wrap(new byte[128]);
buff = ByteBuffer.allocate(128);
DataUtils.readFully(file, filePos, buff);
maxLength = buff.getInt();
//read the first bytes again
}
buff = ByteBuffer.wrap(new byte[length]);
buff = ByteBuffer.allocate(length);
DataUtils.readFully(file, filePos, buff);
} catch (IOException e) {
throw new RuntimeException(e);
......@@ -664,9 +664,9 @@ public class Page {
int compLen = pageLength + start - buff.position();
byte[] comp = new byte[compLen];
buff.get(comp);
byte[] exp = new byte[compLen + lenAdd];
compressor.expand(comp, 0, compLen, exp, 0, exp.length);
buff = ByteBuffer.wrap(exp);
int l = compLen + lenAdd;
buff = ByteBuffer.allocate(l);
compressor.expand(comp, 0, compLen, buff.array(), 0, l);
}
DataType keyType = map.getKeyType();
for (int i = 0; i < len; i++) {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论