Skip to content

Commit d11e068

Browse files
authored
Merge pull request #254 from modelix/MODELIX-514
MODELIX-514 Make changes from the MPS model sync plugin visible to the ModelClientV2
2 parents 7e47f57 + d68207f commit d11e068

File tree

13 files changed

+670
-198
lines changed

13 files changed

+670
-198
lines changed

model-client/src/commonMain/kotlin/org/modelix/model/client2/IModelClientV2.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,13 @@ interface IModelClientV2 {
5353

5454
suspend fun pull(branch: BranchReference, lastKnownVersion: IVersion?): IVersion
5555

56+
suspend fun pullHash(branch: BranchReference): String
57+
58+
/**
59+
* While `pull` returns immediately `poll` returns as soon as a new version, that is different from the given
60+
* `lastKnownVersion`, is pushed to the server or after some timeout specified by the server (usually ~30 seconds).
61+
*/
5662
suspend fun poll(branch: BranchReference, lastKnownVersion: IVersion?): IVersion
63+
64+
suspend fun pollHash(branch: BranchReference, lastKnownVersion: IVersion?): String
5765
}

model-client/src/commonMain/kotlin/org/modelix/model/client2/ModelClientV2.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,31 @@ class ModelClientV2(
165165
return receivedVersion
166166
}
167167

168+
override suspend fun pullHash(branch: BranchReference): String {
169+
val response = httpClient.get {
170+
url {
171+
takeFrom(baseUrl)
172+
appendPathSegmentsEncodingSlash("repositories", branch.repositoryId.id, "branches", branch.branchName, "hash")
173+
}
174+
}
175+
val receivedHash: String = response.body()
176+
return receivedHash
177+
}
178+
179+
override suspend fun pollHash(branch: BranchReference, lastKnownVersion: IVersion?): String {
180+
val response = httpClient.get {
181+
url {
182+
takeFrom(baseUrl)
183+
appendPathSegmentsEncodingSlash("repositories", branch.repositoryId.id, "branches", branch.branchName, "pollHash")
184+
if (lastKnownVersion != null) {
185+
parameters["lastKnown"] = lastKnownVersion.getContentHash()
186+
}
187+
}
188+
}
189+
val receivedHash: String = response.body()
190+
return receivedHash
191+
}
192+
168193
override suspend fun poll(branch: BranchReference, lastKnownVersion: IVersion?): IVersion {
169194
require(lastKnownVersion is CLVersion?)
170195
LOG.debug { "${clientId.toString(16)}.poll($branch, $lastKnownVersion)" }

model-client/src/jvmMain/kotlin/org/modelix/model/client/RestWebModelClient.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ class RestWebModelClient @JvmOverloads constructor(
152152
}
153153
private var clientIdInternal: Int = 0
154154
private val coroutineScope = CoroutineScope(Dispatchers.Default)
155+
private var watchdogJob: Job? = null
155156
private val client = (providedClient ?: HttpClient(CIO)).config {
156157
this.followRedirects = false
157158
install(HttpTimeout) {
@@ -264,7 +265,7 @@ class RestWebModelClient @JvmOverloads constructor(
264265
private var connectionStatusListeners: Set<ConnectionStatusListener> = emptySet()
265266

266267
private fun startConnectionWatchdog() {
267-
coroutineScope.launch {
268+
watchdogJob = coroutineScope.launch {
268269
while (isActive) {
269270
try {
270271
connectNow()
@@ -274,7 +275,7 @@ class RestWebModelClient @JvmOverloads constructor(
274275
delay(3.seconds)
275276
}
276277
} catch (e: CancellationException) {
277-
break
278+
throw e
278279
} catch (e: Exception) {
279280
LOG.debug("", e)
280281
if (connectionStatus == ConnectionStatus.CONNECTED) {
@@ -361,6 +362,7 @@ class RestWebModelClient @JvmOverloads constructor(
361362
listeners.clear()
362363
}
363364
coroutineScope.cancel("model client disposed")
365+
watchdogJob?.cancel("model client disposed")
364366
}
365367

366368
override fun getPendingSize(): Int = pendingWrites.get()

model-datastructure/src/commonMain/kotlin/org/modelix/model/LinearHistory.kt

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,35 @@ import org.modelix.model.lazy.IDeserializingKeyValueStore
55
import org.modelix.model.lazy.KVEntryReference
66
import org.modelix.model.persistent.CPVersion
77

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+
*/
816
class LinearHistory(val baseVersionHash: String?) {
917

10-
val version2descendants: MutableMap<Long, MutableSet<Long>> = HashMap()
11-
val versions: MutableMap<Long, CLVersion> = HashMap()
18+
val version2directDescendants: MutableMap<Long, Set<Long>> = HashMap()
19+
val versions: MutableMap<Long, CLVersion> = LinkedHashMap()
1220

1321
/**
14-
* Oldest version first
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
1527
*/
1628
fun load(vararg fromVersions: CLVersion): List<CLVersion> {
1729
for (fromVersion in fromVersions) {
18-
collect(fromVersion, emptyList())
30+
collect(fromVersion)
1931
}
2032

21-
var result: List<Long> = ArrayList()
33+
var result: List<Long> = emptyList()
2234

2335
for (version in versions.values.filter { !it.isMerge() }.sortedBy { it.id }) {
24-
val descendantIds = version2descendants[version.id]!!.sorted()
36+
val descendantIds = collectAllDescendants(version.id).filter { !versions[it]!!.isMerge() }.sorted().toSet()
2537
val idsInResult = result.toHashSet()
2638
if (idsInResult.contains(version.id)) {
2739
result =
@@ -38,21 +50,45 @@ class LinearHistory(val baseVersionHash: String?) {
3850
return result.map { versions[it]!! }
3951
}
4052

41-
private fun collect(version: CLVersion, path: List<CLVersion>) {
42-
if (version.hash == baseVersionHash) return
43-
44-
if (!versions.containsKey(version.id)) versions[version.id] = version
45-
version2descendants.getOrPut(version.id) { HashSet() }.addAll(path.asSequence().map { it.id })
46-
47-
if (version.isMerge()) {
48-
val version1 = getVersion(version.data!!.mergedVersion1!!, version.store)
49-
val version2 = getVersion(version.data!!.mergedVersion2!!, version.store)
50-
collect(version1, path)
51-
collect(version2, path)
52-
} else {
53-
val previous = version.baseVersion
54-
if (previous != null) {
55-
collect(previous, path + version)
53+
private fun collectAllDescendants(root: Long): Set<Long> {
54+
val result = LinkedHashSet<Long>()
55+
var previousSize = 0
56+
result += root
57+
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 }
63+
}
64+
}
65+
66+
return result.drop(1).toSet()
67+
}
68+
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+
)
85+
} 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)
91+
}
5692
}
5793
}
5894
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
* Copyright (c) 2023.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import org.modelix.model.LinearHistory
18+
import org.modelix.model.VersionMerger
19+
import org.modelix.model.lazy.CLTree
20+
import org.modelix.model.lazy.CLVersion
21+
import org.modelix.model.lazy.ObjectStoreCache
22+
import org.modelix.model.operations.IOperation
23+
import org.modelix.model.persistent.MapBaseStore
24+
import kotlin.test.Test
25+
import kotlin.test.assertEquals
26+
27+
class LinearHistoryTest {
28+
val initialTree = CLTree.builder(ObjectStoreCache(MapBaseStore())).repositoryId("LinearHistoryTest").build()
29+
30+
@Test
31+
fun noCommonHistory() {
32+
val v20 = version(20, null)
33+
val v21 = version(21, null)
34+
35+
assertHistory(v20, v21, listOf(v20, v21))
36+
}
37+
38+
@Test
39+
fun divergedByTwoCommits() {
40+
val v10 = version(10, null)
41+
val v20 = version(20, v10)
42+
val v21 = version(21, v10)
43+
44+
assertHistory(v20, v21, listOf(v20, v21))
45+
}
46+
47+
@Test
48+
fun knownPerformanceIssue() {
49+
// This test was dumped from actual case discovered during a profiling session.
50+
51+
val v30000003a = version(12884901946, null)
52+
val v1000004d1 = version(4294968529, v30000003a)
53+
val v1000004d3 = version(4294968531, v1000004d1)
54+
val v200000353 = version(8589935443, v1000004d3)
55+
val v1000004d5 = version(4294968533, v1000004d3)
56+
val v30000003c = merge(12884901948, v1000004d3, v200000353, v1000004d5)
57+
val v1000004d6 = merge(4294968534, v1000004d3, v1000004d5, v200000353)
58+
val v30000003d = merge(12884901949, v1000004d3, v30000003c, v1000004d6)
59+
val v30000003e = merge(12884901950, v1000004d3, v30000003d, v1000004d6)
60+
val v200000354 = merge(8589935444, v1000004d3, v200000353, v30000003d)
61+
val v30000003f = merge(12884901951, v1000004d3, v30000003e, v200000354)
62+
val v300000040 = merge(12884901952, v1000004d3, v30000003f, v200000354)
63+
val v200000356 = version(8589935446, v200000354)
64+
val v300000041 = merge(12884901953, v1000004d3, v300000040, v200000356)
65+
val v1000004d8 = version(4294968536, v1000004d6)
66+
val v300000042 = merge(12884901954, v1000004d3, v300000041, v1000004d8)
67+
val v1000004d9 = merge(4294968537, v1000004d3, v1000004d8, v300000041)
68+
val v300000043 = merge(12884901955, v1000004d3, v300000042, v1000004d9)
69+
val v300000044 = merge(12884901956, v1000004d3, v300000043, v1000004d9)
70+
val v200000357 = merge(8589935447, v1000004d3, v200000356, v300000041)
71+
val v300000045 = merge(12884901957, v1000004d3, v300000044, v200000357)
72+
val v300000046 = merge(12884901958, v1000004d3, v300000045, v200000357)
73+
val v1000004da = merge(4294968538, v1000004d3, v1000004d9, v300000046)
74+
val v300000047 = merge(12884901959, v1000004d3, v300000046, v1000004da)
75+
val v300000048 = merge(12884901960, v1000004d3, v300000047, v1000004da)
76+
val v200000359 = version(8589935449, v200000357)
77+
val v300000049 = merge(12884901961, v1000004d3, v300000048, v200000359)
78+
val v1000004dc = version(4294968540, v1000004da)
79+
val v30000004a = merge(12884901962, v1000004d3, v300000049, v1000004dc)
80+
val v20000035a = merge(8589935450, v1000004d3, v200000359, v300000046)
81+
val v30000004b = merge(12884901963, v1000004d3, v30000004a, v20000035a)
82+
val v30000004c = merge(12884901964, v1000004d3, v30000004b, v20000035a)
83+
val v1000004dd = merge(4294968541, v1000004d3, v1000004dc, v30000004c)
84+
val v30000004d = merge(12884901965, v1000004d3, v30000004c, v1000004dd)
85+
val v30000004e = merge(12884901966, v1000004d3, v30000004d, v1000004dd)
86+
val v20000035b = merge(8589935451, v1000004d3, v20000035a, v30000004c)
87+
val v30000004f = merge(12884901967, v1000004d3, v30000004e, v20000035b)
88+
val v300000050 = merge(12884901968, v1000004d3, v30000004f, v20000035b)
89+
val v1000004df = version(4294968543, v1000004dd)
90+
val v300000051 = merge(12884901969, v1000004d3, v300000050, v1000004df)
91+
val v20000035d = version(8589935453, v20000035b)
92+
val v300000052 = merge(12884901970, v1000004d3, v300000051, v20000035d)
93+
val v1000004e0 = merge(4294968544, v1000004d3, v1000004df, v300000051)
94+
val v300000053 = merge(12884901971, v1000004d3, v300000052, v1000004e0)
95+
val v300000054 = merge(12884901972, v1000004d3, v300000053, v1000004e0)
96+
val v20000035f = version(8589935455, v20000035d)
97+
val v300000055 = merge(12884901973, v1000004d3, v300000054, v20000035f)
98+
val v200000360 = merge(8589935456, v1000004d3, v20000035f, v300000052)
99+
val v300000056 = merge(12884901974, v1000004d3, v300000055, v200000360)
100+
val v300000057 = merge(12884901975, v1000004d3, v300000056, v200000360)
101+
val v1000004e2 = version(4294968546, v1000004e0)
102+
val v300000058 = merge(12884901976, v1000004d3, v300000057, v1000004e2)
103+
val v200000362 = version(8589935458, v200000360)
104+
val v300000059 = merge(12884901977, v1000004d3, v300000058, v200000362)
105+
val v1000004e3 = merge(4294968547, v1000004d3, v1000004e2, v300000058)
106+
val v30000005a = merge(12884901978, v1000004d3, v300000059, v1000004e3)
107+
val v30000005b = merge(12884901979, v1000004d3, v30000005a, v1000004e3)
108+
val v1000004e5 = version(4294968549, v1000004e3)
109+
val v30000005c = merge(12884901980, v1000004d3, v30000005b, v1000004e5)
110+
val v200000363 = merge(8589935459, v1000004d3, v200000362, v300000058)
111+
val v30000005d = merge(12884901981, v1000004d3, v30000005c, v200000363)
112+
val v30000005e = merge(12884901982, v1000004d3, v30000005d, v200000363)
113+
val v200000365 = version(8589935461, v200000363)
114+
val v30000005f = merge(12884901983, v1000004d3, v30000005e, v200000365)
115+
val v1000004e7 = version(4294968551, v1000004e5)
116+
val v300000060 = merge(12884901984, v1000004d3, v30000005f, v1000004e7)
117+
val v200000367 = version(8589935463, v200000365)
118+
val v300000061 = merge(12884901985, v1000004d3, v300000060, v200000367)
119+
val v1000004e9 = version(4294968553, v1000004e7)
120+
val v300000062 = merge(12884901986, v1000004d3, v300000061, v1000004e9)
121+
val v1000004ea = merge(4294968554, v1000004d3, v1000004e9, v300000060)
122+
val v300000063 = merge(12884901987, v1000004d3, v300000062, v1000004ea)
123+
val v300000064 = merge(12884901988, v1000004d3, v300000063, v1000004ea)
124+
val v200000369 = version(8589935465, v200000367)
125+
val v300000065 = merge(12884901989, v1000004d3, v300000064, v200000369)
126+
val v20000036a = merge(8589935466, v1000004d3, v200000369, v300000060)
127+
val v300000066 = merge(12884901990, v1000004d3, v300000065, v20000036a)
128+
val v1000004eb = merge(4294968555, v1000004d3, v1000004ea, v300000061)
129+
val v300000067 = merge(12884901991, v1000004d3, v300000066, v1000004eb)
130+
val v20000036c = version(8589935468, v20000036a)
131+
val v300000068 = merge(12884901992, v1000004d3, v300000067, v20000036c)
132+
val v300000069 = merge(12884901993, v1000004d3, v300000068, v20000036a)
133+
val v30000006b = merge(12884901995, v1000004d3, v300000069, v1000004eb)
134+
val v20000036d = merge(8589935469, v1000004d3, v20000036c, v300000063)
135+
val v30000006c = merge(12884901996, v1000004d3, v30000006b, v20000036d)
136+
val v30000006d = merge(12884901997, v1000004d3, v30000006c, v20000036d)
137+
val v1000004ec = merge(4294968556, v1000004d3, v1000004eb, v30000006c)
138+
val v30000006e = merge(12884901998, v1000004d3, v30000006d, v1000004ec)
139+
val v300000070 = merge(12884902000, v1000004d3, v30000006e, v1000004ec)
140+
val v20000036e = merge(8589935470, v1000004d3, v20000036d, v30000006e)
141+
val v300000071 = merge(12884902001, v1000004d3, v300000070, v20000036e)
142+
val v1000004ed = merge(4294968557, v1000004d3, v1000004ec, v30000006e)
143+
val v300000072 = merge(12884902002, v1000004d3, v300000071, v1000004ed)
144+
val v20000036f = merge(8589935471, v1000004d3, v20000036e, v30000006e)
145+
val v300000073 = merge(12884902003, v1000004d3, v300000072, v20000036f)
146+
val v300000074 = merge(12884902004, v1000004d3, v300000073, v20000036f)
147+
val v300000075 = merge(12884902005, v1000004d3, v300000074, v20000036e)
148+
val v1000004ee = merge(4294968558, v1000004d3, v30000006e, v300000075)
149+
150+
// val expected = SlowLinearHistory(v1000004d3.getContentHash()).load(v300000075, v1000004ee)
151+
val expected = listOf(
152+
v1000004d5,
153+
v200000353,
154+
v1000004d8,
155+
v1000004dc,
156+
v1000004df,
157+
v1000004e2,
158+
v1000004e5,
159+
v1000004e7,
160+
v1000004e9,
161+
v200000356,
162+
v200000359,
163+
v20000035d,
164+
v20000035f,
165+
v200000362,
166+
v200000365,
167+
v200000367,
168+
v200000369,
169+
v20000036c,
170+
)
171+
assertHistory(v300000075, v1000004ee, expected)
172+
}
173+
174+
private fun assertHistory(v1: CLVersion, v2: CLVersion, expected: List<CLVersion>) {
175+
val actual = history(v1, v2)
176+
assertEquals(expected.map { it.id.toString(16) }, actual.map { it.id.toString(16) })
177+
assertEquals(expected, actual)
178+
}
179+
180+
private fun history(v1: CLVersion, v2: CLVersion): List<CLVersion> {
181+
val base = VersionMerger.commonBaseVersion(v1, v2)
182+
return LinearHistory(base?.getContentHash()).load(v1, v2)
183+
}
184+
185+
private fun version(id: Long, base: CLVersion?): CLVersion {
186+
return CLVersion.createRegularVersion(
187+
id,
188+
null,
189+
null,
190+
initialTree,
191+
base,
192+
emptyArray(),
193+
)
194+
}
195+
196+
private fun merge(id: Long, v1: CLVersion, v2: CLVersion): CLVersion {
197+
return merge(id, VersionMerger.Companion.commonBaseVersion(v1, v2)!!, v1, v2)
198+
}
199+
200+
private fun merge(id: Long, base: CLVersion, v1: CLVersion, v2: CLVersion): CLVersion {
201+
return CLVersion.createAutoMerge(
202+
id,
203+
initialTree,
204+
base,
205+
v1,
206+
v2,
207+
emptyArray<IOperation>(),
208+
initialTree.store,
209+
)
210+
}
211+
}

0 commit comments

Comments
 (0)