Skip to content

Commit 17cc738

Browse files
authored
Merge pull request #213 from modelix/MODELIX-515
MODELIX-515 ReplicatedRepository failed to sync with the server
2 parents 17d2590 + e5798b7 commit 17cc738

File tree

96 files changed

+577
-371
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+577
-371
lines changed

commitlint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
"model-api-gen-gradle",
1414
"model-api",
1515
"model-client",
16+
"model-datastructure",
1617
"model-server",
1718
"model-sync-lib",
1819
"modelql",

kotlin-js-store/yarn.lock

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22
# yarn lockfile v1
33

44

5+
"@aws-crypto/sha256-js@^5.0.0":
6+
version "5.0.0"
7+
resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-5.0.0.tgz#fec6d5a9a097e812207eacaaa707bfa9191b3ad8"
8+
integrity sha512-g+u9iKkaQVp9Mjoxq1IJSHj9NHGZF441+R/GIH0dn7u4mix5QQ4VqgpppHrNm1LzjUzb0BpcFGsBXP6cOVf+ZQ==
9+
dependencies:
10+
"@aws-crypto/util" "^5.0.0"
11+
"@aws-sdk/types" "^3.222.0"
12+
tslib "^1.11.1"
13+
14+
"@aws-crypto/util@^5.0.0":
15+
version "5.0.0"
16+
resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.0.0.tgz#afa286af897ea2bd9fab194b4a6be9cc562db23a"
17+
integrity sha512-1GYqLdYRe96idcCltlqxdJ68OWE6ADT8qGLmVi7PVHKl8AxD2EWSbJSSevPq2eTx6vaPZpkr1RoZ3lcw/uGoEA==
18+
dependencies:
19+
"@aws-sdk/types" "^3.222.0"
20+
"@aws-sdk/util-utf8-browser" "^3.0.0"
21+
tslib "^1.11.1"
22+
23+
"@aws-sdk/types@^3.222.0":
24+
version "3.398.0"
25+
resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.398.0.tgz#8ce02559536670f9188cddfce32e9dd12b4fe965"
26+
integrity sha512-r44fkS+vsEgKCuEuTV+TIk0t0m5ZlXHNjSDYEUvzLStbbfUFiNus/YG4UCa0wOk9R7VuQI67badsvvPeVPCGDQ==
27+
dependencies:
28+
"@smithy/types" "^2.2.2"
29+
tslib "^2.5.0"
30+
31+
"@aws-sdk/util-utf8-browser@^3.0.0":
32+
version "3.259.0"
33+
resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff"
34+
integrity sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==
35+
dependencies:
36+
tslib "^2.3.1"
37+
538
639
version "1.5.0"
740
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -78,6 +111,13 @@
78111
"@modelix/ts-model-api@file:../../ts-model-api":
79112
version "1.3.2-kernelf.4.dirty-SNAPSHOT"
80113

114+
"@smithy/types@^2.2.2":
115+
version "2.2.2"
116+
resolved "https://registry.yarnpkg.com/@smithy/types/-/types-2.2.2.tgz#bd8691eb92dd07ac33b83e0e1c45f283502b1bf7"
117+
integrity sha512-4PS0y1VxDnELGHGgBWlDksB2LJK8TG8lcvlWxIsgR+8vROI7Ms8h1P4FQUx+ftAX2QZv5g1CJCdhdRmQKyonyw==
118+
dependencies:
119+
tslib "^2.5.0"
120+
81121
"@socket.io/component-emitter@~3.1.0":
82122
version "3.1.0"
83123
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
@@ -2045,6 +2085,16 @@ tr46@~0.0.3:
20452085
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
20462086
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
20472087

2088+
tslib@^1.11.1:
2089+
version "1.14.1"
2090+
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
2091+
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
2092+
2093+
tslib@^2.3.1, tslib@^2.5.0:
2094+
version "2.6.2"
2095+
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
2096+
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
2097+
20482098
type-check@~0.3.2:
20492099
version "0.3.2"
20502100
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"

model-client/build.gradle.kts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ val kotlinxSerializationVersion: String by rootProject
2020
kotlin {
2121
jvm()
2222
js(IR) {
23-
// browser {}
23+
browser {
24+
testTask {
25+
useMocha {
26+
timeout = "30s"
27+
}
28+
}
29+
}
2430
nodejs {
2531
testTask {
2632
useMocha {
@@ -34,6 +40,7 @@ kotlin {
3440
val commonMain by getting {
3541
dependencies {
3642
api(project(":model-api"))
43+
api(project(":model-datastructure"))
3744
api(project(":model-server-api"))
3845
kotlin("stdlib-common")
3946
implementation(libs.kotlin.collections.immutable)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ interface IModelClientV2 {
3434
/**
3535
* The pushed version is merged automatically by the server with the current head.
3636
* The merge result is returned.
37+
* @param baseVersion Some version that is known to exist on the server.
38+
* Is used for optimizing the amount of data sent to the server.
3739
*/
38-
suspend fun push(branch: BranchReference, version: IVersion): IVersion
40+
suspend fun push(branch: BranchReference, version: IVersion, baseVersion: IVersion?): IVersion
3941

4042
suspend fun pull(branch: BranchReference, lastKnownVersion: IVersion?): IVersion
4143
suspend fun pull(branch: BranchReference, lastKnownVersion: IVersion?, filter: ModelQuery): IVersion

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

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ import org.modelix.model.lazy.BranchReference
3939
import org.modelix.model.lazy.CLVersion
4040
import org.modelix.model.lazy.ObjectStoreCache
4141
import org.modelix.model.lazy.RepositoryId
42+
import org.modelix.model.lazy.computeDelta
4243
import org.modelix.model.persistent.HashUtil
4344
import org.modelix.model.persistent.MapBaseStore
4445
import org.modelix.model.server.api.ModelQuery
4546
import org.modelix.model.server.api.v2.VersionDelta
46-
import kotlin.jvm.Synchronized
4747
import kotlin.time.Duration.Companion.seconds
4848

4949
class ModelClientV2(
@@ -53,7 +53,7 @@ class ModelClientV2(
5353
private var clientId: Int = 0
5454
private var idGenerator: IIdGenerator = IdGeneratorDummy()
5555
private var userId: String? = null
56-
private val kvStore = UncommitedEntriesStore()
56+
private val kvStore = MapBaseStore()
5757
val store = ObjectStoreCache(kvStore) // TODO the store will accumulate garbage
5858

5959
suspend fun init() {
@@ -129,17 +129,20 @@ class ModelClientV2(
129129
return createVersion(null, delta)
130130
}
131131

132-
override suspend fun push(branch: BranchReference, version: IVersion): IVersion {
132+
override suspend fun push(branch: BranchReference, version: IVersion, baseVersion: IVersion?): IVersion {
133+
LOG.debug { "${clientId.toString(16)}.push($branch, $version, $baseVersion)" }
133134
require(version is CLVersion)
135+
require(baseVersion is CLVersion?)
134136
version.write()
135-
val objects = kvStore.getUncommitedEntries().values.filterNotNull().toSet()
137+
val objects = version.computeDelta(baseVersion)
136138
val response = httpClient.post {
137139
url {
138140
takeFrom(baseUrl)
139141
appendPathSegments("repositories", branch.repositoryId.id, "branches", branch.branchName)
140142
}
141143
contentType(ContentType.Application.Json)
142-
val body = VersionDelta(version.hash, null, objects)
144+
val body = VersionDelta(version.getContentHash(), null, objectsMap = objects)
145+
body.checkObjectHashes()
143146
setBody(body)
144147
}
145148
val mergedVersionDelta = response.body<VersionDelta>()
@@ -157,7 +160,9 @@ class ModelClientV2(
157160
}
158161
}
159162
}
160-
return createVersion(lastKnownVersion, response.body())
163+
val receivedVersion = createVersion(lastKnownVersion, response.body())
164+
LOG.debug { "${clientId.toString(16)}.pull($branch, $lastKnownVersion) -> $receivedVersion" }
165+
return receivedVersion
161166
}
162167

163168
override suspend fun poll(branch: BranchReference, lastKnownVersion: IVersion?): IVersion {
@@ -171,7 +176,9 @@ class ModelClientV2(
171176
}
172177
}
173178
}
174-
return createVersion(lastKnownVersion, response.body())
179+
val receivedVersion = createVersion(lastKnownVersion, response.body())
180+
LOG.debug { "${clientId.toString(16)}.poll($branch, $lastKnownVersion) -> $receivedVersion" }
181+
return receivedVersion
175182
}
176183

177184
override suspend fun pull(branch: BranchReference, lastKnownVersion: IVersion?, filter: ModelQuery): IVersion {
@@ -186,13 +193,13 @@ class ModelClientV2(
186193
return if (baseVersion == null) {
187194
CLVersion(
188195
delta.versionHash,
189-
store.also { it.keyValueStore.putAll(delta.objects.associateBy { HashUtil.sha256(it) }) },
196+
store.also { it.keyValueStore.putAll(delta.getAllObjects()) },
190197
)
191198
} else if (delta.versionHash == baseVersion.hash) {
192199
baseVersion
193200
} else {
194201
require(baseVersion.store == store) { "baseVersion was not created by this client" }
195-
store.keyValueStore.putAll(delta.objects.associateBy { HashUtil.sha256(it) })
202+
store.keyValueStore.putAll(delta.getAllObjects())
196203
CLVersion(
197204
delta.versionHash,
198205
baseVersion.store,
@@ -201,6 +208,7 @@ class ModelClientV2(
201208
}
202209

203210
companion object {
211+
private val LOG = mu.KotlinLogging.logger {}
204212
fun builder(): ModelClientV2Builder = ModelClientV2PlatformSpecificBuilder()
205213
}
206214
}
@@ -269,19 +277,8 @@ abstract class ModelClientV2Builder {
269277

270278
expect class ModelClientV2PlatformSpecificBuilder() : ModelClientV2Builder
271279

272-
private class UncommitedEntriesStore() : MapBaseStore() {
273-
private var uncommitedEntries: MutableMap<String, String?> = HashMap()
274-
275-
@Synchronized
276-
fun getUncommitedEntries(): Map<String, String?> {
277-
val result = uncommitedEntries
278-
uncommitedEntries = HashMap()
279-
return result
280-
}
281-
282-
@Synchronized
283-
override fun putAll(entries: Map<String, String?>) {
284-
uncommitedEntries.putAll(entries)
285-
super.putAll(entries)
286-
}
280+
fun VersionDelta.checkObjectHashes() {
281+
HashUtil.checkObjectHashes(objectsMap)
287282
}
283+
284+
fun VersionDelta.getAllObjects(): Map<String, String> = objectsMap + objects.associateBy { HashUtil.sha256(it) }

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.modelix.model.client2
33
import kotlinx.coroutines.CoroutineScope
44
import kotlinx.coroutines.Dispatchers
55
import kotlinx.coroutines.cancel
6+
import kotlinx.coroutines.delay
67
import kotlinx.coroutines.launch
78
import kotlinx.coroutines.sync.Mutex
89
import kotlinx.coroutines.sync.withLock
@@ -50,13 +51,21 @@ class ReplicatedModel(val client: IModelClientV2, val branchRef: BranchReference
5051
otBranch = OTBranch(rawBranch, client.getIdGenerator(), lastRemoteVersion.store)
5152

5253
scope.launch {
54+
var nextDelayMs: Long = 0
5355
while (state != State.Disposed) {
54-
val newRemoteVersion = if (query == null) {
55-
client.poll(branchRef, lastRemoteVersion)
56-
} else {
57-
client.poll(branchRef, lastRemoteVersion, query)
58-
} as CLVersion
59-
remoteVersionReceived(newRemoteVersion)
56+
if (nextDelayMs > 0) delay(nextDelayMs)
57+
try {
58+
val newRemoteVersion = if (query == null) {
59+
client.poll(branchRef, lastRemoteVersion)
60+
} else {
61+
client.poll(branchRef, lastRemoteVersion, query)
62+
} as CLVersion
63+
remoteVersionReceived(newRemoteVersion)
64+
nextDelayMs = 0
65+
} catch (ex: Throwable) {
66+
LOG.error(ex) { "Failed to poll branch $branchRef" }
67+
nextDelayMs = (nextDelayMs * 3 / 2).coerceIn(1000, 30000)
68+
}
6069
}
6170
}
6271

@@ -111,7 +120,7 @@ class ReplicatedModel(val client: IModelClientV2, val branchRef: BranchReference
111120
createdVersion = applyPendingLocalChanges()
112121
}
113122
if (createdVersion != null) {
114-
remoteVersionReceived(client.push(branchRef, createdVersion) as CLVersion)
123+
remoteVersionReceived(client.push(branchRef, createdVersion, baseVersion = lastRemoteVersion) as CLVersion)
115124
}
116125
}
117126

@@ -147,6 +156,11 @@ class ReplicatedModel(val client: IModelClientV2, val branchRef: BranchReference
147156
}
148157
}
149158

150-
fun IModelClientV2.getReplicatedModel(branchRef: BranchReference, query: ModelQuery? = null): ReplicatedModel {
159+
fun IModelClientV2.getReplicatedModel(branchRef: BranchReference): ReplicatedModel {
160+
return ReplicatedModel(this, branchRef)
161+
}
162+
163+
@Deprecated("ModelQuery is not supported and ignored", ReplaceWith("getReplicatedModel(branchRef)"))
164+
fun IModelClientV2.getReplicatedModel(branchRef: BranchReference, query: ModelQuery?): ReplicatedModel {
151165
return ReplicatedModel(this, branchRef, query)
152166
}

model-client/src/commonMain/kotlin/org/modelix/model/persistent/PlatformSpecificHashUtil.kt

Lines changed: 0 additions & 11 deletions
This file was deleted.

model-client/src/commonTest/kotlin/org/modelix/model/HashUtilsTest.kt

Lines changed: 0 additions & 65 deletions
This file was deleted.

model-client/src/commonTest/kotlin/org/modelix/model/SerializationUtilEscapeTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ class SerializationUtilEscapeTest {
2222
assertEquals("", SerializationUtil.escape(""))
2323
}
2424

25+
@Test
26+
fun escape_space() {
27+
assertEquals("+", SerializationUtil.escape(" "))
28+
}
29+
30+
@Test
31+
fun unescape_space() {
32+
assertEquals(" ", SerializationUtil.unescape("+"))
33+
}
34+
2535
@Test
2636
fun unescape_emptyString() {
2737
assertEquals("", SerializationUtil.unescape(""))

0 commit comments

Comments
 (0)