Skip to content

Commit ee2ee46

Browse files
authored
Cache error responses (#50)
With our publishing mechanism of only including known published artifacts in the BOM, we rarely see 404s or errors. When we do it usually means something bad went wrong but we should not keep retrying it to avoid overwhelming the artifact repo backend.
1 parent 1820a8e commit ee2ee46

File tree

3 files changed

+65
-7
lines changed

3 files changed

+65
-7
lines changed

cli/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ dependencies {
2222
implementation(libs.kotlinxCoroutines)
2323
implementation(libs.moshi)
2424
implementation(libs.okhttp)
25+
implementation(libs.okhttp.logging)
2526
implementation(libs.picocli.core)
2627
implementation(libs.retrofit.core)
2728
implementation(libs.retrofit.converter.jackson)
2829
implementation(libs.retrofit.wire)
30+
implementation(libs.slf4j.api)
2931

3032
runtimeOnly(libs.log4j.slf4j2.impl) {
3133
because("JGit uses SLF4J for logging")

cli/src/main/kotlin/xyz/block/artifactswap/cli/di/NetworkModule.kt

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@ import com.fasterxml.jackson.databind.ObjectMapper
55
import com.fasterxml.jackson.databind.SerializationFeature
66
import com.fasterxml.jackson.dataformat.xml.XmlMapper
77
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
8+
import java.io.File
89
import kotlin.io.path.Path
910
import kotlin.io.path.readLines
1011
import kotlin.time.Duration.Companion.seconds
1112
import kotlin.time.toJavaDuration
13+
import okhttp3.Cache
1214
import okhttp3.Dispatcher
15+
import okhttp3.Interceptor
1316
import okhttp3.OkHttpClient
17+
import okhttp3.logging.HttpLoggingInterceptor
1418
import org.koin.core.KoinApplication
1519
import org.koin.core.qualifier.named
1620
import org.koin.dsl.module
21+
import org.slf4j.LoggerFactory
1722
import retrofit2.Retrofit
1823
import retrofit2.converter.jackson.JacksonConverterFactory
24+
import retrofit2.converter.wire.WireConverterFactory
1925
import retrofit2.create
2026
import xyz.block.artifactswap.core.config.ArtifactSwapConfigHolder
2127
import xyz.block.artifactswap.core.eventstream.Eventstream
@@ -26,18 +32,67 @@ import xyz.block.artifactswap.core.network.ArtifactoryService
2632
private val UNAUTHENTICATED_HTTP_METHODS = listOf("GET", "HEAD")
2733

2834
internal fun artifactoryNetworkModule() = module {
35+
// HTTP cache for OkHttp client (10MB)
36+
// It's small because we only cache error responses from the repo
37+
single<Cache> {
38+
val cacheDirectory = File(System.getProperty("java.io.tmpdir"), "artifactswap-cache")
39+
Cache(cacheDirectory, 10L * 1024L * 1024L)
40+
}
41+
42+
single<HttpLoggingInterceptor> {
43+
val logger = LoggerFactory.getLogger("http")
44+
HttpLoggingInterceptor { message -> logger.trace(message) }
45+
.apply { level = HttpLoggingInterceptor.Level.BASIC }
46+
}
47+
48+
// Interceptor that caches only 404 responses for 1 hour and prevents caching of successful
49+
// responses.
50+
// Successful artifact downloads are implicitly cached in the local .m2 repository.
51+
// Missing artifacts are expensive to query from the repo backend and should be cached.
52+
single<Interceptor>(named("cache404Interceptor")) {
53+
Interceptor { chain ->
54+
val response = chain.proceed(chain.request())
55+
56+
when (response.code) {
57+
// Cache 404 responses for 1 hour
58+
404 -> {
59+
response
60+
.newBuilder()
61+
.header("Cache-Control", "public, max-age=3600, immutable")
62+
.removeHeader("Pragma")
63+
.removeHeader("Expires")
64+
.build()
65+
}
66+
// Prevent caching of successful responses (artifacts are cached in local .m2 repo)
67+
in 200..299 -> {
68+
response
69+
.newBuilder()
70+
.header("Cache-Control", "no-store")
71+
.removeHeader("Pragma")
72+
.removeHeader("Expires")
73+
.build()
74+
}
75+
// Don't modify other response codes
76+
else -> response
77+
}
78+
}
79+
}
80+
2981
single<OkHttpClient>(named("artifactoryClient")) {
3082
OkHttpClient.Builder()
83+
.cache(get<Cache>())
3184
.retryOnConnectionFailure(true)
32-
.connectTimeout(30.seconds.toJavaDuration())
33-
.readTimeout(30.seconds.toJavaDuration())
34-
.callTimeout(30.seconds.toJavaDuration())
85+
.connectTimeout(5.seconds.toJavaDuration())
86+
.readTimeout(10.seconds.toJavaDuration())
87+
.callTimeout(10.seconds.toJavaDuration())
3588
.dispatcher(
3689
Dispatcher().apply {
3790
maxRequestsPerHost = 128
3891
maxRequests = 512
3992
}
4093
)
94+
.addInterceptor(get<HttpLoggingInterceptor>())
95+
.addNetworkInterceptor(get<Interceptor>(named("cache404Interceptor")))
4196
.addInterceptor { chain ->
4297
// GET/HEAD methods don't require authentication
4398
if (chain.request().method !in UNAUTHENTICATED_HTTP_METHODS) {
@@ -95,15 +150,15 @@ internal fun analyticsNetworkModule() = module {
95150
single<EventstreamService> {
96151
val httpClient =
97152
OkHttpClient.Builder()
98-
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
99-
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
100-
.writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
153+
.connectTimeout(5.seconds.toJavaDuration())
154+
.readTimeout(10.seconds.toJavaDuration())
155+
.writeTimeout(10.seconds.toJavaDuration())
101156
.build()
102157

103158
Retrofit.Builder()
104159
.baseUrl(ArtifactSwapConfigHolder.instance.eventstreamBaseUrl)
105160
.client(httpClient)
106-
.addConverterFactory(retrofit2.converter.wire.WireConverterFactory.create())
161+
.addConverterFactory(WireConverterFactory.create())
107162
.build()
108163
.create<EventstreamService>()
109164
}

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
6565
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
6666
moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
6767
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp"}
68+
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
6869
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
6970
picocli-core = { module = "info.picocli:picocli", version.ref = "picocli" }
7071
slf4j-api = { module = "org.slf4j:slf4j-api", version = "1.7.36" }

0 commit comments

Comments
 (0)