Skip to content

Commit

Permalink
Use kotlinx serialization instead of java.io.Serializable
Browse files Browse the repository at this point in the history
  • Loading branch information
sellmair committed Apr 12, 2024
1 parent 58922b3 commit d2c1bac
Show file tree
Hide file tree
Showing 30 changed files with 178 additions and 51 deletions.
4 changes: 3 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import kotlin.io.path.Path

plugins {
//kotlin("jvm") version "2.0.0-Beta5"
kotlin("jvm") version "1.9.21"
kotlin("jvm") version "1.9.23"
kotlin("plugin.serialization") version "1.9.23"
}

kotlin {
Expand Down Expand Up @@ -60,6 +61,7 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.23")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.6.3")
implementation("org.slf4j:slf4j-api:2.0.12")
implementation("org.slf4j:slf4j-jdk14:2.0.12")
implementation("io.ktor:ktor-client-cio-jvm:2.3.9")
Expand Down
16 changes: 11 additions & 5 deletions core/src/main/kotlin/io/sellmair/okay/OkCache.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package io.sellmair.okay

import io.sellmair.okay.serialization.format
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.nio.file.Path
Expand Down Expand Up @@ -32,31 +36,33 @@ internal val OkContext.cacheBlobsDirectory
* @param key obtained by [OkInput.state]
* @return the cache record associated with the given input key or `null` if no such record is available
*/
internal suspend fun OkContext.readCacheRecord(key: OkHash): OkCacheRecord<*>? = withOkContext(okCacheDispatcher) {
@OptIn(ExperimentalSerializationApi::class)
internal suspend fun OkContext.readCacheRecord(key: OkHash): OkCacheRecord? = withOkContext(okCacheDispatcher) {
val file = cacheEntriesDirectory.resolve(key.value)
if (!file.system().isRegularFile()) return@withOkContext null
ObjectInputStream(file.system().inputStream()).use { stream ->
stream.readObject() as? OkCacheRecord<*>
format.decodeFromByteArray<OkCacheRecord>(stream.readAllBytes())
}/*
We should not read entries that have been written during this exact session.
Otherwise, the UP-TO-DATE checks have a problem:
Yes, the recently stored cache entry is UP-TO-DATE (because it was just stored),
but it might not be UP-TO-DATE from the perspective of the previously stored
coroutine as the dependencies (or outputs) might have changed.
*/
.takeUnless { it?.session == currentOkSessionId() }
.takeUnless { it.session == currentOkSessionId() }
}

/**
* ⚠️Only stores the cache record!!! Consider using [storeCachedCoroutine] instead?
*/
@OptIn(ExperimentalSerializationApi::class)
@OkUnsafe("Consider using ")
internal suspend fun OkContext.storeCacheRecord(value: OkCacheRecord<*>): Path = withOkContext(okCacheDispatcher) {
internal suspend fun OkContext.storeCacheRecord(value: OkCacheRecord): Path = withOkContext(okCacheDispatcher) {
cacheEntriesDirectory.system().createDirectories()

val file = cacheEntriesDirectory.resolve(value.inputHash.value).system()
ObjectOutputStream(file.outputStream().buffered()).use { stream ->
stream.writeObject(value)
stream.write(format.encodeToByteArray(value))
}

file
Expand Down
14 changes: 9 additions & 5 deletions core/src/main/kotlin/io/sellmair/okay/OkCacheRecord.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,29 @@ package io.sellmair.okay
import io.sellmair.okay.input.OkInput
import io.sellmair.okay.io.OkPath
import io.sellmair.okay.output.OkOutput
import java.io.Serializable
import io.sellmair.okay.serialization.Base64Serializer
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable

data class OkCacheRecord<T>(
@Serializable
class OkCacheRecord(
val session: OkSessionId,
val descriptor: OkCoroutineDescriptor<T>,
val descriptor: OkCoroutineDescriptor<@Contextual Any?>,
val input: OkInput,
val inputHash: OkHash,
val dependencies: Set<OkHash>,

val output: OkOutput? = null,
val outputHash: OkHash? = null,
val outputValue: T? = null,
@Serializable(Base64Serializer::class)
val payload: ByteArray? = null,
/**
* Represents the state of all regular files associated with the cache record.
* key: The path to the captured file
* value: A hash of the file content (the file content can be retrieved from the cache using this hash)
*/
val outputFiles: Map<OkPath, OkHash>? = null

) : Serializable
)


12 changes: 6 additions & 6 deletions core/src/main/kotlin/io/sellmair/okay/OkCacheRestore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import kotlin.io.path.*
internal sealed class OkCacheResult

internal data class OkCacheHit(
val record: OkCacheRecord<*>,
val upToDate: List<OkCacheRecord<*>> = emptyList(),
val restored: List<OkCacheRecord<*>> = emptyList()
val record: OkCacheRecord,
val upToDate: List<OkCacheRecord> = emptyList(),
val restored: List<OkCacheRecord> = emptyList()
) : OkCacheResult() {
override fun toString(): String {
return "CacheHit(" +
Expand All @@ -23,7 +23,7 @@ internal data class OkCacheHit(

internal data class OkCacheMiss(
val missing: List<OkHash> = emptyList(),
val dirty: List<OkCacheRecord<*>> = emptyList()
val dirty: List<OkCacheRecord> = emptyList()
) : OkCacheResult()

/**
Expand Down Expand Up @@ -58,7 +58,7 @@ private suspend fun OkContext.tryRestoreCachedCoroutineChecked(cacheKey: OkHash)
}

private suspend fun OkContext.tryRestoreCacheRecord(
record: OkCacheRecord<*>
record: OkCacheRecord
): OkCacheResult {
/* Launch & await restore of dependencies */
val dependencyResult = record.dependencies.fold<_, OkCacheResult>(OkCacheHit(record)) { result, dependencyKey ->
Expand Down Expand Up @@ -89,7 +89,7 @@ private suspend fun OkContext.tryRestoreCacheRecord(
}

private fun OkContext.restoreFilesFromCache(
entry: OkCacheRecord<*>
entry: OkCacheRecord
) {
entry.output.withClosure { output -> if (output is OkOutputs) output.values else emptyList() }
.filterIsInstance<OkOutputDirectory>()
Expand Down
14 changes: 10 additions & 4 deletions core/src/main/kotlin/io/sellmair/okay/OkCacheStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import io.sellmair.okay.output.OkOutput
import io.sellmair.okay.output.OkOutputDirectory
import io.sellmair.okay.output.OkOutputFile
import io.sellmair.okay.output.OkOutputs
import io.sellmair.okay.serialization.format
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToStream
import java.nio.file.FileAlreadyExistsException
import java.nio.file.Path
import kotlin.io.path.*
Expand All @@ -28,8 +34,9 @@ suspend fun <T> OkContext.storeCachedCoroutine(
inputHash: OkHash,
output: OkOutput,
outputValue: T,
serializer: KSerializer<T>,
dependencies: Set<OkHash>
): OkCacheRecord<T> = withOkContext(Dispatchers.IO) {
): OkCacheRecord = withOkContext(Dispatchers.IO) {
cacheBlobsDirectory.system().createDirectories()

val outputFiles = output.walkFiles()
Expand All @@ -50,15 +57,14 @@ suspend fun <T> OkContext.storeCachedCoroutine(
}
.toMap()


val entry = OkCacheRecord(
session = currentOkSessionId(),
descriptor = descriptor,
descriptor = descriptor as OkCoroutineDescriptor<Any?>,
input = input,
inputHash = inputHash,
output = output,
outputHash = output.currentHash(ctx),
outputValue = outputValue,
payload = format.encodeToByteArray(serializer, outputValue),
outputFiles = outputFiles,
dependencies = dependencies.toSet(),
)
Expand Down
32 changes: 24 additions & 8 deletions core/src/main/kotlin/io/sellmair/okay/OkCoroutine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package io.sellmair.okay
import io.sellmair.okay.input.OkInput
import io.sellmair.okay.input.plus
import io.sellmair.okay.output.OkOutput
import io.sellmair.okay.serialization.format
import kotlinx.coroutines.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

Expand Down Expand Up @@ -49,7 +52,7 @@ suspend fun <T> OkContext.memoizedCoroutine(
@OptIn(OkUnsafe::class)
storeCacheRecord(
OkCacheRecord(
currentOkSessionId(), descriptor, effectiveInput, inputHash, result.dependencies
currentOkSessionId(), descriptor as OkCoroutineDescriptor<Any?>, effectiveInput, inputHash, result.dependencies
)
)
result.value
Expand All @@ -69,6 +72,14 @@ suspend fun <T> OkContext.memoizedCoroutine(
return coroutine.value.await()
}

suspend inline fun <reified T> OkContext.cachedCoroutine(
descriptor: OkCoroutineDescriptor<T>, input: OkInput, output: OkOutput,
noinline body: suspend OkContext.() -> T
): T = cachedCoroutine(
descriptor, input, output, serializer(), body
)


/**
* Will launch a coroutine which will be shared and then cached.
* If the computation was already done previously, the returned [OkAsync] will be able to provide
Expand All @@ -83,13 +94,15 @@ suspend fun <T> OkContext.memoizedCoroutine(
*
*/
suspend fun <T> OkContext.cachedCoroutine(
descriptor: OkCoroutineDescriptor<T>, input: OkInput, output: OkOutput, body: suspend OkContext.() -> T
descriptor: OkCoroutineDescriptor<T>, input: OkInput, output: OkOutput,
serializer: KSerializer<T>,
body: suspend OkContext.() -> T
): T {
val effectiveInput = descriptor + input
/* How to bind dependencies from the 'cached' coroutine? */
/* The async value from okCoroutineCache should return the dependency key to bind to! */
val coroutine = cs.coroutineContext.okCoroutineCache.getOrPut(effectiveInput) {
restoreOrLaunchTask(descriptor, effectiveInput, output, body)
restoreOrLaunchTask(descriptor, effectiveInput, output, serializer, body)
}

/* Bind the dependency to the new coroutine */
Expand All @@ -106,25 +119,27 @@ suspend fun <T> OkContext.cachedCoroutine(
}

private fun <T> OkContext.restoreOrLaunchTask(
descriptor: OkCoroutineDescriptor<T>, input: OkInput, output: OkOutput, body: suspend OkContext.() -> T
descriptor: OkCoroutineDescriptor<T>, input: OkInput, output: OkOutput,
serializer: KSerializer<T>,
body: suspend OkContext.() -> T
): OkCoroutine<T> {
return launchOkCoroutine(input, cs.coroutineContext.pushOkStack(descriptor) + Job()) { key ->
val cacheResult = tryRestoreCachedCoroutineUnchecked(key)
cs.coroutineContext[OkCoroutineCacheHook]?.onCacheResult(descriptor, cacheResult)

@Suppress("UNCHECKED_CAST")
when (cacheResult) {
is OkCacheHit -> cacheResult.record.outputValue as T
is OkCacheMiss -> runCoroutine(key, descriptor, input, output, body)
is OkCacheHit -> format.decodeFromByteArray(serializer, cacheResult.record.payload!!)
is OkCacheMiss -> runCoroutine(key, descriptor, input, output, serializer, body)
}
}
}

private suspend fun <T> OkContext.runCoroutine(
inputHash: OkHash,
descriptor: OkCoroutineDescriptor<T>,
input: OkInput,
output: OkOutput,
input: OkInput, output: OkOutput,
serializer: KSerializer<T>,
body: suspend OkContext.() -> T
): T {
val resultWithDependencies = withOkCoroutineDependencies {
Expand All @@ -137,6 +152,7 @@ private suspend fun <T> OkContext.runCoroutine(
inputHash = inputHash,
output = output,
outputValue = resultWithDependencies.value,
serializer = serializer,
dependencies = resultWithDependencies.dependencies
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ package io.sellmair.okay

import io.sellmair.okay.input.OkInput
import io.sellmair.okay.io.OkPath
import java.io.Serializable
import kotlinx.serialization.Serializable
import kotlin.reflect.typeOf

@Serializable
data class OkCoroutineDescriptor<T>(
val id: String,
val title: String,
val module: OkPath,
val verbosity: Verbosity,
val signatureOfT: String
) : Serializable, OkInput {
): OkInput {
enum class Verbosity {
Silent, Debug, Info
}
Expand Down
6 changes: 3 additions & 3 deletions core/src/main/kotlin/io/sellmair/okay/OkHash.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.sellmair.okay

import io.sellmair.okay.io.OkPath
import java.io.Serializable
import java.security.MessageDigest
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
Expand All @@ -11,13 +10,14 @@ fun OkHash(hash: ByteArray): OkHash {
return OkHash(Base64.UrlSafe.encode(hash))
}

data class OkHash(val value: String) : Serializable {

@kotlinx.serialization.Serializable
data class OkHash(val value: String) {
override fun toString(): String {
return value.take(6)
}
}


fun HashBuilder(): HashBuilder = HashBuilderImpl()

interface HashBuilder {
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/kotlin/io/sellmair/okay/OkSessionId.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import java.io.Serializable
import java.util.UUID
import kotlin.coroutines.CoroutineContext

class OkSessionId private constructor(private val value: UUID) : Serializable, CoroutineContext.Element {
@kotlinx.serialization.Serializable
class OkSessionId private constructor(private val value: String) : Serializable, CoroutineContext.Element {
override val key get() = Key

companion object Key : CoroutineContext.Key<OkSessionId> {
fun random() = OkSessionId(UUID.randomUUID())
fun random() = OkSessionId(UUID.randomUUID().toString())
}

override fun toString(): String {
Expand Down
12 changes: 8 additions & 4 deletions core/src/main/kotlin/io/sellmair/okay/input/OkInput.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package io.sellmair.okay.input

import io.sellmair.okay.OkCoroutineDescriptor
import io.sellmair.okay.OkState
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.builtins.NothingSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import kotlinx.serialization.protobuf.ProtoBuf
import java.io.Serializable


interface OkInput : Serializable, OkState {

companion object {
fun none(): OkInput = OkInputs(emptyList())
}
}



2 changes: 2 additions & 0 deletions core/src/main/kotlin/io/sellmair/okay/input/OkInputFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import io.sellmair.okay.*
import io.sellmair.okay.io.OkPath
import io.sellmair.okay.io.directoryStateHash
import io.sellmair.okay.io.regularFileStateHash
import kotlinx.serialization.Serializable
import kotlin.io.path.isDirectory

fun OkPath.asInput(): OkInputFile = OkInputFile(this)

@Serializable
data class OkInputFile(val path: OkPath) : OkInput {
override suspend fun currentHash(ctx: OkContext): OkHash {
val systemPath = path.system()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import io.sellmair.okay.OkHash
import io.sellmair.okay.hash
import io.sellmair.okay.io.OkFileCollection
import io.sellmair.okay.io.regularFileStateHash
import kotlinx.serialization.Serializable

fun OkFileCollection.asInput(): OkInputFileCollection =
OkInputFileCollection(this)


@Serializable
data class OkInputFileCollection(val files: OkFileCollection) : OkInput {
override suspend fun currentHash(ctx: OkContext): OkHash {
return hash {
Expand Down
Loading

0 comments on commit d2c1bac

Please sign in to comment.