apollographql / apollo-kotlin

:rocket:  A strongly-typed, caching GraphQL client for the JVM, Android, and Kotlin multiplatform.
https://www.apollographql.com/docs/kotlin
MIT License
3.75k stars 650 forks source link

HttpCache configuration #5878

Open kapilsukhyani opened 5 months ago

kapilsukhyani commented 5 months ago

Question

Can an HTTP cache be shared for multiple ApolloClient instances?

Although the cache policy that we have for now is NetworkOnly, it seems like the queries do get written to the cache respectively here.

Right now we have two clients using the same cache with same cache policy of NetworkOnly but we do see issues with cache where the tmp cache file is sometimes not found, and my assumption here is, that it probably is because of the same cache being used, where a query execution through one client might delete the same file as being used by the other client.

java.io.FileNotFoundException: ..../cache/apolloCache/journal.tmp -> ..../cache/apolloCache/journal
    at okio.NioSystemFileSystem.atomicMove(NioSystemFileSystem.kt:80)
    at com.apollographql.apollo3.cache.http.internal.DiskLruCacheKt.rename(DiskLruCache.kt:1019)
    at com.apollographql.apollo3.cache.http.internal.DiskLruCacheKt.access$rename(DiskLruCache.kt:1)
    at com.apollographql.apollo3.cache.http.internal.DiskLruCache.rebuildJournal(DiskLruCache.kt:371)
    at com.apollographql.apollo3.cache.http.internal.DiskLruCache.initialize(DiskLruCache.kt:225)
    at com.apollographql.apollo3.cache.http.internal.DiskLruCache.edit(DiskLruCache.kt:410)
    at com.apollographql.apollo3.cache.http.internal.DiskLruCache.edit(DiskLruCache.kt:404)
    at com.apollographql.apollo3.cache.http.DiskLruHttpCache.write(DiskLruHttpCache.kt:58)
    at com.apollographql.apollo3.cache.http.CachingHttpInterceptor.networkMightThrow(CachingHttpInterceptor.kt:112)
    at com.apollographql.apollo3.cache.http.CachingHttpInterceptor.intercept(CachingHttpInterceptor.kt:57)
    at com.apollographql.apollo3.network.http.DefaultHttpInterceptorChain.proceed(HttpInterceptor.kt:22)
    at com.apollographql.apollo3.cache.http.HttpCache$httpCache$1.intercept(HttpCacheExtensions.kt:87)
    at com.apollographql.apollo3.network.http.DefaultHttpInterceptorChain.proceed(HttpInterceptor.kt:22)
    at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invokeSuspend(HttpNetworkTransport.kt:65)
    at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invoke(HttpNetworkTransport.kt:0)
    at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invoke(HttpNetworkTransport.kt:0)
    at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invoke(HttpNetworkTransport.kt:0)
    at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invoke(HttpNetworkTransport.kt:0)
martinbonnin commented 5 months ago

Hi 👋

Can you share how you're setting up the HTTP cache?

DiskLruCache accesses are synchronized so I would expect that part to write concurrently as long as it's the same instance of DiskLruCache being used.

kapilsukhyani commented 5 months ago

Here is what it looks like

@[Provides AccountScope]
 fun provideApolloClient(
        site: Site,
        @Authenticated okHttpClient: OkHttpClient,
        @CacheDir cacheDir: File,
        @SentryApolloIntegration sentryApolloIntegration: Boolean,
    ): ApolloClient {
        return buildApolloClient(
            site.graphQLUrl,
            okHttpClient,
            cacheDir
            ApolloBreadcrumbInterceptor("cc-gql"),
            sentryApolloIntegration = sentryApolloIntegration,
        )
    }

    @[Provides AccountScope Agg]
    fun provideAggApolloClient(
        site: Site,
        @Authenticated okHttpClient: OkHttpClient,
        @CacheDir cacheDir: File,
        @SentryApolloIntegration sentryApolloIntegration: Boolean,
    ): ApolloClient {
        return buildApolloClient(
            site.aggGraphQLUrl,
            okHttpClient,
            cacheDir,
            ApolloBreadcrumbInterceptor("AGG"),
            sentryApolloIntegration = sentryApolloIntegration,
        )
    }

    @[Provides CacheDir]
    fun provideCacheDir(application: Application): File = application.cacheDir

    fun buildApolloClient(
    graphQLUrl: HttpUrl,
    okHttpClient: OkHttpClient,
    cacheDir: File
    interceptor: ApolloInterceptor,
    sentryApolloIntegration: Boolean = false,
    additionalInterceptors: List<ApolloInterceptor> = emptyList()
): ApolloClient {
    val builder = ApolloClient.Builder()
        .serverUrl(graphQLUrl.toString())
        .httpCache(File(cacheDir, "apolloCache"), HTTP_CACHE_SIZE)
        .httpFetchPolicy(HttpFetchPolicy.NetworkOnly)
        .okHttpClient(okHttpClient)
        .addInterceptor(interceptor)
        .run {
            if (sentryApolloIntegration) {
                addInterceptor(SentryApollo3Interceptor())
            } else {
                this
            }
        }
        .addHttpInterceptor(LoggingApollo3HttpInterceptor())
    additionalInterceptors.forEach {
        builder.addInterceptor(it)
    }
    return builder.build()
}
martinbonnin commented 5 months ago

Thanks for sending this! My DI skills are a bit lacking (what is @[Provides CacheDir]? Dagger? Something else?) but my guess is that you're sharing the CacheDir which isn't locked. Instead, you should share the DiskLruHttpCache and use the httpCache() overload that takes an ApolloHttpCache.

kapilsukhyani commented 5 months ago

provideCacheDir provides a new instance of a File, but essentially points to the same location, and also the said overload is not available on 3.8.2, the version we are using.

3.8.2, would essentially an instance of cache for each client configured separately, which could then produce the issue as mentioned in the description

For now though, I am choosing to use different sub directory for each client, which should also prevent such issues

martinbonnin commented 5 months ago

I think 3.x should have the overload? See https://github.com/apollographql/apollo-kotlin/blob/release-3.x/libraries/apollo-http-cache/src/main/kotlin/com/apollographql/apollo3/cache/http/HttpCacheExtensions.kt#L82. I would say something like so:

   val sharedCache = DiskLruHttpCache(FileSystem.SYSTEM, File("someDirectory"), HTTP_CACHE_SIZE)
   ApolloClient.Builder()
    .serverUrl("https://...")
    .httpCache(sharedCache)
    ...
    .build()

  // Same with other clients

But using different directories work too 👍

martinbonnin commented 3 months ago

@kapilsukhyani anything else we can help with here?