Unverified 提交 f796e624 authored 作者: Andrei Tokar's avatar Andrei Tokar 提交者: GitHub

Merge pull request #1188 from h2database/undo-log-split

Undo log split to reduce contention
...@@ -77,8 +77,12 @@ public class AlterTableAddConstraint extends SchemaCommand { ...@@ -77,8 +77,12 @@ public class AlterTableAddConstraint extends SchemaCommand {
try { try {
return tryUpdate(); return tryUpdate();
} catch (DbException e) { } catch (DbException e) {
for (Index index : createdIndexes) { try {
session.getDatabase().removeSchemaObject(session, index); for (Index index : createdIndexes) {
session.getDatabase().removeSchemaObject(session, index);
}
} catch (Throwable ex) {
e.addSuppressed(ex);
} }
throw e; throw e;
} finally { } finally {
......
...@@ -159,10 +159,14 @@ public class CreateTable extends CommandWithColumns { ...@@ -159,10 +159,14 @@ public class CreateTable extends CommandWithColumns {
} }
} }
} catch (DbException e) { } catch (DbException e) {
db.checkPowerOff(); try {
db.removeSchemaObject(session, table); db.checkPowerOff();
if (!transactional) { db.removeSchemaObject(session, table);
session.commit(true); if (!transactional) {
session.commit(true);
}
} catch (Throwable ex) {
e.addSuppressed(ex);
} }
throw e; throw e;
} }
......
...@@ -752,6 +752,9 @@ public class Database implements DataHandler { ...@@ -752,6 +752,9 @@ public class Database implements DataHandler {
getPageStore(); getPageStore();
} }
} }
if(mvStore != null) {
mvStore.getTransactionStore().init();
}
systemUser = new User(this, 0, SYSTEM_USER_NAME, true); systemUser = new User(this, 0, SYSTEM_USER_NAME, true);
mainSchema = new Schema(this, 0, Constants.SCHEMA_MAIN, systemUser, true); mainSchema = new Schema(this, 0, Constants.SCHEMA_MAIN, systemUser, true);
infoSchema = new Schema(this, -1, "INFORMATION_SCHEMA", systemUser, true); infoSchema = new Schema(this, -1, "INFORMATION_SCHEMA", systemUser, true);
...@@ -762,9 +765,6 @@ public class Database implements DataHandler { ...@@ -762,9 +765,6 @@ public class Database implements DataHandler {
systemUser.setAdmin(true); systemUser.setAdmin(true);
systemSession = new Session(this, systemUser, ++nextSessionId); systemSession = new Session(this, systemUser, ++nextSessionId);
lobSession = new Session(this, systemUser, ++nextSessionId); lobSession = new Session(this, systemUser, ++nextSessionId);
if(mvStore != null) {
mvStore.getTransactionStore().init(systemSession);
}
CreateTableData data = new CreateTableData(); CreateTableData data = new CreateTableData();
ArrayList<Column> cols = data.columns; ArrayList<Column> cols = data.columns;
Column columnId = new Column("ID", Value.INT); Column columnId = new Column("ID", Value.INT);
......
...@@ -300,10 +300,10 @@ public class Trace { ...@@ -300,10 +300,10 @@ public class Trace {
if (!space) { if (!space) {
buff.append(' '); buff.append(' ');
} }
buff.append("*/"). buff.append("*/");
append(StringUtils.javaEncode(sql)). StringUtils.javaEncode(sql, buff);
append(StringUtils.javaEncode(params)). StringUtils.javaEncode(params, buff);
append(';'); buff.append(';');
sql = buff.toString(); sql = buff.toString();
traceWriter.write(TraceSystem.INFO, module, sql, null); traceWriter.write(TraceSystem.INFO, module, sql, null);
} }
......
...@@ -128,17 +128,15 @@ public class FileStore { ...@@ -128,17 +128,15 @@ public class FileStore {
if (file != null) { if (file != null) {
return; return;
} }
if (fileName != null) { // ensure the Cache file system is registered
// ensure the Cache file system is registered FilePathCache.INSTANCE.getScheme();
FilePathCache.INSTANCE.getScheme(); FilePath p = FilePath.get(fileName);
FilePath p = FilePath.get(fileName); // if no explicit scheme was specified, NIO is used
// if no explicit scheme was specified, NIO is used if (p instanceof FilePathDisk &&
if (p instanceof FilePathDisk && !fileName.startsWith(p.getScheme() + ":")) {
!fileName.startsWith(p.getScheme() + ":")) { // ensure the NIO file system is registered
// ensure the NIO file system is registered FilePathNio.class.getName();
FilePathNio.class.getName(); fileName = "nio:" + fileName;
fileName = "nio:" + fileName;
}
} }
this.fileName = fileName; this.fileName = fileName;
FilePath f = FilePath.get(fileName); FilePath f = FilePath.get(fileName);
......
...@@ -23,9 +23,10 @@ import java.util.PriorityQueue; ...@@ -23,9 +23,10 @@ import java.util.PriorityQueue;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock;
import org.h2.compress.CompressDeflate; import org.h2.compress.CompressDeflate;
import org.h2.compress.CompressLZF; import org.h2.compress.CompressLZF;
import org.h2.compress.Compressor; import org.h2.compress.Compressor;
...@@ -33,6 +34,7 @@ import org.h2.engine.Constants; ...@@ -33,6 +34,7 @@ import org.h2.engine.Constants;
import org.h2.mvstore.cache.CacheLongKeyLIRS; import org.h2.mvstore.cache.CacheLongKeyLIRS;
import org.h2.util.MathUtils; import org.h2.util.MathUtils;
import static org.h2.mvstore.MVMap.INITIAL_VERSION; import static org.h2.mvstore.MVMap.INITIAL_VERSION;
import org.h2.util.Utils;
/* /*
...@@ -144,6 +146,14 @@ public class MVStore { ...@@ -144,6 +146,14 @@ public class MVStore {
*/ */
private static final int MARKED_FREE = 10_000_000; private static final int MARKED_FREE = 10_000_000;
/**
* Lock which governs access to major store operations: store(), close(), ...
* It should used in a non-reentrant fashion.
* It serves as a replacement for synchronized(this), except it allows for
* non-blocking lock attempts.
*/
private final ReentrantLock storeLock = new ReentrantLock(true);
/** /**
* The background thread, if any. * The background thread, if any.
*/ */
...@@ -195,8 +205,7 @@ public class MVStore { ...@@ -195,8 +205,7 @@ public class MVStore {
private final Map<Integer, Chunk> freedPageSpace = new HashMap<>(); private final Map<Integer, Chunk> freedPageSpace = new HashMap<>();
/** /**
* The metadata map. Write access to this map needs to be synchronized on * The metadata map. Write access to this map needs to be done under storeLock.
* the store.
*/ */
private final MVMap<String, String> meta; private final MVMap<String, String> meta;
...@@ -207,7 +216,7 @@ public class MVStore { ...@@ -207,7 +216,7 @@ public class MVStore {
private WriteBuffer writeBuffer; private WriteBuffer writeBuffer;
private int lastMapId; private final AtomicInteger lastMapId = new AtomicInteger();
private int versionsToKeep = 5; private int versionsToKeep = 5;
...@@ -274,12 +283,6 @@ public class MVStore { ...@@ -274,12 +283,6 @@ public class MVStore {
*/ */
private volatile long currentStoreVersion = -1; private volatile long currentStoreVersion = -1;
/**
* Holds reference to a thread performing store operation (if any)
* or null if there is none is in progress.
*/
private final AtomicReference<Thread> currentStoreThread = new AtomicReference<>();
private volatile boolean metaChanged; private volatile boolean metaChanged;
/** /**
...@@ -289,8 +292,10 @@ public class MVStore { ...@@ -289,8 +292,10 @@ public class MVStore {
private final int autoCompactFillRate; private final int autoCompactFillRate;
private long autoCompactLastFileOpCount; private long autoCompactLastFileOpCount;
/**
private final Object compactSync = new Object(); * Simple lock to ensure that no more than one compaction runs at any given time
*/
private boolean compactInProgress;
private volatile IllegalStateException panicException; private volatile IllegalStateException panicException;
...@@ -352,9 +357,10 @@ public class MVStore { ...@@ -352,9 +357,10 @@ public class MVStore {
meta.init(); meta.init();
if (this.fileStore != null) { if (this.fileStore != null) {
retentionTime = this.fileStore.getDefaultRetentionTime(); retentionTime = this.fileStore.getDefaultRetentionTime();
int kb = DataUtils.getConfigParam(config, "autoCommitBufferSize", 1024);
// 19 KB memory is about 1 KB storage // 19 KB memory is about 1 KB storage
autoCommitMemory = kb * 1024 * 19; int kb = Math.max(1, Math.min(19, Utils.scaleForAvailableMemory(64))) * 1024;
kb = DataUtils.getConfigParam(config, "autoCommitBufferSize", kb);
autoCommitMemory = kb * 1024;
autoCompactFillRate = DataUtils.getConfigParam(config, "autoCompactFillRate", 40); autoCompactFillRate = DataUtils.getConfigParam(config, "autoCompactFillRate", 40);
char[] encryptionKey = (char[]) config.get("encryptionKey"); char[] encryptionKey = (char[]) config.get("encryptionKey");
try { try {
...@@ -472,15 +478,14 @@ public class MVStore { ...@@ -472,15 +478,14 @@ public class MVStore {
* @param builder the map builder * @param builder the map builder
* @return the map * @return the map
*/ */
public synchronized <M extends MVMap<K, V>, K, V> M openMap( public <M extends MVMap<K, V>, K, V> M openMap(String name, MVMap.MapBuilder<M, K, V> builder) {
String name, MVMap.MapBuilder<M, K, V> builder) {
int id = getMapId(name); int id = getMapId(name);
M map; M map;
if (id >= 0) { if (id >= 0) {
map = openMap(id, builder); map = openMap(id, builder);
} else { } else {
HashMap<String, Object> c = new HashMap<>(); HashMap<String, Object> c = new HashMap<>();
id = ++lastMapId; id = lastMapId.incrementAndGet();
c.put("id", id); c.put("id", id);
c.put("createVersion", currentVersion); c.put("createVersion", currentVersion);
map = builder.create(this, c); map = builder.create(this, c);
...@@ -491,32 +496,36 @@ public class MVStore { ...@@ -491,32 +496,36 @@ public class MVStore {
map.setRootPos(0, lastStoredVersion); map.setRootPos(0, lastStoredVersion);
markMetaChanged(); markMetaChanged();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
M existingMap = (M)maps.putIfAbsent(id, map); M existingMap = (M) maps.putIfAbsent(id, map);
if(existingMap != null) { if (existingMap != null) {
map = existingMap; map = existingMap;
} }
} }
return map; return map;
} }
public synchronized <M extends MVMap<K, V>, K, V> M openMap(int id, public <M extends MVMap<K, V>, K, V> M openMap(int id, MVMap.MapBuilder<M, K, V> builder) {
MVMap.MapBuilder<M, K, V> builder) { storeLock.lock();
@SuppressWarnings("unchecked") try {
M map = (M) getMap(id); @SuppressWarnings("unchecked")
if (map == null) { M map = (M) getMap(id);
String configAsString = meta.get(MVMap.getMapKey(id)); if (map == null) {
if(configAsString != null) { String configAsString = meta.get(MVMap.getMapKey(id));
HashMap<String, Object> config = if(configAsString != null) {
new HashMap<String, Object>(DataUtils.parseMap(configAsString)); HashMap<String, Object> config =
config.put("id", id); new HashMap<String, Object>(DataUtils.parseMap(configAsString));
map = builder.create(this, config); config.put("id", id);
map.init(); map = builder.create(this, config);
long root = getRootPos(meta, id); map.init();
map.setRootPos(root, lastStoredVersion); long root = getRootPos(meta, id);
maps.put(id, map); map.setRootPos(root, lastStoredVersion);
maps.put(id, map);
}
} }
return map;
} finally {
storeLock.unlock();
} }
return map;
} }
public <K, V> MVMap<K,V> getMap(int id) { public <K, V> MVMap<K,V> getMap(int id) {
...@@ -764,12 +773,12 @@ public class MVStore { ...@@ -764,12 +773,12 @@ public class MVStore {
lastChunk = last; lastChunk = last;
if (last == null) { if (last == null) {
// no valid chunk // no valid chunk
lastMapId = 0; lastMapId.set(0);
currentVersion = 0; currentVersion = 0;
lastStoredVersion = INITIAL_VERSION; lastStoredVersion = INITIAL_VERSION;
meta.setRootPos(0, INITIAL_VERSION); meta.setRootPos(0, INITIAL_VERSION);
} else { } else {
lastMapId = last.mapId; lastMapId.set(last.mapId);
currentVersion = last.version; currentVersion = last.version;
chunks.put(last.id, last); chunks.put(last.id, last);
lastStoredVersion = currentVersion - 1; lastStoredVersion = currentVersion - 1;
...@@ -952,12 +961,10 @@ public class MVStore { ...@@ -952,12 +961,10 @@ public class MVStore {
if (closed) { if (closed) {
return; return;
} }
// can not synchronize on this yet, because
// the thread also synchronized on this, which
// could result in a deadlock
stopBackgroundThread(); stopBackgroundThread();
closed = true; closed = true;
synchronized (this) { storeLock.lock();
try {
if (fileStore != null && shrinkIfPossible) { if (fileStore != null && shrinkIfPossible) {
shrinkFileIfPossible(0); shrinkFileIfPossible(0);
} }
...@@ -977,6 +984,8 @@ public class MVStore { ...@@ -977,6 +984,8 @@ public class MVStore {
if (fileStore != null && !fileStoreIsProvided) { if (fileStore != null && !fileStoreIsProvided) {
fileStore.close(); fileStore.close();
} }
} finally {
storeLock.unlock();
} }
} }
...@@ -1041,11 +1050,15 @@ public class MVStore { ...@@ -1041,11 +1050,15 @@ public class MVStore {
* @return the new version (incremented if there were changes) * @return the new version (incremented if there were changes)
*/ */
public long tryCommit() { public long tryCommit() {
// unlike synchronization, this will also prevent re-entrance, // we need to prevent re-entrance, which may be possible,
// which may be possible, if the meta map have changed // because meta map is modified within storeNow() and that
if (currentStoreThread.compareAndSet(null, Thread.currentThread())) { // causes beforeWrite() call with possibility of going back here
synchronized (this) { if ((!storeLock.isHeldByCurrentThread() || currentStoreVersion < 0) &&
storeLock.tryLock()) {
try {
store(); store();
} finally {
storeLock.unlock();
} }
} }
return currentVersion; return currentVersion;
...@@ -1067,9 +1080,18 @@ public class MVStore { ...@@ -1067,9 +1080,18 @@ public class MVStore {
* *
* @return the new version (incremented if there were changes) * @return the new version (incremented if there were changes)
*/ */
public synchronized long commit() { public long commit() {
currentStoreThread.set(Thread.currentThread()); // we need to prevent re-entrance, which may be possible,
store(); // because meta map is modified within storeNow() and that
// causes beforeWrite() call with possibility of going back here
if(!storeLock.isHeldByCurrentThread() || currentStoreVersion < 0) {
storeLock.lock();
try {
store();
} finally {
storeLock.unlock();
}
}
return currentVersion; return currentVersion;
} }
...@@ -1101,12 +1123,11 @@ public class MVStore { ...@@ -1101,12 +1123,11 @@ public class MVStore {
// in any case reset the current store version, // in any case reset the current store version,
// to allow closing the store // to allow closing the store
currentStoreVersion = -1; currentStoreVersion = -1;
currentStoreThread.set(null);
} }
} }
private void storeNow() { private void storeNow() {
assert Thread.holdsLock(this); assert storeLock.isHeldByCurrentThread();
long time = getTimeSinceCreation(); long time = getTimeSinceCreation();
freeUnusedIfNeeded(time); freeUnusedIfNeeded(time);
int currentUnsavedPageCount = unsavedMemory; int currentUnsavedPageCount = unsavedMemory;
...@@ -1151,7 +1172,7 @@ public class MVStore { ...@@ -1151,7 +1172,7 @@ public class MVStore {
c.len = Integer.MAX_VALUE; c.len = Integer.MAX_VALUE;
c.time = time; c.time = time;
c.version = version; c.version = version;
c.mapId = lastMapId; c.mapId = lastMapId.get();
c.next = Long.MAX_VALUE; c.next = Long.MAX_VALUE;
chunks.put(c.id, c); chunks.put(c.id, c);
// force a metadata update // force a metadata update
...@@ -1312,7 +1333,8 @@ public class MVStore { ...@@ -1312,7 +1333,8 @@ public class MVStore {
} }
} }
private synchronized void freeUnusedChunks() { private void freeUnusedChunks() {
assert storeLock.isHeldByCurrentThread();
if (lastChunk != null && reuseSpace) { if (lastChunk != null && reuseSpace) {
Set<Integer> referenced = collectReferencedChunks(); Set<Integer> referenced = collectReferencedChunks();
long time = getTimeSinceCreation(); long time = getTimeSinceCreation();
...@@ -1652,7 +1674,6 @@ public class MVStore { ...@@ -1652,7 +1674,6 @@ public class MVStore {
* @return if there are any changes * @return if there are any changes
*/ */
public boolean hasUnsavedChanges() { public boolean hasUnsavedChanges() {
assert !metaChanged || meta.hasChangesSince(lastStoredVersion) : metaChanged;
if (metaChanged) { if (metaChanged) {
return true; return true;
} }
...@@ -1684,31 +1705,37 @@ public class MVStore { ...@@ -1684,31 +1705,37 @@ public class MVStore {
* *
* @return if anything was written * @return if anything was written
*/ */
public synchronized boolean compactRewriteFully() { public boolean compactRewriteFully() {
checkOpen(); storeLock.lock();
if (lastChunk == null) { try {
// nothing to do checkOpen();
return false; if (lastChunk == null) {
} // nothing to do
for (MVMap<?, ?> m : maps.values()) { return false;
@SuppressWarnings("unchecked") }
MVMap<Object, Object> map = (MVMap<Object, Object>) m; for (MVMap<?, ?> m : maps.values()) {
Cursor<Object, Object> cursor = map.cursor(null); @SuppressWarnings("unchecked")
Page lastPage = null; MVMap<Object, Object> map = (MVMap<Object, Object>) m;
while (cursor.hasNext()) { Cursor<Object, Object> cursor = map.cursor(null);
cursor.next(); Page lastPage = null;
Page p = cursor.getPage(); while (cursor.hasNext()) {
if (p == lastPage) { cursor.next();
continue; Page p = cursor.getPage();
if (p == lastPage) {
continue;
}
Object k = p.getKey(0);
Object v = p.getValue(0);
map.put(k, v);
lastPage = p;
} }
Object k = p.getKey(0);
Object v = p.getValue(0);
map.put(k, v);
lastPage = p;
} }
commit();
return true;
} finally {
storeLock.unlock();
} }
commit();
return true;
} }
/** /**
...@@ -1728,23 +1755,28 @@ public class MVStore { ...@@ -1728,23 +1755,28 @@ public class MVStore {
* than this * than this
* @param moveSize the number of bytes to move * @param moveSize the number of bytes to move
*/ */
public synchronized void compactMoveChunks(int targetFillRate, long moveSize) { public void compactMoveChunks(int targetFillRate, long moveSize) {
checkOpen(); storeLock.lock();
if (lastChunk != null && reuseSpace) { try {
int oldRetentionTime = retentionTime; checkOpen();
boolean oldReuse = reuseSpace; if (lastChunk != null && reuseSpace) {
try { int oldRetentionTime = retentionTime;
retentionTime = -1; boolean oldReuse = reuseSpace;
freeUnusedChunks(); try {
if (fileStore.getFillRate() <= targetFillRate) { retentionTime = -1;
long start = fileStore.getFirstFree() / BLOCK_SIZE; freeUnusedChunks();
ArrayList<Chunk> move = findChunksToMove(start, moveSize); if (fileStore.getFillRate() <= targetFillRate) {
compactMoveChunks(move); long start = fileStore.getFirstFree() / BLOCK_SIZE;
ArrayList<Chunk> move = findChunksToMove(start, moveSize);
compactMoveChunks(move);
}
} finally {
reuseSpace = oldReuse;
retentionTime = oldRetentionTime;
} }
} finally {
reuseSpace = oldReuse;
retentionTime = oldRetentionTime;
} }
} finally {
storeLock.unlock();
} }
} }
...@@ -1884,17 +1916,31 @@ public class MVStore { ...@@ -1884,17 +1916,31 @@ public class MVStore {
if (!reuseSpace) { if (!reuseSpace) {
return false; return false;
} }
synchronized (compactSync) { checkOpen();
checkOpen(); // We can't wait forever for the lock here,
ArrayList<Chunk> old; // because if called from the background thread,
synchronized (this) { // it might go into deadlock with concurrent database closure
old = findOldChunks(targetFillRate, write); // and attempt to stop this thread.
} try {
if (old == null || old.isEmpty()) { if (storeLock.tryLock(10, TimeUnit.MILLISECONDS)) {
return false; try {
if (!compactInProgress) {
compactInProgress = true;
ArrayList<Chunk> old = findOldChunks(targetFillRate, write);
if (old == null || old.isEmpty()) {
return false;
}
compactRewrite(old);
return true;
}
} finally {
compactInProgress = false;
storeLock.unlock();
}
} }
compactRewrite(old); return false;
return true; } catch (InterruptedException e) {
throw new RuntimeException(e);
} }
} }
...@@ -2293,11 +2339,18 @@ public class MVStore { ...@@ -2293,11 +2339,18 @@ public class MVStore {
* @param map the map * @param map the map
*/ */
void beforeWrite(MVMap<?, ?> map) { void beforeWrite(MVMap<?, ?> map) {
if (saveNeeded && fileStore != null && !closed && autoCommitDelay > 0) { if (saveNeeded && fileStore != null && !closed) {
saveNeeded = false; saveNeeded = false;
// check again, because it could have been written by now // check again, because it could have been written by now
if (unsavedMemory > autoCommitMemory && autoCommitMemory > 0) { if (unsavedMemory > autoCommitMemory && autoCommitMemory > 0) {
tryCommit(); // if unsaved memory creation rate is to high,
// some back pressure need to be applied
// to slow things down and avoid OOME
if (3 * unsavedMemory > 4 * autoCommitMemory) {
commit();
} else {
tryCommit();
}
} }
} }
} }
...@@ -2320,10 +2373,15 @@ public class MVStore { ...@@ -2320,10 +2373,15 @@ public class MVStore {
* *
* @param version the new store version * @param version the new store version
*/ */
public synchronized void setStoreVersion(int version) { public void setStoreVersion(int version) {
checkOpen(); storeLock.lock();
markMetaChanged(); try {
meta.put("setting.storeVersion", Integer.toHexString(version)); checkOpen();
markMetaChanged();
meta.put("setting.storeVersion", Integer.toHexString(version));
} finally {
storeLock.unlock();
}
} }
/** /**
...@@ -2342,104 +2400,109 @@ public class MVStore { ...@@ -2342,104 +2400,109 @@ public class MVStore {
* *
* @param version the version to revert to * @param version the version to revert to
*/ */
public synchronized void rollbackTo(long version) { public void rollbackTo(long version) {
checkOpen(); storeLock.lock();
if (version == 0) { try {
// special case: remove all data checkOpen();
for (MVMap<?, ?> m : maps.values()) { if (version == 0) {
m.close(); // special case: remove all data
} for (MVMap<?, ?> m : maps.values()) {
meta.setInitialRoot(meta.createEmptyLeaf(), INITIAL_VERSION); m.close();
}
meta.setInitialRoot(meta.createEmptyLeaf(), INITIAL_VERSION);
chunks.clear(); chunks.clear();
if (fileStore != null) { if (fileStore != null) {
fileStore.clear(); fileStore.clear();
}
maps.clear();
lastChunk = null;
synchronized (freedPageSpace) {
freedPageSpace.clear();
}
versions.clear();
currentVersion = version;
setWriteVersion(version);
metaChanged = false;
lastStoredVersion = INITIAL_VERSION;
return;
} }
maps.clear(); DataUtils.checkArgument(
lastChunk = null; isKnownVersion(version),
synchronized (freedPageSpace) { "Unknown version {0}", version);
freedPageSpace.clear(); for (MVMap<?, ?> m : maps.values()) {
m.rollbackTo(version);
} }
versions.clear();
currentVersion = version;
setWriteVersion(version);
metaChanged = false;
lastStoredVersion = INITIAL_VERSION;
return;
}
DataUtils.checkArgument(
isKnownVersion(version),
"Unknown version {0}", version);
for (MVMap<?, ?> m : maps.values()) {
m.rollbackTo(version);
}
TxCounter txCounter; TxCounter txCounter;
while ((txCounter = versions.peekLast()) != null && txCounter.version >= version) { while ((txCounter = versions.peekLast()) != null && txCounter.version >= version) {
versions.removeLast(); versions.removeLast();
} }
currentTxCounter = new TxCounter(version); currentTxCounter = new TxCounter(version);
meta.rollbackTo(version); meta.rollbackTo(version);
metaChanged = false; metaChanged = false;
boolean loadFromFile = false; boolean loadFromFile = false;
// find out which chunks to remove, // find out which chunks to remove,
// and which is the newest chunk to keep // and which is the newest chunk to keep
// (the chunk list can have gaps) // (the chunk list can have gaps)
ArrayList<Integer> remove = new ArrayList<>(); ArrayList<Integer> remove = new ArrayList<>();
Chunk keep = null; Chunk keep = null;
for (Chunk c : chunks.values()) { for (Chunk c : chunks.values()) {
if (c.version > version) { if (c.version > version) {
remove.add(c.id); remove.add(c.id);
} else if (keep == null || keep.id < c.id) { } else if (keep == null || keep.id < c.id) {
keep = c; keep = c;
} }
} }
if (!remove.isEmpty()) { if (!remove.isEmpty()) {
// remove the youngest first, so we don't create gaps // remove the youngest first, so we don't create gaps
// (in case we remove many chunks) // (in case we remove many chunks)
Collections.sort(remove, Collections.reverseOrder()); Collections.sort(remove, Collections.reverseOrder());
loadFromFile = true; loadFromFile = true;
for (int id : remove) { for (int id : remove) {
Chunk c = chunks.remove(id); Chunk c = chunks.remove(id);
long start = c.block * BLOCK_SIZE; long start = c.block * BLOCK_SIZE;
int length = c.len * BLOCK_SIZE; int length = c.len * BLOCK_SIZE;
fileStore.free(start, length); fileStore.free(start, length);
assert fileStore.getFileLengthInUse() == measureFileLengthInUse() : assert fileStore.getFileLengthInUse() == measureFileLengthInUse() :
fileStore.getFileLengthInUse() + " != " + measureFileLengthInUse(); fileStore.getFileLengthInUse() + " != " + measureFileLengthInUse();
// overwrite the chunk, // overwrite the chunk,
// so it is not be used later on // so it is not be used later on
WriteBuffer buff = getWriteBuffer(); WriteBuffer buff = getWriteBuffer();
buff.limit(length); buff.limit(length);
// buff.clear() does not set the data // buff.clear() does not set the data
Arrays.fill(buff.getBuffer().array(), (byte) 0); Arrays.fill(buff.getBuffer().array(), (byte) 0);
write(start, buff.getBuffer()); write(start, buff.getBuffer());
releaseWriteBuffer(buff); releaseWriteBuffer(buff);
// only really needed if we remove many chunks, when writes are // only really needed if we remove many chunks, when writes are
// re-ordered - but we do it always, because rollback is not // re-ordered - but we do it always, because rollback is not
// performance critical // performance critical
sync(); sync();
} }
lastChunk = keep; lastChunk = keep;
writeStoreHeader(); writeStoreHeader();
readStoreHeader(); readStoreHeader();
} }
for (MVMap<?, ?> m : new ArrayList<>(maps.values())) { for (MVMap<?, ?> m : new ArrayList<>(maps.values())) {
int id = m.getId(); int id = m.getId();
if (m.getCreateVersion() >= version) { if (m.getCreateVersion() >= version) {
m.close(); m.close();
maps.remove(id); maps.remove(id);
} else {
if (loadFromFile) {
m.setRootPos(getRootPos(meta, id), version);
} else { } else {
m.rollbackRoot(version); if (loadFromFile) {
m.setRootPos(getRootPos(meta, id), version);
} else {
m.rollbackRoot(version);
}
} }
} }
} currentVersion = version;
currentVersion = version; if (lastStoredVersion == INITIAL_VERSION) {
if (lastStoredVersion == INITIAL_VERSION) { lastStoredVersion = currentVersion - 1;
lastStoredVersion = currentVersion - 1; }
} finally {
storeLock.unlock();
} }
} }
...@@ -2495,19 +2558,25 @@ public class MVStore { ...@@ -2495,19 +2558,25 @@ public class MVStore {
* @param map the map * @param map the map
* @param newName the new name * @param newName the new name
*/ */
public synchronized void renameMap(MVMap<?, ?> map, String newName) { public void renameMap(MVMap<?, ?> map, String newName) {
checkOpen(); checkOpen();
DataUtils.checkArgument(map != meta, DataUtils.checkArgument(map != meta,
"Renaming the meta map is not allowed"); "Renaming the meta map is not allowed");
int id = map.getId(); int id = map.getId();
String oldName = getMapName(id); String oldName = getMapName(id);
if (oldName != null && !oldName.equals(newName)) { if (oldName != null && !oldName.equals(newName)) {
String idHexStr = Integer.toHexString(id);
// we need to cope whith the case of previously unfinished rename
String existingIdHexStr = meta.get("name." + newName);
DataUtils.checkArgument( DataUtils.checkArgument(
!meta.containsKey("name." + newName), existingIdHexStr == null || existingIdHexStr.equals(idHexStr),
"A map named {0} already exists", newName); "A map named {0} already exists", newName);
meta.remove("name." + oldName); // at first create a new name as an "alias"
meta.put("name." + newName, idHexStr);
// switch roles of a new and old names - old one is an alias now
meta.put(MVMap.getMapKey(id), map.asString(newName)); meta.put(MVMap.getMapKey(id), map.asString(newName));
meta.put("name." + newName, Integer.toHexString(id)); // get rid of the old name completely
meta.remove("name." + oldName);
markMetaChanged(); markMetaChanged();
} }
} }
...@@ -2522,18 +2591,23 @@ public class MVStore { ...@@ -2522,18 +2591,23 @@ public class MVStore {
removeMap(map, true); removeMap(map, true);
} }
public synchronized void removeMap(MVMap<?, ?> map, boolean delayed) { public void removeMap(MVMap<?, ?> map, boolean delayed) {
checkOpen(); storeLock.lock();
DataUtils.checkArgument(map != meta, try {
"Removing the meta map is not allowed"); checkOpen();
map.close(); DataUtils.checkArgument(map != meta,
MVMap.RootReference rootReference = map.getRoot(); "Removing the meta map is not allowed");
updateCounter += rootReference.updateCounter; map.close();
updateAttemptCounter += rootReference.updateAttemptCounter; MVMap.RootReference rootReference = map.getRoot();
updateCounter += rootReference.updateCounter;
int id = map.getId(); updateAttemptCounter += rootReference.updateAttemptCounter;
String name = getMapName(id);
removeMap(name, id, delayed); int id = map.getId();
String name = getMapName(id);
removeMap(name, id, delayed);
} finally {
storeLock.unlock();
}
} }
private void removeMap(String name, int id, boolean delayed) { private void removeMap(String name, int id, boolean delayed) {
...@@ -2659,11 +2733,7 @@ public class MVStore { ...@@ -2659,11 +2733,7 @@ public class MVStore {
synchronized (t.sync) { synchronized (t.sync) {
t.sync.notifyAll(); t.sync.notifyAll();
} }
if (Thread.holdsLock(this)) {
// called from storeNow: can not join,
// because that could result in a deadlock
return;
}
try { try {
t.join(); t.join();
} catch (Exception e) { } catch (Exception e) {
...@@ -2833,11 +2903,11 @@ public class MVStore { ...@@ -2833,11 +2903,11 @@ public class MVStore {
public void deregisterVersionUsage(TxCounter txCounter) { public void deregisterVersionUsage(TxCounter txCounter) {
if(txCounter != null) { if(txCounter != null) {
if(txCounter.counter.decrementAndGet() <= 0) { if(txCounter.counter.decrementAndGet() <= 0) {
if (currentStoreThread.compareAndSet(null, Thread.currentThread())) { if (!storeLock.isHeldByCurrentThread() && storeLock.tryLock()) {
try { try {
dropUnusedVersions(); dropUnusedVersions();
} finally { } finally {
currentStoreThread.set(null); storeLock.unlock();
} }
} }
} }
......
...@@ -253,7 +253,7 @@ public class MVTableEngine implements TableEngine { ...@@ -253,7 +253,7 @@ public class MVTableEngine implements TableEngine {
public void initTransactions() { public void initTransactions() {
List<Transaction> list = transactionStore.getOpenTransactions(); List<Transaction> list = transactionStore.getOpenTransactions();
for (Transaction t : list) { for (Transaction t : list) {
if (t.getStatus() == Transaction.STATUS_COMMITTING) { if (t.getStatus() == Transaction.STATUS_COMMITTED) {
t.commit(); t.commit();
} else if (t.getStatus() != Transaction.STATUS_PREPARED) { } else if (t.getStatus() != Transaction.STATUS_PREPARED) {
t.rollback(); t.rollback();
......
...@@ -30,19 +30,22 @@ final class RollbackDecisionMaker extends MVMap.DecisionMaker<Object[]> { ...@@ -30,19 +30,22 @@ final class RollbackDecisionMaker extends MVMap.DecisionMaker<Object[]> {
@Override @Override
public MVMap.Decision decide(Object[] existingValue, Object[] providedValue) { public MVMap.Decision decide(Object[] existingValue, Object[] providedValue) {
assert decision == null; assert decision == null;
assert existingValue != null; // normaly existingValue will always be there except of db initialization
VersionedValue valueToRestore = (VersionedValue) existingValue[2]; // where some undo log enty was captured on disk but actual map entry was not
long operationId; if (existingValue != null ) {
if (valueToRestore == null || VersionedValue valueToRestore = (VersionedValue) existingValue[2];
(operationId = valueToRestore.getOperationId()) == 0 || long operationId;
TransactionStore.getTransactionId(operationId) == transactionId if (valueToRestore == null ||
&& TransactionStore.getLogId(operationId) < toLogId) { (operationId = valueToRestore.getOperationId()) == 0 ||
int mapId = (Integer) existingValue[0]; TransactionStore.getTransactionId(operationId) == transactionId
MVMap<Object, VersionedValue> map = store.openMap(mapId); && TransactionStore.getLogId(operationId) < toLogId) {
if (map != null && !map.isClosed()) { int mapId = (Integer) existingValue[0];
Object key = existingValue[1]; MVMap<Object, VersionedValue> map = store.openMap(mapId);
VersionedValue previousValue = map.operate(key, valueToRestore, MVMap.DecisionMaker.DEFAULT); if (map != null && !map.isClosed()) {
listener.onRollback(map, key, previousValue, valueToRestore); Object key = existingValue[1];
VersionedValue previousValue = map.operate(key, valueToRestore, MVMap.DecisionMaker.DEFAULT);
listener.onRollback(map, key, previousValue, valueToRestore);
}
} }
} }
decision = MVMap.Decision.REMOVE; decision = MVMap.Decision.REMOVE;
......
...@@ -32,41 +32,36 @@ public class Transaction { ...@@ -32,41 +32,36 @@ public class Transaction {
*/ */
public static final int STATUS_PREPARED = 2; public static final int STATUS_PREPARED = 2;
/**
* The status of a transaction that is being committed, but possibly not
* yet finished. A transactions can go into this state when the store is
* closed while the transaction is committing. When opening a store,
* such transactions should be committed.
*/
public static final int STATUS_COMMITTING = 3;
/** /**
* The status of a transaction that has been logically committed or rather * The status of a transaction that has been logically committed or rather
* marked as committed, because it might be still listed among prepared, * marked as committed, because it might be still listed among prepared,
* if it was prepared for commit, undo log entries might still exists for it * if it was prepared for commit. Undo log entries might still exists for it
* and not all of it's changes within map's are re-written as committed yet. * and not all of it's changes within map's are re-written as committed yet.
* Nevertheless, those changes should be already viewed by other * Nevertheless, those changes should be already viewed by other
* transactions as committed. * transactions as committed.
* This transaction's id can not be re-used until all the above is completed * This transaction's id can not be re-used until all of the above is completed
* and transaction is closed. * and transaction is closed.
* A transactions can be observed in this state when the store was
* closed while the transaction was not closed yet.
* When opening a store, such transactions will automatically
* be processed and closed as committed.
*/ */
private static final int STATUS_COMMITTED = 4; public static final int STATUS_COMMITTED = 3;
/** /**
* The status of a transaction that currently in a process of rolling back * The status of a transaction that currently in a process of rolling back
* to a savepoint. * to a savepoint.
*/ */
private static final int STATUS_ROLLING_BACK = 5; private static final int STATUS_ROLLING_BACK = 4;
/** /**
* The status of a transaction that has been rolled back completely, * The status of a transaction that has been rolled back completely,
* but undo operations are not finished yet. * but undo operations are not finished yet.
*/ */
private static final int STATUS_ROLLED_BACK = 6; private static final int STATUS_ROLLED_BACK = 5;
private static final String STATUS_NAMES[] = { private static final String STATUS_NAMES[] = {
"CLOSED", "OPEN", "PREPARED", "COMMITTING", "CLOSED", "OPEN", "PREPARED", "COMMITTED", "ROLLING_BACK", "ROLLED_BACK"
"COMMITTED", "ROLLING_BACK", "ROLLED_BACK"
}; };
static final int LOG_ID_BITS = 40; static final int LOG_ID_BITS = 40;
private static final int LOG_ID_BITS1 = LOG_ID_BITS + 1; private static final int LOG_ID_BITS1 = LOG_ID_BITS + 1;
...@@ -175,6 +170,11 @@ public class Transaction { ...@@ -175,6 +170,11 @@ public class Transaction {
return getStatus(statusAndLogId.get()); return getStatus(statusAndLogId.get());
} }
/**
* Changes transaction status to a specified value
* @param status to be set
* @return transaction state as it was before status change
*/
long setStatus(int status) { long setStatus(int status) {
while (true) { while (true) {
long currentState = statusAndLogId.get(); long currentState = statusAndLogId.get();
...@@ -192,23 +192,19 @@ public class Transaction { ...@@ -192,23 +192,19 @@ public class Transaction {
case STATUS_PREPARED: case STATUS_PREPARED:
valid = currentStatus == STATUS_OPEN; valid = currentStatus == STATUS_OPEN;
break; break;
case STATUS_COMMITTING: case STATUS_COMMITTED:
valid = currentStatus == STATUS_OPEN || valid = currentStatus == STATUS_OPEN ||
currentStatus == STATUS_PREPARED || currentStatus == STATUS_PREPARED ||
// this case is only possible if called // this case is only possible if called
// from endLeftoverTransactions() // from endLeftoverTransactions()
currentStatus == STATUS_COMMITTING; currentStatus == STATUS_COMMITTED;
break;
case STATUS_COMMITTED:
valid = currentStatus == STATUS_COMMITTING;
break; break;
case STATUS_ROLLED_BACK: case STATUS_ROLLED_BACK:
valid = currentStatus == STATUS_OPEN || valid = currentStatus == STATUS_OPEN ||
currentStatus == STATUS_PREPARED; currentStatus == STATUS_PREPARED;
break; break;
case STATUS_CLOSED: case STATUS_CLOSED:
valid = currentStatus == STATUS_COMMITTING || valid = currentStatus == STATUS_COMMITTED ||
currentStatus == STATUS_COMMITTED ||
currentStatus == STATUS_ROLLED_BACK; currentStatus == STATUS_ROLLED_BACK;
break; break;
default: default:
...@@ -365,11 +361,11 @@ public class Transaction { ...@@ -365,11 +361,11 @@ public class Transaction {
Throwable ex = null; Throwable ex = null;
boolean hasChanges = false; boolean hasChanges = false;
try { try {
long state = setStatus(STATUS_COMMITTING); long state = setStatus(STATUS_COMMITTED);
hasChanges = hasChanges(state); hasChanges = hasChanges(state);
int previousStatus = getStatus(state);
if (hasChanges) { if (hasChanges) {
long logId = getLogId(state); store.commit(this, previousStatus == STATUS_COMMITTED);
store.commit(this, logId);
} }
} catch (Throwable e) { } catch (Throwable e) {
ex = e; ex = e;
......
...@@ -77,11 +77,22 @@ public class TransactionMap<K, V> { ...@@ -77,11 +77,22 @@ public class TransactionMap<K, V> {
// when none of the variables concurrently changes it's value. // when none of the variables concurrently changes it's value.
BitSet committingTransactions; BitSet committingTransactions;
MVMap.RootReference mapRootReference; MVMap.RootReference mapRootReference;
MVMap.RootReference undoLogRootReference; MVMap.RootReference[] undoLogRootReferences;
long undoLogSize;
do { do {
committingTransactions = store.committingTransactions.get(); committingTransactions = store.committingTransactions.get();
mapRootReference = map.getRoot(); mapRootReference = map.getRoot();
undoLogRootReference = store.undoLog.getRoot(); BitSet opentransactions = store.openTransactions.get();
undoLogRootReferences = new MVMap.RootReference[opentransactions.length()];
undoLogSize = 0;
for (int i = opentransactions.nextSetBit(0); i >= 0; i = opentransactions.nextSetBit(i+1)) {
MVMap<Long, Object[]> undoLog = store.undoLogs[i];
if (undoLog != null) {
MVMap.RootReference rootReference = undoLog.getRoot();
undoLogRootReferences[i] = rootReference;
undoLogSize += rootReference.root.getTotalCount();
}
}
} while(committingTransactions != store.committingTransactions.get() || } while(committingTransactions != store.committingTransactions.get() ||
mapRootReference != map.getRoot()); mapRootReference != map.getRoot());
// Now we have a snapshot, where mapRootReference points to state of the map, // Now we have a snapshot, where mapRootReference points to state of the map,
...@@ -89,8 +100,6 @@ public class TransactionMap<K, V> { ...@@ -89,8 +100,6 @@ public class TransactionMap<K, V> {
// and committingTransactions mask tells us which of seemingly uncommitted changes // and committingTransactions mask tells us which of seemingly uncommitted changes
// should be considered as committed. // should be considered as committed.
// Subsequent processing uses this snapshot info only. // Subsequent processing uses this snapshot info only.
Page undoRootPage = undoLogRootReference.root;
long undoLogSize = undoRootPage.getTotalCount();
Page mapRootPage = mapRootReference.root; Page mapRootPage = mapRootReference.root;
long size = mapRootPage.getTotalCount(); long size = mapRootPage.getTotalCount();
// if we are looking at the map without any uncommitted values // if we are looking at the map without any uncommitted values
...@@ -112,7 +121,8 @@ public class TransactionMap<K, V> { ...@@ -112,7 +121,8 @@ public class TransactionMap<K, V> {
long operationId = currentValue.getOperationId(); long operationId = currentValue.getOperationId();
if (operationId != 0) { // skip committed entries if (operationId != 0) { // skip committed entries
int txId = TransactionStore.getTransactionId(operationId); int txId = TransactionStore.getTransactionId(operationId);
boolean isVisible = txId == transaction.transactionId || committingTransactions.get(txId); boolean isVisible = txId == transaction.transactionId ||
committingTransactions.get(txId);
Object v = isVisible ? currentValue.value : currentValue.getCommittedValue(); Object v = isVisible ? currentValue.value : currentValue.getCommittedValue();
if (v == null) { if (v == null) {
--size; --size;
...@@ -120,26 +130,31 @@ public class TransactionMap<K, V> { ...@@ -120,26 +130,31 @@ public class TransactionMap<K, V> {
} }
} }
} else { } else {
// The undo log is much smaller than the map - scan the undo log, and then lookup relevant map entry. // The undo logs are much smaller than the map - scan all undo logs, and then lookup relevant map entry.
Cursor<Long, Object[]> cursor = new Cursor<>(undoRootPage, null); for (MVMap.RootReference undoLogRootReference : undoLogRootReferences) {
while(cursor.hasNext()) { if (undoLogRootReference != null) {
cursor.next(); Cursor<Long, Object[]> cursor = new Cursor<>(undoLogRootReference.root, null);
Object op[] = cursor.getValue(); while (cursor.hasNext()) {
if ((int)op[0] == map.getId()) { cursor.next();
VersionedValue currentValue = map.get(mapRootPage, op[1]); Object op[] = cursor.getValue();
// If map entry is not there, then we never counted it, in the first place, so skip it. if ((int) op[0] == map.getId()) {
// This is possible when undo entry exists because it belongs VersionedValue currentValue = map.get(mapRootPage, op[1]);
// to a committed but not yet closed transaction, // If map entry is not there, then we never counted it, in the first place, so skip it.
// and it was later deleted by some other already committed and closed transaction. // This is possible when undo entry exists because it belongs
if (currentValue != null) { // to a committed but not yet closed transaction,
// only the last undo entry for any given map key should be considered // and it was later deleted by some other already committed and closed transaction.
long operationId = cursor.getKey(); if (currentValue != null) {
if (currentValue.getOperationId() == operationId) { // only the last undo entry for any given map key should be considered
int txId = TransactionStore.getTransactionId(operationId); long operationId = cursor.getKey();
boolean isVisible = txId == transaction.transactionId || committingTransactions.get(txId); if (currentValue.getOperationId() == operationId) {
Object v = isVisible ? currentValue.value : currentValue.getCommittedValue(); int txId = TransactionStore.getTransactionId(operationId);
if (v == null) { boolean isVisible = txId == transaction.transactionId ||
--size; committingTransactions.get(txId);
Object v = isVisible ? currentValue.value : currentValue.getCommittedValue();
if (v == null) {
--size;
}
}
} }
} }
} }
......
...@@ -12,6 +12,7 @@ import java.util.Iterator; ...@@ -12,6 +12,7 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.concurrent.atomic.AtomicReferenceArray;
import org.h2.mvstore.Cursor;
import org.h2.mvstore.DataUtils; import org.h2.mvstore.DataUtils;
import org.h2.mvstore.MVMap; import org.h2.mvstore.MVMap;
import org.h2.mvstore.MVStore; import org.h2.mvstore.MVStore;
...@@ -41,7 +42,7 @@ public class TransactionStore { ...@@ -41,7 +42,7 @@ public class TransactionStore {
private final MVMap<Integer, Object[]> preparedTransactions; private final MVMap<Integer, Object[]> preparedTransactions;
/** /**
* The undo log. * Undo logs.
* <p> * <p>
* If the first entry for a transaction doesn't have a logId * If the first entry for a transaction doesn't have a logId
* of 0, then the transaction is partially committed (which means rollback * of 0, then the transaction is partially committed (which means rollback
...@@ -50,7 +51,9 @@ public class TransactionStore { ...@@ -50,7 +51,9 @@ public class TransactionStore {
* <p> * <p>
* Key: opId, value: [ mapId, key, oldValue ]. * Key: opId, value: [ mapId, key, oldValue ].
*/ */
final MVMap<Long, Object[]> undoLog; @SuppressWarnings("unchecked")
final MVMap<Long,Object[]> undoLogs[] = new MVMap[MAX_OPEN_TRANSACTIONS];
private final MVMap.Builder<Long,Object[]> undoLogBuilder;
private final DataType dataType; private final DataType dataType;
...@@ -91,6 +94,9 @@ public class TransactionStore { ...@@ -91,6 +94,9 @@ public class TransactionStore {
*/ */
private int nextTempMapId; private int nextTempMapId;
private static final String UNDO_LOG_NAME_PEFIX = "undoLog";
private static final char UNDO_LOG_COMMITTED = '-'; // must come before open in lexicographical order
private static final char UNDO_LOG_OPEN = '.';
/** /**
* Hard limit on the number of concurrently opened transactions * Hard limit on the number of concurrently opened transactions
...@@ -99,6 +105,11 @@ public class TransactionStore { ...@@ -99,6 +105,11 @@ public class TransactionStore {
private static final int MAX_OPEN_TRANSACTIONS = 65535; private static final int MAX_OPEN_TRANSACTIONS = 65535;
public static String getUndoLogName(boolean committed, int transactionId) {
return UNDO_LOG_NAME_PEFIX +
(committed ? UNDO_LOG_COMMITTED : UNDO_LOG_OPEN) +
(transactionId > 0 ? String.valueOf(transactionId) : "");
}
/** /**
* Create a new transaction store. * Create a new transaction store.
...@@ -126,15 +137,7 @@ public class TransactionStore { ...@@ -126,15 +137,7 @@ public class TransactionStore {
ArrayType undoLogValueType = new ArrayType(new DataType[]{ ArrayType undoLogValueType = new ArrayType(new DataType[]{
new ObjectDataType(), dataType, oldValueType new ObjectDataType(), dataType, oldValueType
}); });
MVMap.Builder<Long, Object[]> builder = undoLogBuilder = new MVMap.Builder<Long, Object[]>().valueType(undoLogValueType);
new MVMap.Builder<Long, Object[]>().
valueType(undoLogValueType);
undoLog = store.openMap("undoLog", builder);
if (undoLog.getValueType() != undoLogValueType) {
throw DataUtils.newIllegalStateException(
DataUtils.ERROR_TRANSACTION_CORRUPT,
"Undo map open with a different value type");
}
} }
/** /**
...@@ -143,10 +146,6 @@ public class TransactionStore { ...@@ -143,10 +146,6 @@ public class TransactionStore {
* in which case the store can only be used for reading. * in which case the store can only be used for reading.
*/ */
public void init() { public void init() {
init(RollbackListener.NONE);
}
public synchronized void init(RollbackListener listener) {
if (!init) { if (!init) {
// remove all temporary maps // remove all temporary maps
for (String mapName : store.getMapNames()) { for (String mapName : store.getMapNames()) {
...@@ -155,32 +154,32 @@ public class TransactionStore { ...@@ -155,32 +154,32 @@ public class TransactionStore {
store.removeMap(temp); store.removeMap(temp);
} }
} }
if (!undoLog.isEmpty()) {
Long key = undoLog.firstKey(); for (String mapName : store.getMapNames()) {
while (key != null) { if (mapName.startsWith(UNDO_LOG_NAME_PEFIX)) {
int transactionId = getTransactionId(key); if (store.hasData(mapName)) {
if (!openTransactions.get().get(transactionId)) { int transactionId = Integer.parseInt(mapName.substring(UNDO_LOG_NAME_PEFIX.length() + 1));
Object[] data = preparedTransactions.get(transactionId); VersionedBitSet openTxBitSet = openTransactions.get();
int status; if (!openTxBitSet.get(transactionId)) {
String name; Object[] data = preparedTransactions.get(transactionId);
if (data == null) { int status;
if (undoLog.containsKey(getOperationId(transactionId, 0))) { String name;
status = Transaction.STATUS_OPEN; if (data == null) {
status = mapName.charAt(UNDO_LOG_NAME_PEFIX.length()) == UNDO_LOG_OPEN ?
Transaction.STATUS_OPEN : Transaction.STATUS_COMMITTED;
name = null;
} else { } else {
status = Transaction.STATUS_COMMITTING; status = (Integer) data[0];
name = (String) data[1];
} }
name = null; MVMap<Long, Object[]> undoLog = store.openMap(mapName, undoLogBuilder);
} else { undoLogs[transactionId] = undoLog;
status = (Integer) data[0]; Long lastUndoKey = undoLog.lastKey();
name = (String) data[1]; assert lastUndoKey != null;
assert getTransactionId(lastUndoKey) == transactionId;
long logId = getLogId(lastUndoKey) + 1;
registerTransaction(transactionId, status, name, logId, timeoutMillis, 0, RollbackListener.NONE);
} }
long nextTxUndoKey = getOperationId(transactionId + 1, 0);
Long lastUndoKey = undoLog.lowerKey(nextTxUndoKey);
assert lastUndoKey != null;
assert getTransactionId(lastUndoKey) == transactionId;
long logId = getLogId(lastUndoKey) + 1;
registerTransaction(transactionId, status, name, logId, timeoutMillis, 0, listener);
key = undoLog.ceilingKey(nextTxUndoKey);
} }
} }
} }
...@@ -337,6 +336,11 @@ public class TransactionStore { ...@@ -337,6 +336,11 @@ public class TransactionStore {
assert transactions.get(transactionId) == null; assert transactions.get(transactionId) == null;
transactions.set(transactionId, transaction); transactions.set(transactionId, transaction);
if (undoLogs[transactionId] == null) {
String undoName = getUndoLogName(status == Transaction.STATUS_COMMITTED, transactionId);
MVMap<Long, Object[]> undoLog = store.openMap(undoName, undoLogBuilder);
undoLogs[transactionId] = undoLog;
}
return transaction; return transaction;
} }
...@@ -345,7 +349,7 @@ public class TransactionStore { ...@@ -345,7 +349,7 @@ public class TransactionStore {
* *
* @param t the transaction * @param t the transaction
*/ */
synchronized void storeTransaction(Transaction t) { void storeTransaction(Transaction t) {
if (t.getStatus() == Transaction.STATUS_PREPARED || if (t.getStatus() == Transaction.STATUS_PREPARED ||
t.getName() != null) { t.getName() != null) {
Object[] v = { t.getStatus(), t.getName() }; Object[] v = { t.getStatus(), t.getName() };
...@@ -362,29 +366,27 @@ public class TransactionStore { ...@@ -362,29 +366,27 @@ public class TransactionStore {
* @param undoLogRecord Object[mapId, key, previousValue] * @param undoLogRecord Object[mapId, key, previousValue]
*/ */
long addUndoLogRecord(int transactionId, long logId, Object[] undoLogRecord) { long addUndoLogRecord(int transactionId, long logId, Object[] undoLogRecord) {
MVMap<Long, Object[]> undoLog = undoLogs[transactionId];
Long undoKey = getOperationId(transactionId, logId); Long undoKey = getOperationId(transactionId, logId);
if (logId == 0) { if (logId == 0 && !undoLog.isEmpty()) {
if (undoLog.containsKey(undoKey)) { throw DataUtils.newIllegalStateException(
throw DataUtils.newIllegalStateException( DataUtils.ERROR_TOO_MANY_OPEN_TRANSACTIONS,
DataUtils.ERROR_TOO_MANY_OPEN_TRANSACTIONS, "An old transaction with the same id " +
"An old transaction with the same id " + "is still open: {0}",
"is still open: {0}", transactionId);
transactionId);
}
} }
undoLog.put(undoKey, undoLogRecord); undoLog.put(undoKey, undoLogRecord);
return undoKey; return undoKey;
} }
/** /**
* Remove a log entry. * Remove an undo log entry.
*
* @param transactionId id of the transaction * @param transactionId id of the transaction
* @param logId sequential number of the log record within transaction * @param logId sequential number of the log record within transaction
*/ */
public void removeUndoLogRecord(int transactionId, long logId) { public void removeUndoLogRecord(int transactionId, long logId) {
Long undoKey = getOperationId(transactionId, logId); Long undoKey = getOperationId(transactionId, logId);
Object[] old = undoLog.remove(undoKey); Object[] old = undoLogs[transactionId].remove(undoKey);
if (old == null) { if (old == null) {
throw DataUtils.newIllegalStateException( throw DataUtils.newIllegalStateException(
DataUtils.ERROR_TRANSACTION_ILLEGAL_STATE, DataUtils.ERROR_TRANSACTION_ILLEGAL_STATE,
...@@ -400,51 +402,49 @@ public class TransactionStore { ...@@ -400,51 +402,49 @@ public class TransactionStore {
* @param <V> the value type * @param <V> the value type
* @param map the map * @param map the map
*/ */
synchronized <K, V> void removeMap(TransactionMap<K, V> map) { <K, V> void removeMap(TransactionMap<K, V> map) {
store.removeMap(map.map); store.removeMap(map.map, true);
} }
/** /**
* Commit a transaction. * Commit a transaction.
* * @param t transaction to commit
* @param t the transaction * @param recovery if called during initial transaction recovery procedure
* @param maxLogId the last log id * therefore undo log is stored under "committed" name already
*/ */
void commit(Transaction t, long maxLogId) { void commit(Transaction t, boolean recovery) {
if (store.isClosed()) { if (!store.isClosed()) {
return; int transactionId = t.transactionId;
} // this is an atomic action that causes all changes
int transactionId = t.transactionId; // made by this transaction, to be considered as "committed"
// this is an atomic action that causes all changes flipCommittingTransactionsBit(transactionId, true);
// made by this transaction, to be considered as "committed"
flipCommittingTransactionsBit(transactionId, true); CommitDecisionMaker commitDecisionMaker = new CommitDecisionMaker();
try {
CommitDecisionMaker commitDecisionMaker = new CommitDecisionMaker(); MVMap<Long, Object[]> undoLog = undoLogs[transactionId];
try { if(!recovery) {
for (long logId = 0; logId < maxLogId; logId++) { store.renameMap(undoLog, getUndoLogName(true, transactionId));
Long undoKey = getOperationId(transactionId, logId);
Object[] op = undoLog.get(undoKey);
if (op == null) {
// partially committed: load next
undoKey = undoLog.ceilingKey(undoKey);
if (undoKey == null ||
getTransactionId(undoKey) != transactionId) {
break;
}
logId = getLogId(undoKey) - 1;
continue;
} }
int mapId = (Integer) op[0]; try {
MVMap<Object, VersionedValue> map = openMap(mapId); Cursor<Long, Object[]> cursor = undoLog.cursor(null);
if (map != null) { // might be null if map was removed later while (cursor.hasNext()) {
Object key = op[1]; Long undoKey = cursor.next();
commitDecisionMaker.setUndoKey(undoKey); Object[] op = cursor.getValue();
map.operate(key, null, commitDecisionMaker); int mapId = (Integer) op[0];
MVMap<Object, VersionedValue> map = openMap(mapId);
if (map != null) { // might be null if map was removed later
Object key = op[1];
commitDecisionMaker.setUndoKey(undoKey);
map.operate(key, null, commitDecisionMaker);
}
}
undoLog.clear();
} finally {
store.renameMap(undoLog, getUndoLogName(false, transactionId));
} }
undoLog.remove(undoKey); } finally {
flipCommittingTransactionsBit(transactionId, false);
} }
} finally {
flipCommittingTransactionsBit(transactionId, false);
} }
} }
...@@ -541,11 +541,9 @@ public class TransactionStore { ...@@ -541,11 +541,9 @@ public class TransactionStore {
* (even if they are fully rolled back), * (even if they are fully rolled back),
* false if it just performed a data access * false if it just performed a data access
*/ */
synchronized void endTransaction(Transaction t, boolean hasChanges) { void endTransaction(Transaction t, boolean hasChanges) {
t.closeIt(); t.closeIt();
int txId = t.transactionId; int txId = t.transactionId;
assert transactions.get(txId) == t : transactions.get(txId) + " != " + t;
transactions.set(txId, null); transactions.set(txId, null);
boolean success; boolean success;
...@@ -562,13 +560,14 @@ public class TransactionStore { ...@@ -562,13 +560,14 @@ public class TransactionStore {
if (wasStored && !preparedTransactions.isClosed()) { if (wasStored && !preparedTransactions.isClosed()) {
preparedTransactions.remove(txId); preparedTransactions.remove(txId);
} }
if (wasStored || store.getAutoCommitDelay() == 0) { if (wasStored || store.getAutoCommitDelay() == 0) {
store.tryCommit(); store.tryCommit();
} else { } else {
// to avoid having to store the transaction log, if (isUndoEmpty()) {
// if there is no open transaction, // to avoid having to store the transaction log,
// and if there have been many changes, store them now // if there is no open transaction,
if (undoLog.isEmpty()) { // and if there have been many changes, store them now
int unsaved = store.getUnsavedMemory(); int unsaved = store.getUnsavedMemory();
int max = store.getAutoCommitMemory(); int max = store.getAutoCommitMemory();
// save at 3/4 capacity // save at 3/4 capacity
...@@ -580,6 +579,17 @@ public class TransactionStore { ...@@ -580,6 +579,17 @@ public class TransactionStore {
} }
} }
private boolean isUndoEmpty() {
BitSet openTrans = openTransactions.get();
for (int i = openTrans.nextSetBit(0); i >= 0; i = openTrans.nextSetBit(i + 1)) {
MVMap<Long, Object[]> undoLog = undoLogs[i];
if (undoLog != null && !undoLog.isEmpty()) {
return false;
}
}
return true;
}
Transaction getTransaction(int transactionId) { Transaction getTransaction(int transactionId) {
return transactions.get(transactionId); return transactions.get(transactionId);
} }
...@@ -593,6 +603,7 @@ public class TransactionStore { ...@@ -593,6 +603,7 @@ public class TransactionStore {
*/ */
void rollbackTo(Transaction t, long maxLogId, long toLogId) { void rollbackTo(Transaction t, long maxLogId, long toLogId) {
int transactionId = t.getId(); int transactionId = t.getId();
MVMap<Long, Object[]> undoLog = undoLogs[transactionId];
RollbackDecisionMaker decisionMaker = new RollbackDecisionMaker(this, transactionId, toLogId, t.listener); RollbackDecisionMaker decisionMaker = new RollbackDecisionMaker(this, transactionId, toLogId, t.listener);
for (long logId = maxLogId - 1; logId >= toLogId; logId--) { for (long logId = maxLogId - 1; logId >= toLogId; logId--) {
Long undoKey = getOperationId(transactionId, logId); Long undoKey = getOperationId(transactionId, logId);
...@@ -612,6 +623,8 @@ public class TransactionStore { ...@@ -612,6 +623,8 @@ public class TransactionStore {
*/ */
Iterator<Change> getChanges(final Transaction t, final long maxLogId, Iterator<Change> getChanges(final Transaction t, final long maxLogId,
final long toLogId) { final long toLogId) {
final MVMap<Long, Object[]> undoLog = undoLogs[t.getId()];
return new Iterator<Change>() { return new Iterator<Change>() {
private long logId = maxLogId - 1; private long logId = maxLogId - 1;
...@@ -626,8 +639,7 @@ public class TransactionStore { ...@@ -626,8 +639,7 @@ public class TransactionStore {
if (op == null) { if (op == null) {
// partially rolled back: load previous // partially rolled back: load previous
undoKey = undoLog.floorKey(undoKey); undoKey = undoLog.floorKey(undoKey);
if (undoKey == null || if (undoKey == null || getTransactionId(undoKey) != transactionId) {
getTransactionId(undoKey) != transactionId) {
break; break;
} }
logId = getLogId(undoKey); logId = getLogId(undoKey);
......
...@@ -148,8 +148,13 @@ public class StringUtils { ...@@ -148,8 +148,13 @@ public class StringUtils {
* @return the Java representation * @return the Java representation
*/ */
public static String javaEncode(String s) { public static String javaEncode(String s) {
StringBuilder buff = new StringBuilder(s.length());
javaEncode(s, buff);
return buff.toString();
}
public static void javaEncode(String s, StringBuilder buff) {
int length = s.length(); int length = s.length();
StringBuilder buff = new StringBuilder(length);
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
char c = s.charAt(i); char c = s.charAt(i);
switch (c) { switch (c) {
...@@ -202,7 +207,6 @@ public class StringUtils { ...@@ -202,7 +207,6 @@ public class StringUtils {
} }
} }
} }
return buff.toString();
} }
/** /**
......
...@@ -513,8 +513,10 @@ public class TestConcurrent extends TestMVStore { ...@@ -513,8 +513,10 @@ public class TestConcurrent extends TestMVStore {
Thread.sleep(1); Thread.sleep(1);
} }
Exception e = task.getException(); Exception e = task.getException();
assertEquals(DataUtils.ERROR_CLOSED, if (e != null) {
DataUtils.getErrorCode(e.getMessage())); assertEquals(DataUtils.ERROR_CLOSED,
DataUtils.getErrorCode(e.getMessage()));
}
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
// sometimes storing works, in which case // sometimes storing works, in which case
// closing must fail // closing must fail
......
...@@ -151,7 +151,7 @@ public class TestStreamStore extends TestBase { ...@@ -151,7 +151,7 @@ public class TestStreamStore extends TestBase {
long readCount = s.getFileStore().getReadCount(); long readCount = s.getFileStore().getReadCount();
// the read count should be low because new blocks // the read count should be low because new blocks
// are appended at the end (not between existing blocks) // are appended at the end (not between existing blocks)
assertTrue("rc: " + readCount, readCount <= 17); assertTrue("rc: " + readCount, readCount <= 20);
map = s.openMap("data"); map = s.openMap("data");
assertTrue("size: " + map.size(), map.sizeAsLong() >= 200); assertTrue("size: " + map.size(), map.sizeAsLong() >= 200);
s.close(); s.close();
......
...@@ -204,6 +204,7 @@ public class TestTransactionStore extends TestBase { ...@@ -204,6 +204,7 @@ public class TestTransactionStore extends TestBase {
break; break;
} }
} }
task.get();
// we expect at least 10% the operations were successful // we expect at least 10% the operations were successful
assertTrue(failCount.toString() + " >= " + (count * 0.9), assertTrue(failCount.toString() + " >= " + (count * 0.9),
failCount.get() < count * 0.9); failCount.get() < count * 0.9);
...@@ -395,24 +396,22 @@ public class TestTransactionStore extends TestBase { ...@@ -395,24 +396,22 @@ public class TestTransactionStore extends TestBase {
store.close(); store.close();
s = MVStore.open(fileName); s = MVStore.open(fileName);
// roll back a bit, until we have some undo log entries // roll back a bit, until we have some undo log entries
assertTrue(s.hasMap("undoLog"));
for (int back = 0; back < 100; back++) { for (int back = 0; back < 100; back++) {
int minus = r.nextInt(10); int minus = r.nextInt(10);
s.rollbackTo(Math.max(0, s.getCurrentVersion() - minus)); s.rollbackTo(Math.max(0, s.getCurrentVersion() - minus));
MVMap<?, ?> undo = s.openMap("undoLog"); if (hasDataUndoLog(s)) {
if (undo.size() > 0) {
break; break;
} }
} }
// re-open the store, because we have opened // re-open TransactionStore, because we rolled back
// the undoLog map with the wrong data type // underlying MVStore without rolling back TranactionStore
s.close(); s.close();
s = MVStore.open(fileName); s = MVStore.open(fileName);
ts = new TransactionStore(s); ts = new TransactionStore(s);
List<Transaction> list = ts.getOpenTransactions(); List<Transaction> list = ts.getOpenTransactions();
if (list.size() != 0) { if (list.size() != 0) {
tx = list.get(0); tx = list.get(0);
if (tx.getStatus() == Transaction.STATUS_COMMITTING) { if (tx.getStatus() == Transaction.STATUS_COMMITTED) {
i++; i++;
} }
} }
...@@ -422,6 +421,15 @@ public class TestTransactionStore extends TestBase { ...@@ -422,6 +421,15 @@ public class TestTransactionStore extends TestBase {
} }
} }
private boolean hasDataUndoLog(MVStore s) {
for (int i = 0; i < 255; i++) {
if(s.hasData(TransactionStore.getUndoLogName(true, 1))) {
return true;
}
}
return false;
}
private void testGetModifiedMaps() { private void testGetModifiedMaps() {
MVStore s = MVStore.open(null); MVStore s = MVStore.open(null);
TransactionStore ts = new TransactionStore(s); TransactionStore ts = new TransactionStore(s);
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论