From 3a2e0c9633d69406f604b7f74fd3e6ff99243a71 Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Thu, 18 Dec 2025 23:14:53 +0100 Subject: [PATCH 1/2] fix(server): support caching absence of version artifacts Part of https://github.com/typesafegithub/github-workflows-kt/issues/2160. The goal is to reduce the number of calls to GitHub and hopefully make the server more stable when lots of requests come. --- .../jitbindingserver/ArtifactRoutes.kt | 4 +-- .../jitbindingserver/CachedVersionArtifact.kt | 27 +++++++++++++++++++ .../workflows/jitbindingserver/Main.kt | 2 +- .../jitbindingserver/ArtifactRoutesTest.kt | 8 +++--- 4 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/CachedVersionArtifact.kt diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt index 553e969ee9..c7cd7960fd 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt @@ -28,8 +28,6 @@ import kotlinx.coroutines.launch private val logger = logger { } -typealias CachedVersionArtifact = VersionArtifacts? - private val prefetchScope = CoroutineScope(Dispatchers.IO) fun Routing.artifactRoutes( @@ -120,7 +118,7 @@ private suspend fun ApplicationCall.toBindingArtifacts( if (refresh) { bindingsCache.invalidate(actionCoords) } - return bindingsCache.get(actionCoords) + return bindingsCache.get(actionCoords).toNullableVersionArtifacts() } private fun PrometheusMeterRegistry.incrementArtifactCounter( diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/CachedVersionArtifact.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/CachedVersionArtifact.kt new file mode 100644 index 0000000000..9c3f1f74d0 --- /dev/null +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/CachedVersionArtifact.kt @@ -0,0 +1,27 @@ +package io.github.typesafegithub.workflows.jitbindingserver + +import io.github.typesafegithub.workflows.mavenbinding.VersionArtifacts + +/** + * This wrapper exists because Caffeine/Aedile cache doesn't support caching + * null values. + */ +sealed interface CachedVersionArtifact { + object Absent : CachedVersionArtifact + + class Present( + val artifacts: VersionArtifacts, + ) : CachedVersionArtifact +} + +fun CachedVersionArtifact.toNullableVersionArtifacts(): VersionArtifacts? = + when (this) { + is CachedVersionArtifact.Absent -> null + is CachedVersionArtifact.Present -> artifacts + } + +fun VersionArtifacts?.toCachedVersionArtifact(): CachedVersionArtifact = + when (this) { + null -> CachedVersionArtifact.Absent + else -> CachedVersionArtifact.Present(this) + } diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt index bb51985e6e..9b4140ccee 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt @@ -109,7 +109,7 @@ private fun buildBindingsCache( .newBuilder() .refreshAfterWrite(1.hours) .recordStats() - .asLoadingCache { buildVersionArtifacts(it, httpClient) } + .asLoadingCache { buildVersionArtifacts(it, httpClient).toCachedVersionArtifact() } @Suppress("ktlint:standard:function-signature") // Conflict with detekt. private fun buildMetadataCache( diff --git a/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutesTest.kt b/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutesTest.kt index 1820b004c2..0e4b160097 100644 --- a/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutesTest.kt +++ b/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutesTest.kt @@ -88,11 +88,9 @@ class ArtifactRoutesTest : // Then response2.status shouldBe HttpStatusCode.NotFound - // This test shows the current behavior where requesting a resource - // that doesn't exist twice in a row causes calling the version artifact - // twice. - // Fix in scope of https://github.com/typesafegithub/github-workflows-kt/issues/2160 - verify(exactly = 2) { mockBuildVersionArtifacts(any(), any()) } + // The fact that the resource doesn't exist is cached, and the + // resource generation logic isn't called in the second request. + verify(exactly = 1) { mockBuildVersionArtifacts(any(), any()) } } } From 3c10dd75b22c1c3b6cd69c4e7212d932403714dc Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Thu, 18 Dec 2025 23:18:31 +0100 Subject: [PATCH 2/2] Use Optional --- .../jitbindingserver/ArtifactRoutes.kt | 6 ++++- .../jitbindingserver/CachedVersionArtifact.kt | 27 ------------------- .../workflows/jitbindingserver/Main.kt | 3 ++- 3 files changed, 7 insertions(+), 29 deletions(-) delete mode 100644 jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/CachedVersionArtifact.kt diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt index c7cd7960fd..25de75eff8 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt @@ -25,9 +25,13 @@ import io.micrometer.prometheusmetrics.PrometheusMeterRegistry import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.util.Optional +import kotlin.jvm.optionals.getOrNull private val logger = logger { } +typealias CachedVersionArtifact = Optional + private val prefetchScope = CoroutineScope(Dispatchers.IO) fun Routing.artifactRoutes( @@ -118,7 +122,7 @@ private suspend fun ApplicationCall.toBindingArtifacts( if (refresh) { bindingsCache.invalidate(actionCoords) } - return bindingsCache.get(actionCoords).toNullableVersionArtifacts() + return bindingsCache.get(actionCoords).getOrNull() } private fun PrometheusMeterRegistry.incrementArtifactCounter( diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/CachedVersionArtifact.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/CachedVersionArtifact.kt deleted file mode 100644 index 9c3f1f74d0..0000000000 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/CachedVersionArtifact.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.typesafegithub.workflows.jitbindingserver - -import io.github.typesafegithub.workflows.mavenbinding.VersionArtifacts - -/** - * This wrapper exists because Caffeine/Aedile cache doesn't support caching - * null values. - */ -sealed interface CachedVersionArtifact { - object Absent : CachedVersionArtifact - - class Present( - val artifacts: VersionArtifacts, - ) : CachedVersionArtifact -} - -fun CachedVersionArtifact.toNullableVersionArtifacts(): VersionArtifacts? = - when (this) { - is CachedVersionArtifact.Absent -> null - is CachedVersionArtifact.Present -> artifacts - } - -fun VersionArtifacts?.toCachedVersionArtifact(): CachedVersionArtifact = - when (this) { - null -> CachedVersionArtifact.Absent - else -> CachedVersionArtifact.Present(this) - } diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt index 9b4140ccee..05b9e9fdad 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt @@ -26,6 +26,7 @@ import io.micrometer.core.instrument.Tag import io.micrometer.prometheusmetrics.PrometheusConfig import io.micrometer.prometheusmetrics.PrometheusMeterRegistry import java.time.Duration +import java.util.Optional import kotlin.time.Duration.Companion.hours private val logger = @@ -109,7 +110,7 @@ private fun buildBindingsCache( .newBuilder() .refreshAfterWrite(1.hours) .recordStats() - .asLoadingCache { buildVersionArtifacts(it, httpClient).toCachedVersionArtifact() } + .asLoadingCache { Optional.ofNullable(buildVersionArtifacts(it, httpClient)) } @Suppress("ktlint:standard:function-signature") // Conflict with detekt. private fun buildMetadataCache(