Skip to content

Commit 2d65b8c

Browse files
committed
feat: index for efficient history queries
1 parent 2f95bcd commit 2d65b8c

File tree

8 files changed

+951
-2
lines changed

8 files changed

+951
-2
lines changed

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.modelix.model.client2
22

33
import io.ktor.http.Url
4+
import org.modelix.datastructures.objects.ObjectHash
45
import org.modelix.kotlin.utils.DeprecationInfo
56
import org.modelix.model.IVersion
67
import org.modelix.model.ObjectDeltaFilter
@@ -11,6 +12,7 @@ import org.modelix.model.lazy.BranchReference
1112
import org.modelix.model.lazy.RepositoryId
1213
import org.modelix.model.server.api.RepositoryConfig
1314
import org.modelix.modelql.core.IMonoStep
15+
import kotlin.time.Duration
1416

1517
/**
1618
* This interface is meant exclusively for model client usage.
@@ -105,4 +107,41 @@ interface IModelClientV2 {
105107
suspend fun <R> query(repositoryId: RepositoryId, versionHash: String, body: (IMonoStep<INode>) -> IMonoStep<R>): R
106108

107109
fun getFrontendUrl(branch: BranchReference): Url
110+
111+
/**
112+
* @param headVersion starting point for history computations. For a paginated view this value should be the same and
113+
* the value for [skip] should be incremented instead. Only then its guaranteed that the returned list is
114+
* complete.
115+
* @param skip for a paginated view of the history
116+
* @param limit maximum size of the returned list
117+
* @param interval splits the timeline into equally sized intervals and returns only the last version of each interval
118+
*/
119+
suspend fun getHistory(
120+
repositoryId: RepositoryId,
121+
headVersion: ObjectHash,
122+
skip: Int = 0,
123+
limit: Int = 1000,
124+
interval: Duration?,
125+
): HistoryResponse
126+
127+
suspend fun getHistoryRange(
128+
repositoryId: RepositoryId,
129+
headVersion: ObjectHash,
130+
skip: Long = 0L,
131+
limit: Long = 1000L,
132+
): List<IVersion>
108133
}
134+
135+
data class HistoryResponse(
136+
val entries: List<HistoryEntry>,
137+
val nextVersions: List<ObjectHash>,
138+
)
139+
140+
data class HistoryEntry(
141+
val firstVersionHash: ObjectHash,
142+
val lastVersionHash: ObjectHash,
143+
val minTime: Long?,
144+
val maxTime: Long?,
145+
val authors: Set<String>,
146+
// val headOfBranch: Set<BranchReference>,
147+
)

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,12 @@ class ModelClientGraph(
128128
hash: ObjectHash,
129129
data: T,
130130
): ObjectReference<T> {
131-
// Should never be called, because all deserialization happens internally.
132-
throw UnsupportedOperationException()
131+
if (config.lazyLoadingEnabled) {
132+
return ObjectReferenceImpl(this, hash, data)
133+
} else {
134+
// Should never be called, because all deserialization happens internally.
135+
throw UnsupportedOperationException()
136+
}
133137
}
134138

135139
@Synchronized

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ import kotlinx.coroutines.flow.flow
3737
import kotlinx.coroutines.flow.map
3838
import kotlinx.coroutines.launch
3939
import mu.KotlinLogging
40+
import org.modelix.datastructures.model.HistoryIndexNode
4041
import org.modelix.datastructures.objects.IObjectGraph
42+
import org.modelix.datastructures.objects.Object
4143
import org.modelix.datastructures.objects.ObjectHash
4244
import org.modelix.kotlin.utils.DeprecationInfo
4345
import org.modelix.kotlin.utils.WeakValueMap
@@ -89,6 +91,8 @@ import org.modelix.model.server.api.v2.toMap
8991
import org.modelix.modelql.client.ModelQLClient
9092
import org.modelix.modelql.core.IMonoStep
9193
import org.modelix.streams.IExecutableStream
94+
import org.modelix.streams.getSuspending
95+
import org.modelix.streams.iterateSuspending
9296
import kotlin.time.Duration
9397
import kotlin.time.Duration.Companion.seconds
9498

@@ -328,6 +332,106 @@ class ModelClientV2(
328332
}
329333
}
330334

335+
override suspend fun getHistory(
336+
repositoryId: RepositoryId,
337+
headVersion: ObjectHash,
338+
skip: Int,
339+
limit: Int,
340+
interval: Duration?,
341+
): HistoryResponse {
342+
val index: Object<HistoryIndexNode> = getHistoryIndex(repositoryId, headVersion)
343+
val entries = if (interval != null) {
344+
val mergedEntries = ArrayList<HistoryEntry>()
345+
var previousIntervalId: Long = Long.MAX_VALUE
346+
try {
347+
index.data.splitAtInterval(interval).iterateSuspending(index.graph) {
348+
val intervalId = it.maxTime.toEpochMilliseconds() / interval.inWholeMilliseconds
349+
require(intervalId <= previousIntervalId)
350+
if (intervalId == previousIntervalId) {
351+
val entry = mergedEntries[mergedEntries.lastIndex]
352+
mergedEntries[mergedEntries.lastIndex] = HistoryEntry(
353+
firstVersionHash = it.firstVersion.getHash(),
354+
lastVersionHash = entry.lastVersionHash,
355+
minTime = it.minTime.epochSeconds,
356+
maxTime = entry.maxTime,
357+
authors = entry.authors + it.authors,
358+
)
359+
} else {
360+
if (mergedEntries.size >= limit) throw LimitedReached()
361+
previousIntervalId = intervalId
362+
mergedEntries += HistoryEntry(
363+
firstVersionHash = it.firstVersion.getHash(),
364+
lastVersionHash = it.lastVersion.getHash(),
365+
minTime = it.minTime.epochSeconds,
366+
maxTime = it.maxTime.epochSeconds,
367+
authors = it.authors,
368+
)
369+
}
370+
}
371+
} catch (ex: LimitedReached) {
372+
// Expected exception used for exiting the iterateSuspending call
373+
}
374+
mergedEntries
375+
} else {
376+
index.data.getAllVersionsReversed().flatMap { it.resolve() }.map { CLVersion(it) }
377+
.map {
378+
val hash = it.getObjectHash()
379+
val time = it.getTimestamp()?.epochSeconds
380+
HistoryEntry(
381+
firstVersionHash = hash,
382+
lastVersionHash = hash,
383+
minTime = time,
384+
maxTime = time,
385+
authors = setOfNotNull(it.author),
386+
)
387+
}
388+
.take(limit)
389+
.toList()
390+
.getSuspending(index.graph)
391+
}
392+
return HistoryResponse(entries = entries, nextVersions = emptyList())
393+
}
394+
395+
override suspend fun getHistoryRange(
396+
repositoryId: RepositoryId,
397+
headVersion: ObjectHash,
398+
skip: Long,
399+
limit: Long,
400+
): List<IVersion> {
401+
val index: Object<HistoryIndexNode> = getHistoryIndex(repositoryId, headVersion)
402+
return index.data.getRange(skip until (limit + skip))
403+
.flatMap { it.getAllVersionsReversed() }
404+
.flatMap { it.resolve() }
405+
.map { CLVersion(it) }
406+
.toList()
407+
.getSuspending(index.graph)
408+
}
409+
410+
suspend fun getHistoryIndex(
411+
repositoryId: RepositoryId?,
412+
versionHash: ObjectHash,
413+
): Object<HistoryIndexNode> {
414+
return httpClient.prepareGet {
415+
url {
416+
takeFrom(baseUrl)
417+
if (repositoryId == null) {
418+
appendPathSegments("versions", versionHash.toString())
419+
} else {
420+
appendPathSegments("repositories", repositoryId.id, "versions", versionHash.toString())
421+
}
422+
appendPathSegments("history-index")
423+
}
424+
}.execute { response ->
425+
val graph = getObjectGraph(repositoryId).also { it.config = it.config.copy(lazyLoadingEnabled = true) }
426+
val text = response.bodyAsText()
427+
val hashString = text.substringBefore('\n')
428+
val serialized = text.substringAfter('\n')
429+
val deserialized = HistoryIndexNode.deserialize(serialized, graph)
430+
val ref = graph.fromDeserialized(ObjectHash(hashString), deserialized)
431+
Object(deserialized, ref)
432+
}
433+
}
434+
331435
override suspend fun loadVersion(
332436
repositoryId: RepositoryId,
333437
versionHash: String,
@@ -968,3 +1072,5 @@ fun IVersion.runWrite(idGenerator: IIdGenerator, author: String?, body: (IBranch
9681072
}
9691073

9701074
private fun String.ensureSuffix(suffix: String) = if (endsWith(suffix)) this else this + suffix
1075+
1076+
private class LimitedReached : RuntimeException("limited reached")

0 commit comments

Comments
 (0)