Skip to content

Commit c60adf9

Browse files
authored
Merge pull request #354 from modelix/fix/LinearHistory
fix(model-datastructure): fix LinearHistory
2 parents 7fed07e + ad7618a commit c60adf9

File tree

7 files changed

+398
-113
lines changed

7 files changed

+398
-113
lines changed
Lines changed: 113 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,138 @@
11
package org.modelix.model
22

33
import org.modelix.model.lazy.CLVersion
4-
import org.modelix.model.lazy.IDeserializingKeyValueStore
5-
import org.modelix.model.lazy.KVEntryReference
6-
import org.modelix.model.persistent.CPVersion
74

8-
/**
9-
* Was introduced in https://github.com/modelix/modelix/commit/19c74bed5921028af3ac3ee9d997fc1c4203ad44
10-
* together with the UndoOp. The idea is that an undo should only revert changes if there is no other change that relies
11-
* on it. In that case the undo should do nothing, to not indirectly undo newer changes.
12-
* For example, if you added a node and someone else started changing properties on the that node, your undo should not
13-
* remove the node to not lose the property changes.
14-
* This requires the versions to be ordered in a way that the undo appears later.
15-
*/
16-
class LinearHistory(val baseVersionHash: String?) {
5+
class LinearHistory(private val baseVersionHash: String?) {
6+
/**
7+
* Children indexed by their parent versions.
8+
* A version is a parent of a child,
9+
* if the [CLVersion.baseVersion], the [CLVersion.getMergedVersion1] or [CLVersion.getMergedVersion2]
10+
*/
11+
private val byVersionChildren = mutableMapOf<CLVersion, MutableSet<CLVersion>>()
1712

18-
val version2directDescendants: MutableMap<Long, Set<Long>> = HashMap()
19-
val versions: MutableMap<Long, CLVersion> = LinkedHashMap()
13+
/**
14+
* Global roots are versions without parents.
15+
* It may be only the version denoted [baseVersionHash]
16+
* or many versions, if no base version was specified and versions without a common global root are ordered.
17+
*/
18+
private val globalRoot = mutableSetOf<CLVersion>()
2019

2120
/**
22-
* @param fromVersions it is assumed that the versions are sorted by the oldest version first. When merging a new
23-
* version into an existing one the new version should appear after the existing one. The resulting order
24-
* will prefer existing versions to new ones, meaning during the conflict resolution the existing changes
25-
* have a higher probability of surviving.
26-
* @returns oldest version first
21+
* The distance of a version from its root.
22+
* Aka how many children a between the root and a version.
23+
*/
24+
private val byVersionDistanceFromGlobalRoot = mutableMapOf<CLVersion, Int>()
25+
26+
/**
27+
* Returns all versions between the [fromVersions] and a common version.
28+
* The common version may be identified by [baseVersionHash].
29+
* If no [baseVersionHash] is given, the common version wile be the first version
30+
* aka the version without a [CLVersion.baseVersion].
31+
*
32+
* The order also ensures three properties:
33+
* 1. The versions are ordered topologically starting with the versions without parents.
34+
* 2. The order is also "monotonic".
35+
* This means adding a version to the set of all versions will never change
36+
* the order of versions that were previously in the history.
37+
* For example, given versions 1, 2 and 3:
38+
* If 1 and 2 are ordered as (1, 2), ordering 1, 2 and 3 will never produce (2, 3, 1).
39+
* 3 can come anywhere (respecting the topological ordering), but 2 has to come after 1.
40+
* 3. "Close versions are kept together"
41+
* Formally: A version that has only one child (ignoring) should always come before the child.
42+
* Example: 1 <- 2 <- 3 and 1 <- x, then [1, 2, 4, 3] is not allowed,
43+
* because 3 is the only child of 2.
44+
* Valid orders would be (1, x, 3, 4) and (1, x, 2, 3)
45+
* This is relevant for UnduOp and RedoOp.
46+
* See UndoTest.
2747
*/
2848
fun load(vararg fromVersions: CLVersion): List<CLVersion> {
29-
for (fromVersion in fromVersions) {
30-
collect(fromVersion)
31-
}
49+
// Traverse the versions once to index need data:
50+
// * Collect all relevant versions.
51+
// * Collect the distance to the base for each version.
52+
// * Collect the roots of relevant versions.
53+
// * Collect the children of each version.
54+
indexData(*fromVersions)
3255

33-
var result: List<Long> = emptyList()
56+
// The following algorithm orders the version by
57+
// 1. Finding out the roots of so-called subtrees.
58+
// A subtree is a tree of all versions that have the same version as root ancestor.
59+
// A root ancestor of a version is the first ancestor in the chain of ancestors
60+
// that is either a merge or a global root.
61+
// Each version belongs to exactly one root ancestor, and it will be the same (especially future merges).
62+
// 2. Sort the roots of subtrees according to their distance (primary) and id (secondary)
63+
// 3. Order topologically inside each subtree.
3464

35-
for (version in versions.values.filter { !it.isMerge() }.sortedBy { it.id }) {
36-
val descendantIds = collectAllDescendants(version.id).filter { !versions[it]!!.isMerge() }.sorted().toSet()
37-
val idsInResult = result.toHashSet()
38-
if (idsInResult.contains(version.id)) {
39-
result =
40-
result +
41-
descendantIds.filter { !idsInResult.contains(it) }
42-
} else {
43-
result =
44-
result.filter { !descendantIds.contains(it) } +
45-
version.id +
46-
result.filter { descendantIds.contains(it) } +
47-
descendantIds.filter { !idsInResult.contains(it) }
48-
}
49-
}
50-
return result.map { versions[it]!! }
51-
}
65+
// Ordering the subtree root first, ensures the order is also "monotonic".
66+
// Then ordering the inside subtree ensures "close versions are kept together" without breaking "monotonicity".
67+
// Ordering inside a subtree ensures "monotonicity", because a subtree has no merges.
68+
// Only a subtrees root can be a merge.
5269

53-
private fun collectAllDescendants(root: Long): Set<Long> {
54-
val result = LinkedHashSet<Long>()
55-
var previousSize = 0
56-
result += root
70+
// Sorting the subtree roots by distance from base ensures topological order.
71+
val comparator = compareBy(byVersionDistanceFromGlobalRoot::getValue)
72+
// Sorting the subtree roots by distance from base and then by id ensures "monotonic" order.
73+
.thenBy(CLVersion::id)
74+
val rootsOfSubtreesToVisit = globalRoot + byVersionDistanceFromGlobalRoot.keys.filter(CLVersion::isMerge)
75+
val orderedRootsOfSubtree = rootsOfSubtreesToVisit.distinct().sortedWith(comparator)
5776

58-
while (previousSize != result.size) {
59-
val nextElements = result.asSequence().drop(previousSize).toList()
60-
previousSize = result.size
61-
for (ancestor in nextElements) {
62-
version2directDescendants[ancestor]?.let { result += it }
77+
val history = orderedRootsOfSubtree.flatMap { rootOfSubtree ->
78+
val historyOfSubtree = mutableListOf<CLVersion>()
79+
val stack = ArrayDeque<CLVersion>()
80+
stack.add(rootOfSubtree)
81+
while (stack.isNotEmpty()) {
82+
val version = stack.removeLast()
83+
historyOfSubtree.add(version)
84+
val children = byVersionChildren.getOrElse(version, ::emptyList)
85+
val childrenWithoutMerges = children.filterNot(CLVersion::isMerge)
86+
// Order so that child with the lowest id is processed first
87+
// and comes first in the history.
88+
stack.addAll(childrenWithoutMerges.sortedByDescending(CLVersion::id))
6389
}
90+
historyOfSubtree
6491
}
65-
66-
return result.drop(1).toSet()
92+
return history.filterNot(CLVersion::isMerge)
6793
}
6894

69-
private fun collect(root: CLVersion) {
70-
if (root.getContentHash() == baseVersionHash) return
71-
72-
var previousSize = versions.size
73-
versions[root.id] = root
74-
75-
while (previousSize != versions.size) {
76-
val nextElements = versions.asSequence().drop(previousSize).map { it.value }.toList()
77-
previousSize = versions.size
78-
79-
for (descendant in nextElements) {
80-
val ancestors = if (descendant.isMerge()) {
81-
sequenceOf(
82-
getVersion(descendant.data!!.mergedVersion1!!, descendant.store),
83-
getVersion(descendant.data!!.mergedVersion2!!, descendant.store),
84-
)
95+
private fun indexData(vararg fromVersions: CLVersion): MutableMap<CLVersion, Int> {
96+
val stack = ArrayDeque<CLVersion>()
97+
fromVersions.forEach { fromVersion ->
98+
if (byVersionDistanceFromGlobalRoot.contains(fromVersion)) {
99+
return@forEach
100+
}
101+
stack.addLast(fromVersion)
102+
while (stack.isNotEmpty()) {
103+
val version = stack.last()
104+
val parents = version.getParents()
105+
// Version is the base version or the first version and therfore a root.
106+
if (parents.isEmpty()) {
107+
stack.removeLast()
108+
globalRoot.add(version)
109+
byVersionDistanceFromGlobalRoot[version] = 0
85110
} else {
86-
sequenceOf(descendant.baseVersion)
87-
}.filterNotNull().filter { it.getContentHash() != baseVersionHash }.toList()
88-
for (ancestor in ancestors) {
89-
versions[ancestor.id] = ancestor
90-
version2directDescendants[ancestor.id] = (version2directDescendants[ancestor.id] ?: emptySet()) + setOf(descendant.id)
111+
parents.forEach { parent ->
112+
byVersionChildren.getOrPut(parent, ::mutableSetOf).add(version)
113+
}
114+
val (visitedParents, notVisitedParents) = parents.partition(byVersionDistanceFromGlobalRoot::contains)
115+
// All children where already visited and have their distance known.
116+
if (notVisitedParents.isEmpty()) {
117+
stack.removeLast()
118+
val depth = visitedParents.maxOf { byVersionDistanceFromGlobalRoot[it]!! } + 1
119+
byVersionDistanceFromGlobalRoot[version] = depth
120+
// Children need to be visited
121+
} else {
122+
stack.addAll(notVisitedParents)
123+
}
91124
}
92125
}
93126
}
127+
return byVersionDistanceFromGlobalRoot
94128
}
95129

96-
private fun getVersion(hash: KVEntryReference<CPVersion>, store: IDeserializingKeyValueStore): CLVersion {
97-
return CLVersion(hash.getValue(store), store)
130+
private fun CLVersion.getParents(): List<CLVersion> {
131+
val ancestors = if (isMerge()) {
132+
listOf(getMergedVersion1()!!, getMergedVersion2()!!)
133+
} else {
134+
listOfNotNull(baseVersion)
135+
}
136+
return ancestors.filter { it.getContentHash() != baseVersionHash }
98137
}
99138
}

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/CLVersion.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ import org.modelix.model.IKeyListener
2323
import org.modelix.model.IKeyValueStore
2424
import org.modelix.model.IVersion
2525
import org.modelix.model.LinearHistory
26+
import org.modelix.model.api.IIdGenerator
2627
import org.modelix.model.api.INodeReference
28+
import org.modelix.model.api.IWriteTransaction
2729
import org.modelix.model.api.LocalPNodeReference
2830
import org.modelix.model.api.PNodeReference
31+
import org.modelix.model.api.TreePointer
2932
import org.modelix.model.operations.IOperation
33+
import org.modelix.model.operations.OTBranch
3034
import org.modelix.model.operations.SetReferenceOp
3135
import org.modelix.model.persistent.CPHamtNode
3236
import org.modelix.model.persistent.CPNode
@@ -432,3 +436,16 @@ private class AccessTrackingStore(val store: IKeyValueStore) : IKeyValueStore {
432436
TODO("Not yet implemented")
433437
}
434438
}
439+
440+
fun CLVersion.runWrite(idGenerator: IIdGenerator, author: String?, body: (IWriteTransaction) -> Unit): CLVersion {
441+
val branch = OTBranch(TreePointer(getTree(), idGenerator), idGenerator, store)
442+
branch.computeWriteT(body)
443+
val (ops, newTree) = branch.getPendingChanges()
444+
return CLVersion.createRegularVersion(
445+
id = idGenerator.generate(),
446+
author = author,
447+
tree = newTree as CLTree,
448+
baseVersion = this,
449+
operations = ops.map { it.getOriginalOp() }.toTypedArray(),
450+
)
451+
}

model-datastructure/src/commonMain/kotlin/org/modelix/model/operations/OTBranch.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class OTBranch(
3333
private var currentOperations: MutableList<IAppliedOperation> = ArrayList()
3434
private val completedChanges: MutableList<OpsAndTree> = ArrayList()
3535
private val id: String = branch.getId()
36+
private var inWriteTransaction = false
3637

3738
fun operationApplied(op: IAppliedOperation) {
3839
check(canWrite()) { "Only allowed inside a write transaction" }
@@ -84,18 +85,20 @@ class OTBranch(
8485

8586
override fun <T> computeWrite(computable: () -> T): T {
8687
checkNotEDT()
87-
return if (canWrite()) {
88-
// Already in a transaction. Just append changes to the active one.
89-
branch.computeWrite(computable)
90-
} else {
91-
branch.computeWriteT { t ->
88+
return branch.computeWriteT { t ->
89+
// canWrite() cannot be used as the condition, because that may statically return true (see TreePointer)
90+
if (inWriteTransaction) {
91+
computable()
92+
} else {
9293
try {
94+
inWriteTransaction = true
9395
val result = computable()
9496
runSynchronized(completedChanges) {
9597
completedChanges += OpsAndTree(currentOperations, t.tree)
9698
}
9799
result
98100
} finally {
101+
inWriteTransaction = false
99102
currentOperations = ArrayList()
100103
}
101104
}

model-datastructure/src/commonTest/kotlin/ConflictResolutionTest.kt

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -815,35 +815,35 @@ class ConflictResolutionTest : TreeTestBase() {
815815
operations = opsAndTree.first.map { it.getOriginalOp() }.toTypedArray(),
816816
)
817817
}
818+
}
818819

819-
fun assertSameTree(tree1: ITree, tree2: ITree) {
820-
tree2.visitChanges(
821-
tree1,
822-
object : ITreeChangeVisitorEx {
823-
override fun containmentChanged(nodeId: Long) {
824-
fail("containmentChanged ${nodeId.toString(16)}")
825-
}
820+
fun assertSameTree(tree1: ITree, tree2: ITree) {
821+
tree2.visitChanges(
822+
tree1,
823+
object : ITreeChangeVisitorEx {
824+
override fun containmentChanged(nodeId: Long) {
825+
fail("containmentChanged ${nodeId.toString(16)}")
826+
}
826827

827-
override fun childrenChanged(nodeId: Long, role: String?) {
828-
fail("childrenChanged ${nodeId.toString(16)}, $role")
829-
}
828+
override fun childrenChanged(nodeId: Long, role: String?) {
829+
fail("childrenChanged ${nodeId.toString(16)}, $role")
830+
}
830831

831-
override fun referenceChanged(nodeId: Long, role: String) {
832-
fail("referenceChanged ${nodeId.toString(16)}, $role")
833-
}
832+
override fun referenceChanged(nodeId: Long, role: String) {
833+
fail("referenceChanged ${nodeId.toString(16)}, $role")
834+
}
834835

835-
override fun propertyChanged(nodeId: Long, role: String) {
836-
fail("propertyChanged ${nodeId.toString(16)}, $role")
837-
}
836+
override fun propertyChanged(nodeId: Long, role: String) {
837+
fail("propertyChanged ${nodeId.toString(16)}, $role")
838+
}
838839

839-
override fun nodeRemoved(nodeId: Long) {
840-
fail("nodeRemoved ${nodeId.toString(16)}")
841-
}
840+
override fun nodeRemoved(nodeId: Long) {
841+
fail("nodeRemoved ${nodeId.toString(16)}")
842+
}
842843

843-
override fun nodeAdded(nodeId: Long) {
844-
fail("nodeAdded nodeId")
845-
}
846-
},
847-
)
848-
}
844+
override fun nodeAdded(nodeId: Long) {
845+
fail("nodeAdded nodeId")
846+
}
847+
},
848+
)
849849
}

0 commit comments

Comments
 (0)