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

@ -205,7 +205,7 @@ module.exports = function (Y/* :any */) {
} }
length = target.id[1] - targetId[1] length = target.id[1] - targetId[1]
} }
if (target != null) { if (target != null) {
if (!target.deleted) { if (!target.deleted) {
callType = true callType = true
@ -240,10 +240,10 @@ module.exports = function (Y/* :any */) {
} else { } else {
left = null left = null
} }
// set here because it was deleted and/or gc'd // set here because it was deleted and/or gc'd
yield* this.setOperation(target) yield* this.setOperation(target)
/* /*
Check if it is possible to add right to the gc. Check if it is possible to add right to the gc.
Because this delete can't be responsible for left being gc'd, Because this delete can't be responsible for left being gc'd,
@ -380,7 +380,7 @@ module.exports = function (Y/* :any */) {
// gc is stronger, so reduce length of n // gc is stronger, so reduce length of n
n.len -= diff n.len -= diff
if (diff >= next.len) { if (diff >= next.len) {
// delete the missing range after next // delete the missing range after next
diff = diff - next.len // missing range after next diff = diff - next.len // missing range after next
if (diff > 0) { if (diff > 0) {
yield* this.ds.put(n) // unneccessary? TODO! yield* this.ds.put(n) // unneccessary? TODO!
@ -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) {