all tests working. Fixed an older bug: When gc an op I forgot to update the state. This only affected offline editing, and was very hard to catch in the past

This commit is contained in:
Kevin Jahns 2016-04-22 21:21:23 +01:00
parent bffd130b92
commit 895ec86ff6
4 changed files with 59 additions and 71 deletions

View File

@ -143,8 +143,8 @@ module.exports = function (Y/* :any */) {
break break
} }
} }
var conn = this
if (syncUser != null) { if (syncUser != null) {
var conn = this
this.currentSyncTarget = syncUser this.currentSyncTarget = syncUser
this.y.db.requestTransaction(function *() { this.y.db.requestTransaction(function *() {
var stateSet = yield* this.getStateSet() var stateSet = yield* this.getStateSet()
@ -157,14 +157,15 @@ module.exports = function (Y/* :any */) {
}) })
}) })
} else { } else {
this.isSynced = true
// call when synced listeners
for (var f of this.whenSyncedListeners) {
f()
}
this.whenSyncedListeners = []
this.y.db.requestTransaction(function *() { this.y.db.requestTransaction(function *() {
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
conn.isSynced = true
yield* this.garbageCollectAfterSync() yield* this.garbageCollectAfterSync()
// call whensynced listeners
for (var f of conn.whenSyncedListeners) {
f()
}
conn.whenSyncedListeners = []
}) })
} }
} }

View File

@ -75,10 +75,14 @@ module.exports = function (Y /* :any */) {
} }
this.gc1 = [] // first stage this.gc1 = [] // first stage
this.gc2 = [] // second stage -> after that, remove the op this.gc2 = [] // second stage -> after that, remove the op
this.gcTimeout = opts.gcTimeout || 50000 this.gcTimeout = !opts.gcTimeout ? 50000 : opts.gcTimeoutś
function garbageCollect () { function garbageCollect () {
return os.whenTransactionsFinished().then(function () { return os.whenTransactionsFinished().then(function () {
if (os.gc1.length > 0 || os.gc2.length > 0) { if (os.gc1.length > 0 || os.gc2.length > 0) {
if (!os.y.isConnected()) {
debugger
console.log('gc should be empty when disconnected!')
}
return new Promise((resolve) => { return new Promise((resolve) => {
os.requestTransaction(function * () { os.requestTransaction(function * () {
if (os.y.connector != null && os.y.connector.isSynced) { if (os.y.connector != null && os.y.connector.isSynced) {
@ -346,15 +350,16 @@ module.exports = function (Y /* :any */) {
// yield* this.store.operationAdded(this, op) // yield* this.store.operationAdded(this, op)
} else { } else {
// check if this op was defined // check if this op was defined
var defined = yield* this.getOperation(op.id) var defined = yield* this.getInsertion(op.id)
while (defined != null && defined.content != null) { while (defined != null && defined.content != null) {
// check if this op has a longer content in the case it is defined // check if this op has a longer content in the case it is defined
if (defined.content.length < op.content.length) { if (defined.id[1] + defined.content.length < op.id[1] + op.content.length) {
op.content.splice(0, defined.content.length) var overlapSize = defined.content.length - (op.id[1] - defined.id[1])
op.id = [op.id[0], op.id[1] + defined.content.length] op.content.splice(0, overlapSize)
op.left = defined.id op.id = [op.id[0], op.id[1] + overlapSize]
op.origin = defined.id op.left = Y.utils.getLastId(defined)
defined = yield* this.getOperation(op.id) op.origin = op.left
defined = yield* this.getOperation(op.id) // getOperation suffices here
} else { } else {
break break
} }

View File

@ -123,7 +123,6 @@ g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransaction
g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) { g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions) yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
yield Y.utils.globalRoom.flushAll() yield Y.utils.globalRoom.flushAll()
yield g.garbageCollectAllUsers(users)
for (var u in users) { for (var u in users) {
// TODO: here, we enforce that two users never sync at the same time with u[0] // TODO: here, we enforce that two users never sync at the same time with u[0]
// enforce that in the connector itself! // enforce that in the connector itself!

View File

@ -420,6 +420,9 @@ module.exports = function (Y/* :any */) {
operations that can be gc'd and add them to the garbage collector. operations that can be gc'd and add them to the garbage collector.
*/ */
* garbageCollectAfterSync () { * garbageCollectAfterSync () {
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
console.warn('gc should be empty after sync')
}
yield* this.os.iterate(this, null, null, function * (op) { yield* this.os.iterate(this, null, null, function * (op) {
if (op.gc) { if (op.gc) {
delete op.gc delete op.gc
@ -469,7 +472,9 @@ module.exports = function (Y/* :any */) {
var o = yield* this.getOperation(id) var o = yield* this.getOperation(id)
yield* this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd yield* this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd
// if op exists, then clean that mess up.. // if op exists, then clean that mess up..
if (o != null) { if (o == null) {
yield* this.updateState(id[0])
} else if (o != null) {
var deps = [] var deps = []
if (o.opContent != null) { if (o.opContent != null) {
deps.push(o.opContent) deps.push(o.opContent)
@ -647,9 +652,6 @@ module.exports = function (Y/* :any */) {
*/ */
* applyDeleteSet (ds) { * applyDeleteSet (ds) {
var deletions = [] var deletions = []
function createDeletions (user, start, len, gc) {
deletions.push([user, start, len, gc])
}
for (var user in ds) { for (var user in ds) {
var dv = ds[user] var dv = ds[user]
@ -676,14 +678,14 @@ module.exports = function (Y/* :any */) {
// delete maximum the len of d // delete maximum the len of d
// else delete as much as possible // else delete as much as possible
diff = Math.min(n.id[1] - d[0], d[1]) diff = Math.min(n.id[1] - d[0], d[1])
createDeletions(user, d[0], diff, d[2]) deletions.push([user, d[0], diff, d[2]])
} else { } else {
// 3) // 3)
diff = n.id[1] + n.len - d[0] // never null (see 1) diff = n.id[1] + n.len - d[0] // never null (see 1)
if (d[2] && !n.gc) { if (d[2] && !n.gc) {
// d marks as gc'd but n does not // d marks as gc'd but n does not
// then delete either way // then delete either way
createDeletions(user, d[0], Math.min(diff, d[1]), d[2]) deletions.push([user, d[0], Math.min(diff, d[1]), d[2]])
} }
} }
if (d[1] <= diff) { if (d[1] <= diff) {
@ -698,57 +700,38 @@ module.exports = function (Y/* :any */) {
// for the rest.. just apply it // for the rest.. just apply it
for (; pos < dv.length; pos++) { for (; pos < dv.length; pos++) {
d = dv[pos] d = dv[pos]
createDeletions(user, d[0], d[1], d[2]) deletions.push([user, d[0], d[1], d[2]])
} }
} }
for (var i = 0; i < deletions.length; i++) { for (var i = 0; i < deletions.length; i++) {
var del = deletions[i] var del = deletions[i]
// always try to delete.. // always try to delete..
var state = yield* this.getState(del[0]) yield* this.deleteOperation([del[0], del[1]], del[2])
if (del[1] < state.clock) {
yield* this.deleteOperation([del[0], del[1]], del[2])
if (del[3]) {
// gc..
yield* this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd
// remove operation..
var counter = del[1] + del[2]
while (counter >= del[1]) {
var o = yield* this.os.findWithUpperBound([del[0], counter - 1])
if (o == null) {
break
}
var oLen = o.content != null ? o.content.length : 1
if (o.id[0] !== del[0] || o.id[1] + oLen <= del[1]) {
// not in range
break
}
if (o.id[1] + oLen > del[1] + del[2]) {
// overlaps right
o = yield* this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1])
}
if (o.id[1] < del[1]) {
// overlaps left
o = yield* this.getInsertionCleanStart([del[0], del[1]])
}
counter = o.id[1]
yield* this.garbageCollectOperation(o.id)
}
}
} else {
if (del[3]) {
yield* this.markGarbageCollected([del[0], del[1]], del[2])
} else {
yield* this.markDeleted([del[0], del[1]], del[2])
}
}
if (del[3]) { if (del[3]) {
// check to increase the state of the respective user // gc..
if (state.clock >= del[1] && state.clock < del[1] + del[2]) { yield* this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd
state.clock = del[1] + del[2] // remove operation..
// also check if more expected operations were gc'd var counter = del[1] + del[2]
yield* this.checkDeleteStoreForState(state) // TODO: unneccessary? while (counter >= del[1]) {
// then set the state var o = yield* this.os.findWithUpperBound([del[0], counter - 1])
yield* this.setState(state) if (o == null) {
break
}
var oLen = o.content != null ? o.content.length : 1
if (o.id[0] !== del[0] || o.id[1] + oLen <= del[1]) {
// not in range
break
}
if (o.id[1] + oLen > del[1] + del[2]) {
// overlaps right
o = yield* this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1])
}
if (o.id[1] < del[1]) {
// overlaps left
o = yield* this.getInsertionCleanStart([del[0], del[1]])
}
counter = o.id[1]
yield* this.garbageCollectOperation(o.id)
} }
} }
if (this.store.forwardAppliedOperations) { if (this.store.forwardAppliedOperations) {