@@ -27,13 +27,17 @@ import io.ktor.server.resources.get
27
27
import io.ktor.server.resources.post
28
28
import io.ktor.server.resources.put
29
29
import io.ktor.server.response.respond
30
+ import io.ktor.server.response.respondBytesWriter
30
31
import io.ktor.server.response.respondText
31
- import io.ktor.server.response.respondTextWriter
32
32
import io.ktor.server.routing.Route
33
33
import io.ktor.server.routing.route
34
34
import io.ktor.server.routing.routing
35
35
import io.ktor.server.websocket.webSocket
36
+ import io.ktor.util.cio.use
36
37
import io.ktor.util.pipeline.PipelineContext
38
+ import io.ktor.utils.io.ByteWriteChannel
39
+ import io.ktor.utils.io.close
40
+ import io.ktor.utils.io.writeStringUtf8
37
41
import io.ktor.websocket.send
38
42
import kotlinx.coroutines.Dispatchers
39
43
import kotlinx.coroutines.Job
@@ -46,6 +50,7 @@ import kotlinx.serialization.encodeToString
46
50
import kotlinx.serialization.json.Json
47
51
import org.modelix.api.public.Paths
48
52
import org.modelix.authorization.getUserName
53
+ import org.modelix.model.InMemoryModels
49
54
import org.modelix.model.api.ITree
50
55
import org.modelix.model.api.PBranch
51
56
import org.modelix.model.api.TreePointer
@@ -69,15 +74,21 @@ import org.slf4j.LoggerFactory
69
74
* Implements the endpoints used by the 'model-client', but compared to KeyValueLikeModelServer also understands what
70
75
* client sends. This allows more validations and more responsibilities on the server side.
71
76
*/
72
- class ModelReplicationServer (val repositoriesManager : RepositoriesManager ) {
73
- constructor (modelClient: LocalModelClient ) : this (RepositoriesManager (modelClient))
77
+ class ModelReplicationServer (
78
+ private val repositoriesManager : IRepositoriesManager ,
79
+ private val modelClient : LocalModelClient ,
80
+ private val inMemoryModels : InMemoryModels ,
81
+ ) {
82
+ constructor (repositoriesManager: RepositoriesManager ) :
83
+ this (repositoriesManager, repositoriesManager.client, InMemoryModels ())
84
+
85
+ constructor (modelClient: LocalModelClient ) : this (RepositoriesManager (modelClient), modelClient, InMemoryModels ())
74
86
constructor (storeClient: IStoreClient ) : this (LocalModelClient (storeClient))
75
87
76
88
companion object {
77
89
private val LOG = LoggerFactory .getLogger(ModelReplicationServer ::class .java)
78
90
}
79
91
80
- private val modelClient: LocalModelClient get() = repositoriesManager.client
81
92
private val storeClient: IStoreClient get() = modelClient.store
82
93
83
94
fun init (application : Application ) {
@@ -268,16 +279,16 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
268
279
LOG .trace(" Running query on {} @ {}" , branchRef, version)
269
280
val initialTree = version!! .getTree()
270
281
val branch = OTBranch (
271
- PBranch (initialTree, repositoriesManager.client .idGenerator),
272
- repositoriesManager.client .idGenerator,
273
- repositoriesManager.client .storeCache,
282
+ PBranch (initialTree, modelClient .idGenerator),
283
+ modelClient .idGenerator,
284
+ modelClient .storeCache,
274
285
)
275
286
276
287
ModelQLServer .handleCall(call, { writeAccess ->
277
288
if (writeAccess) {
278
289
branch.getRootNode() to branch.getArea()
279
290
} else {
280
- val model = repositoriesManager. inMemoryModels.getModel(initialTree).await()
291
+ val model = inMemoryModels.getModel(initialTree).await()
281
292
model.getNode(ITree .ROOT_ID ) to model.getArea()
282
293
}
283
294
}, {
@@ -286,7 +297,7 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
286
297
val (ops, newTree) = branch.getPendingChanges()
287
298
if (newTree != initialTree) {
288
299
val newVersion = CLVersion .createRegularVersion(
289
- id = repositoriesManager.client .idGenerator.generate(),
300
+ id = modelClient .idGenerator.generate(),
290
301
author = getUserName(),
291
302
tree = newTree as CLTree ,
292
303
baseVersion = version,
@@ -299,7 +310,7 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
299
310
300
311
post<Paths .postRepositoryVersionHashQuery> {
301
312
val versionHash = call.parameters[" versionHash" ]!!
302
- val version = CLVersion .loadFromHash(versionHash, repositoriesManager.client .storeCache)
313
+ val version = CLVersion .loadFromHash(versionHash, modelClient .storeCache)
303
314
val initialTree = version.getTree()
304
315
val branch = TreePointer (initialTree)
305
316
ModelQLServer .handleCall(call, branch.getRootNode(), branch.getArea())
@@ -372,21 +383,47 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
372
383
respond(delta)
373
384
}
374
385
375
- private suspend fun ApplicationCall.respondDeltaAsObjectStream (versionHash : String , baseVersionHash : String? , plainText : Boolean ) {
376
- respondTextWriter(contentType = if (plainText) ContentType .Text .Plain else VersionDeltaStream .CONTENT_TYPE ) {
377
- repositoriesManager.computeDelta(versionHash, baseVersionHash).asFlow()
378
- .flatten()
379
- .withSeparator(" \n " )
380
- .onEmpty { emit(versionHash) }
381
- .withIndex()
382
- .collect {
383
- if (it.index == 0 ) check(it.value == versionHash) { " First object should be the version" }
384
- append(it.value)
385
- }
386
+ private suspend fun ApplicationCall.respondDeltaAsObjectStream (
387
+ versionHash : String ,
388
+ baseVersionHash : String? ,
389
+ plainText : Boolean ,
390
+ ) {
391
+ // Call `computeDelta` before starting to respond.
392
+ // It could already throw an exception, and in that case we do not want a successful response status.
393
+ val objectData = repositoriesManager.computeDelta(versionHash, baseVersionHash)
394
+ val contentType = if (plainText) ContentType .Text .Plain else VersionDeltaStream .CONTENT_TYPE
395
+ respondBytesWriter(contentType) {
396
+ this .useClosingWithoutCause {
397
+ objectData.asFlow()
398
+ .flatten()
399
+ .withSeparator(" \n " )
400
+ .onEmpty { emit(versionHash) }
401
+ .withIndex()
402
+ .collect {
403
+ if (it.index == 0 ) check(it.value == versionHash) { " First object should be the version" }
404
+ writeStringUtf8(it.value)
405
+ }
406
+ }
386
407
}
387
408
}
388
409
}
389
410
411
+ /* *
412
+ * Same as [[ByteWriteChannel.use]] but closing without a cause in case of an exception.
413
+ *
414
+ * Calling [[ByteWriteChannel.close]] with a cause results in not closing the connection properly.
415
+ * See ModelReplicationServerTest.`server closes connection when failing to compute delta after starting to respond`
416
+ * This will only be fixed in Ktor 3.
417
+ * See https://youtrack.jetbrains.com/issue/KTOR-4862/Ktor-hangs-if-exception-occurs-during-write-response-body
418
+ */
419
+ private inline fun ByteWriteChannel.useClosingWithoutCause (block : ByteWriteChannel .() -> Unit ) {
420
+ try {
421
+ block()
422
+ } finally {
423
+ close()
424
+ }
425
+ }
426
+
390
427
private fun <T > Flow <Pair <T , T >>.flatten () = flow<T > {
391
428
collect {
392
429
emit(it.first)
0 commit comments