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 {
try {
return tryUpdate();
} catch (DbException e) {
for (Index index : createdIndexes) {
session.getDatabase().removeSchemaObject(session, index);
try {
for (Index index : createdIndexes) {
session.getDatabase().removeSchemaObject(session, index);
}
} catch (Throwable ex) {
e.addSuppressed(ex);
}
throw e;
} finally {
......
......@@ -159,10 +159,14 @@ public class CreateTable extends CommandWithColumns {
}
}
} catch (DbException e) {
db.checkPowerOff();
db.removeSchemaObject(session, table);
if (!transactional) {
session.commit(true);
try {
db.checkPowerOff();
db.removeSchemaObject(session, table);
if (!transactional) {
session.commit(true);
}
} catch (Throwable ex) {
e.addSuppressed(ex);
}
throw e;
}
......
......@@ -752,6 +752,9 @@ public class Database implements DataHandler {
getPageStore();
}
}
if(mvStore != null) {
mvStore.getTransactionStore().init();
}
systemUser = new User(this, 0, SYSTEM_USER_NAME, true);
mainSchema = new Schema(this, 0, Constants.SCHEMA_MAIN, systemUser, true);
infoSchema = new Schema(this, -1, "INFORMATION_SCHEMA", systemUser, true);
......@@ -762,9 +765,6 @@ public class Database implements DataHandler {
systemUser.setAdmin(true);
systemSession = new Session(this, systemUser, ++nextSessionId);
lobSession = new Session(this, systemUser, ++nextSessionId);
if(mvStore != null) {
mvStore.getTransactionStore().init(systemSession);
}
CreateTableData data = new CreateTableData();
ArrayList<Column> cols = data.columns;
Column columnId = new Column("ID", Value.INT);
......
......@@ -300,10 +300,10 @@ public class Trace {
if (!space) {
buff.append(' ');
}
buff.append("*/").
append(StringUtils.javaEncode(sql)).
append(StringUtils.javaEncode(params)).
append(';');
buff.append("*/");
StringUtils.javaEncode(sql, buff);
StringUtils.javaEncode(params, buff);
buff.append(';');
sql = buff.toString();
traceWriter.write(TraceSystem.INFO, module, sql, null);
}
......
......@@ -128,17 +128,15 @@ public class FileStore {
if (file != null) {
return;
}
if (fileName != null) {
// ensure the Cache file system is registered
FilePathCache.INSTANCE.getScheme();
FilePath p = FilePath.get(fileName);
// if no explicit scheme was specified, NIO is used
if (p instanceof FilePathDisk &&
!fileName.startsWith(p.getScheme() + ":")) {
// ensure the NIO file system is registered
FilePathNio.class.getName();
fileName = "nio:" + fileName;
}
// ensure the Cache file system is registered
FilePathCache.INSTANCE.getScheme();
FilePath p = FilePath.get(fileName);
// if no explicit scheme was specified, NIO is used
if (p instanceof FilePathDisk &&
!fileName.startsWith(p.getScheme() + ":")) {
// ensure the NIO file system is registered
FilePathNio.class.getName();
fileName = "nio:" + fileName;
}
this.fileName = fileName;
FilePath f = FilePath.get(fileName);
......
......@@ -253,7 +253,7 @@ public class MVTableEngine implements TableEngine {
public void initTransactions() {
List<Transaction> list = transactionStore.getOpenTransactions();
for (Transaction t : list) {
if (t.getStatus() == Transaction.STATUS_COMMITTING) {
if (t.getStatus() == Transaction.STATUS_COMMITTED) {
t.commit();
} else if (t.getStatus() != Transaction.STATUS_PREPARED) {
t.rollback();
......
......@@ -30,19 +30,22 @@ final class RollbackDecisionMaker extends MVMap.DecisionMaker<Object[]> {
@Override
public MVMap.Decision decide(Object[] existingValue, Object[] providedValue) {
assert decision == null;
assert existingValue != null;
VersionedValue valueToRestore = (VersionedValue) existingValue[2];
long operationId;
if (valueToRestore == null ||
(operationId = valueToRestore.getOperationId()) == 0 ||
TransactionStore.getTransactionId(operationId) == transactionId
&& TransactionStore.getLogId(operationId) < toLogId) {
int mapId = (Integer) existingValue[0];
MVMap<Object, VersionedValue> map = store.openMap(mapId);
if (map != null && !map.isClosed()) {
Object key = existingValue[1];
VersionedValue previousValue = map.operate(key, valueToRestore, MVMap.DecisionMaker.DEFAULT);
listener.onRollback(map, key, previousValue, valueToRestore);
// normaly existingValue will always be there except of db initialization
// where some undo log enty was captured on disk but actual map entry was not
if (existingValue != null ) {
VersionedValue valueToRestore = (VersionedValue) existingValue[2];
long operationId;
if (valueToRestore == null ||
(operationId = valueToRestore.getOperationId()) == 0 ||
TransactionStore.getTransactionId(operationId) == transactionId
&& TransactionStore.getLogId(operationId) < toLogId) {
int mapId = (Integer) existingValue[0];
MVMap<Object, VersionedValue> map = store.openMap(mapId);
if (map != null && !map.isClosed()) {
Object key = existingValue[1];
VersionedValue previousValue = map.operate(key, valueToRestore, MVMap.DecisionMaker.DEFAULT);
listener.onRollback(map, key, previousValue, valueToRestore);
}
}
}
decision = MVMap.Decision.REMOVE;
......
......@@ -32,41 +32,36 @@ public class Transaction {
*/
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
* 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.
* Nevertheless, those changes should be already viewed by other
* 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.
* 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
* 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,
* 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[] = {
"CLOSED", "OPEN", "PREPARED", "COMMITTING",
"COMMITTED", "ROLLING_BACK", "ROLLED_BACK"
"CLOSED", "OPEN", "PREPARED", "COMMITTED", "ROLLING_BACK", "ROLLED_BACK"
};
static final int LOG_ID_BITS = 40;
private static final int LOG_ID_BITS1 = LOG_ID_BITS + 1;
......@@ -175,6 +170,11 @@ public class Transaction {
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) {
while (true) {
long currentState = statusAndLogId.get();
......@@ -192,23 +192,19 @@ public class Transaction {
case STATUS_PREPARED:
valid = currentStatus == STATUS_OPEN;
break;
case STATUS_COMMITTING:
case STATUS_COMMITTED:
valid = currentStatus == STATUS_OPEN ||
currentStatus == STATUS_PREPARED ||
// this case is only possible if called
// from endLeftoverTransactions()
currentStatus == STATUS_COMMITTING;
break;
case STATUS_COMMITTED:
valid = currentStatus == STATUS_COMMITTING;
currentStatus == STATUS_COMMITTED;
break;
case STATUS_ROLLED_BACK:
valid = currentStatus == STATUS_OPEN ||
currentStatus == STATUS_PREPARED;
break;
case STATUS_CLOSED:
valid = currentStatus == STATUS_COMMITTING ||
currentStatus == STATUS_COMMITTED ||
valid = currentStatus == STATUS_COMMITTED ||
currentStatus == STATUS_ROLLED_BACK;
break;
default:
......@@ -365,11 +361,11 @@ public class Transaction {
Throwable ex = null;
boolean hasChanges = false;
try {
long state = setStatus(STATUS_COMMITTING);
long state = setStatus(STATUS_COMMITTED);
hasChanges = hasChanges(state);
int previousStatus = getStatus(state);
if (hasChanges) {
long logId = getLogId(state);
store.commit(this, logId);
store.commit(this, previousStatus == STATUS_COMMITTED);
}
} catch (Throwable e) {
ex = e;
......
......@@ -77,11 +77,22 @@ public class TransactionMap<K, V> {
// when none of the variables concurrently changes it's value.
BitSet committingTransactions;
MVMap.RootReference mapRootReference;
MVMap.RootReference undoLogRootReference;
MVMap.RootReference[] undoLogRootReferences;
long undoLogSize;
do {
committingTransactions = store.committingTransactions.get();
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() ||
mapRootReference != map.getRoot());
// Now we have a snapshot, where mapRootReference points to state of the map,
......@@ -89,8 +100,6 @@ public class TransactionMap<K, V> {
// and committingTransactions mask tells us which of seemingly uncommitted changes
// should be considered as committed.
// Subsequent processing uses this snapshot info only.
Page undoRootPage = undoLogRootReference.root;
long undoLogSize = undoRootPage.getTotalCount();
Page mapRootPage = mapRootReference.root;
long size = mapRootPage.getTotalCount();
// if we are looking at the map without any uncommitted values
......@@ -112,7 +121,8 @@ public class TransactionMap<K, V> {
long operationId = currentValue.getOperationId();
if (operationId != 0) { // skip committed entries
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();
if (v == null) {
--size;
......@@ -120,26 +130,31 @@ public class TransactionMap<K, V> {
}
}
} else {
// The undo log is much smaller than the map - scan the undo log, and then lookup relevant map entry.
Cursor<Long, Object[]> cursor = new Cursor<>(undoRootPage, null);
while(cursor.hasNext()) {
cursor.next();
Object op[] = cursor.getValue();
if ((int)op[0] == map.getId()) {
VersionedValue currentValue = map.get(mapRootPage, op[1]);
// If map entry is not there, then we never counted it, in the first place, so skip it.
// This is possible when undo entry exists because it belongs
// to a committed but not yet closed transaction,
// and it was later deleted by some other already committed and closed transaction.
if (currentValue != null) {
// only the last undo entry for any given map key should be considered
long operationId = cursor.getKey();
if (currentValue.getOperationId() == operationId) {
int txId = TransactionStore.getTransactionId(operationId);
boolean isVisible = txId == transaction.transactionId || committingTransactions.get(txId);
Object v = isVisible ? currentValue.value : currentValue.getCommittedValue();
if (v == null) {
--size;
// The undo logs are much smaller than the map - scan all undo logs, and then lookup relevant map entry.
for (MVMap.RootReference undoLogRootReference : undoLogRootReferences) {
if (undoLogRootReference != null) {
Cursor<Long, Object[]> cursor = new Cursor<>(undoLogRootReference.root, null);
while (cursor.hasNext()) {
cursor.next();
Object op[] = cursor.getValue();
if ((int) op[0] == map.getId()) {
VersionedValue currentValue = map.get(mapRootPage, op[1]);
// If map entry is not there, then we never counted it, in the first place, so skip it.
// This is possible when undo entry exists because it belongs
// to a committed but not yet closed transaction,
// and it was later deleted by some other already committed and closed transaction.
if (currentValue != null) {
// only the last undo entry for any given map key should be considered
long operationId = cursor.getKey();
if (currentValue.getOperationId() == operationId) {
int txId = TransactionStore.getTransactionId(operationId);
boolean isVisible = txId == transaction.transactionId ||
committingTransactions.get(txId);
Object v = isVisible ? currentValue.value : currentValue.getCommittedValue();
if (v == null) {
--size;
}
}
}
}
}
......
......@@ -148,8 +148,13 @@ public class StringUtils {
* @return the Java representation
*/
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();
StringBuilder buff = new StringBuilder(length);
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
switch (c) {
......@@ -202,7 +207,6 @@ public class StringUtils {
}
}
}
return buff.toString();
}
/**
......
......@@ -513,8 +513,10 @@ public class TestConcurrent extends TestMVStore {
Thread.sleep(1);
}
Exception e = task.getException();
assertEquals(DataUtils.ERROR_CLOSED,
DataUtils.getErrorCode(e.getMessage()));
if (e != null) {
assertEquals(DataUtils.ERROR_CLOSED,
DataUtils.getErrorCode(e.getMessage()));
}
} catch (IllegalStateException e) {
// sometimes storing works, in which case
// closing must fail
......
......@@ -151,7 +151,7 @@ public class TestStreamStore extends TestBase {
long readCount = s.getFileStore().getReadCount();
// the read count should be low because new blocks
// are appended at the end (not between existing blocks)
assertTrue("rc: " + readCount, readCount <= 17);
assertTrue("rc: " + readCount, readCount <= 20);
map = s.openMap("data");
assertTrue("size: " + map.size(), map.sizeAsLong() >= 200);
s.close();
......
......@@ -204,6 +204,7 @@ public class TestTransactionStore extends TestBase {
break;
}
}
task.get();
// we expect at least 10% the operations were successful
assertTrue(failCount.toString() + " >= " + (count * 0.9),
failCount.get() < count * 0.9);
......@@ -395,24 +396,22 @@ public class TestTransactionStore extends TestBase {
store.close();
s = MVStore.open(fileName);
// roll back a bit, until we have some undo log entries
assertTrue(s.hasMap("undoLog"));
for (int back = 0; back < 100; back++) {
int minus = r.nextInt(10);
s.rollbackTo(Math.max(0, s.getCurrentVersion() - minus));
MVMap<?, ?> undo = s.openMap("undoLog");
if (undo.size() > 0) {
if (hasDataUndoLog(s)) {
break;
}
}
// re-open the store, because we have opened
// the undoLog map with the wrong data type
// re-open TransactionStore, because we rolled back
// underlying MVStore without rolling back TranactionStore
s.close();
s = MVStore.open(fileName);
ts = new TransactionStore(s);
List<Transaction> list = ts.getOpenTransactions();
if (list.size() != 0) {
tx = list.get(0);
if (tx.getStatus() == Transaction.STATUS_COMMITTING) {
if (tx.getStatus() == Transaction.STATUS_COMMITTED) {
i++;
}
}
......@@ -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() {
MVStore s = MVStore.open(null);
TransactionStore ts = new TransactionStore(s);
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论