Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Store Cache and merge optimistic cache with Memory cache #5651

Merged
merged 18 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ import com.apollographql.apollo3.benchmark.test.R
import com.apollographql.apollo3.cache.normalized.incubating.api.CacheHeaders
import com.apollographql.apollo3.cache.normalized.incubating.api.CacheKeyGenerator
import com.apollographql.apollo3.cache.normalized.incubating.api.CacheResolver
import com.apollographql.apollo3.cache.normalized.incubating.api.DefaultRecordMerger
import com.apollographql.apollo3.cache.normalized.incubating.api.FieldPolicyCacheResolver
import com.apollographql.apollo3.cache.normalized.incubating.api.MemoryCacheFactory
import com.apollographql.apollo3.cache.normalized.incubating.api.ReadOnlyNormalizedCache
import com.apollographql.apollo3.cache.normalized.incubating.api.Record
import com.apollographql.apollo3.cache.normalized.incubating.api.TypePolicyCacheKeyGenerator
import com.apollographql.apollo3.cache.normalized.incubating.sql.SqlNormalizedCacheFactory
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import java.lang.reflect.Method
import java.util.concurrent.Executors

@Suppress("UNCHECKED_CAST")
class CacheIncubatingTests {
Expand All @@ -55,6 +58,27 @@ class CacheIncubatingTests {
readFromCache("cacheResponseSql", responseBasedQuery, sql = true, ::checkResponseBased)
}

@Test
fun concurrentCacheOperationMemory() {
concurrentReadWriteFromCache(operationBasedQuery, sql = false)
}

@Test
fun concurrentCacheOperationSql() {
concurrentReadWriteFromCache(operationBasedQuery, sql = true)
}

@Test
fun concurrentCacheResponseMemory() {
concurrentReadWriteFromCache(responseBasedQuery, sql = false)
}

@Test
fun concurrentCacheResponseSql() {
concurrentReadWriteFromCache(responseBasedQuery, sql = true)
}


private fun <D : Query.Data> readFromCache(testName: String, query: Query<D>, sql: Boolean, check: (D) -> Unit) {
val cache = if (sql) {
Utils.dbFile.delete()
Expand All @@ -74,7 +98,7 @@ class CacheIncubatingTests {
) as Map<String, Record>

runBlocking {
cache.merge(records.values.toList(), CacheHeaders.NONE)
cache.merge(records.values.toList(), CacheHeaders.NONE, DefaultRecordMerger)
}

if (sql) {
Expand All @@ -93,7 +117,55 @@ class CacheIncubatingTests {
}
}

private fun <D : Query.Data> concurrentReadWriteFromCache(query: Query<D>, sql: Boolean) {
val cache = if (sql) {
Utils.dbFile.delete()
// Pass context explicitly here because androidx.startup fails due to relocation
SqlNormalizedCacheFactory(InstrumentationRegistry.getInstrumentation().context, Utils.dbName, withDates = true).create()
} else {
MemoryCacheFactory().create()
}
val data = query.parseJsonResponse(resource(R.raw.calendar_response_simple).jsonReader()).data!!

val records = normalizeMethod.invoke(
null,
query,
data,
CustomScalarAdapters.Empty,
TypePolicyCacheKeyGenerator,
) as Map<String, Record>

val threadPool = Executors.newFixedThreadPool(CONCURRENCY)
benchmarkRule.measureRepeated {
val futures = (1..CONCURRENCY).map {
threadPool.submit {
// Let each thread execute a few writes/reads
repeat(WORK_LOAD) {
cache.merge(records.values.toList(), CacheHeaders.NONE, DefaultRecordMerger)

val data2 = readDataFromCacheMethod.invoke(
null,
query,
CustomScalarAdapters.Empty,
cache,
FieldPolicyCacheResolver,
CacheHeaders.NONE
) as D

Assert.assertEquals(data, data2)
}
}
}
// Wait for all threads to finish
futures.forEach { it.get() }
}
}


companion object {
private const val CONCURRENCY = 15
private const val WORK_LOAD = 15

/**
* There doesn't seem to be a way to relocate Kotlin metdata and kotlin_module files so we rely on reflection to call top-level
* methods
Expand Down
1 change: 1 addition & 0 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies {
// builds but this seems just overkill for now
runtimeOnly(libs.kotlin.allopen)
runtimeOnly(libs.kotlinx.serialization.plugin)
runtimeOnly(libs.atomicfu.plugin)

runtimeOnly(libs.sqldelight.plugin)
runtimeOnly(libs.gradle.publish.plugin)
Expand Down
5 changes: 4 additions & 1 deletion gradle/libraries.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ androidx-sqlite = "2.3.1"
apollo = "4.0.0-beta.5-SNAPSHOT"
# Used by the apollo-tooling project which uses a published version of Apollo
apollo-published = "4.0.0-beta.3"
atomicfu = "0.23.1"
cache = "2.0.2"
# See https://developer.android.com/jetpack/androidx/releases/compose-kotlin
compose-compiler = "1.5.11-dev-k2.0.0-Beta4-21f5e479a96"
Expand Down Expand Up @@ -89,7 +90,9 @@ apollo-testingsupport = { group = "com.apollographql.apollo3", name = "apollo-te
apollo-testingsupport-published = { group = "com.apollographql.apollo3", name = "apollo-testing-support", version.ref = "apollo-published" }
apollo-tooling = { group = "com.apollographql.apollo3", name = "apollo-tooling", version.ref = "apollo" }
assertj = { group = "org.assertj", name = "assertj-core", version = "3.24.2" }
atomicfu = { group = "org.jetbrains.kotlinx", name = "atomicfu", version = "0.23.1" }
atomicfu-library = { group = "org.jetbrains.kotlinx", name = "atomicfu", version.ref = "atomicfu" }
atomicfu-plugin = { group = "org.jetbrains.kotlinx", name = "atomicfu-gradle-plugin", version.ref = "atomicfu" }

benmanes-versions = { group = "com.github.ben-manes", name = "gradle-versions-plugin", version = "0.33.0" }
clikt = { group = "com.github.ajalt.clikt", name = "clikt", version = "3.5.2" }
compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "compose" }
Expand Down
2 changes: 1 addition & 1 deletion libraries/apollo-execution-incubating/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ kotlin {
dependencies {
api(project(":apollo-ast"))
api(project(":apollo-api"))
implementation(libs.atomicfu)
implementation(libs.atomicfu.library)
implementation(libs.kotlinx.coroutines)
}
}
Expand Down
2 changes: 1 addition & 1 deletion libraries/apollo-mockserver/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ kotlin {
dependencies {
api(project(":apollo-annotations"))
api(libs.okio)
implementation(libs.atomicfu.get().toString()) {
implementation(libs.atomicfu.library.get().toString()) {
because("We need locks for native (we don't use the gradle plugin rewrite)")
}
api(libs.kotlinx.coroutines)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("kotlinx-atomicfu")
}

apolloLibrary(
javaModuleName = "com.apollographql.apollo3.cache.normalized.api",
javaModuleName = "com.apollographql.apollo3.cache.normalized.api",
withLinux = false,
)

Expand All @@ -15,9 +16,13 @@ kotlin {
api(project(":apollo-mpp-utils"))
implementation(libs.okio)
api(libs.uuid)
implementation(libs.atomicfu.get().toString()) {
because("Use of ReentrantLock for Apple (we don't use the gradle plugin rewrite)")
}
implementation(libs.atomicfu.library)
}
}

findByName("commonTest")?.apply {
dependencies {
implementation(project(":apollo-testing-support"))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.apollographql.apollo3.cache.normalized.api.internal

internal actual fun <K, V> ConcurrentMap(): MutableMap<K, V> = CommonConcurrentMap()