Skip to content

Commit 92ee947

Browse files
committed
Defer SQLite driver creation to NormalizedCacheFactory.create()
The SqlNormalizedCacheFactory() functions resolved the database path and built the driver as soon as they were called, so that work ran while the ApolloClient was being built rather than when the cache is first used. The JVM factory created the base directory, and the Android one checked whether the database file already exists before falling back to the cache directory — which StrictMode reports as a main thread disk read.
1 parent 34276eb commit 92ee947

8 files changed

Lines changed: 91 additions & 53 deletions

File tree

normalized-cache-sqlite/src/androidMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCacheFactory.android.kt

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
77
import app.cash.sqldelight.async.coroutines.synchronous
88
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
99
import com.apollographql.apollo.exception.apolloExceptionHandler
10+
import com.apollographql.cache.normalized.api.NormalizedCache
1011
import com.apollographql.cache.normalized.api.NormalizedCacheFactory
1112
import com.apollographql.cache.normalized.sql.internal.record.SqlRecordDatabase
1213

@@ -30,50 +31,50 @@ fun SqlNormalizedCacheFactory(
3031
configure: ((SupportSQLiteDatabase) -> Unit)? = null,
3132
useNoBackupDirectory: Boolean = false,
3233
windowSizeBytes: Long? = 4 * 1024 * 1024,
33-
): NormalizedCacheFactory {
34-
val synchronousSchema = SqlRecordDatabase.Schema.synchronous()
35-
val filePath = when {
36-
name == null -> {
37-
null
38-
}
34+
): NormalizedCacheFactory = object : NormalizedCacheFactory() {
35+
override fun create(): NormalizedCache {
36+
val synchronousSchema = SqlRecordDatabase.Schema.synchronous()
37+
val filePath = when {
38+
name == null -> {
39+
null
40+
}
3941

40-
name.startsWith("/") -> {
41-
// Absolute path: keep as-is
42-
name
43-
}
42+
name.startsWith("/") -> {
43+
// Absolute path: keep as-is
44+
name
45+
}
4446

45-
else -> {
46-
// Old versions of the library used to store the database in the database directory.
47-
// If such file exists, use it, otherwise, use the cache directory.
48-
(context.getDatabasePath(name).takeIf { it.exists() } ?: context.cacheDir.resolve(name)).absolutePath
47+
else -> {
48+
// Old versions of the library used to store the database in the database directory.
49+
// If such file exists, use it, otherwise, use the cache directory.
50+
(context.getDatabasePath(name).takeIf { it.exists() } ?: context.cacheDir.resolve(name)).absolutePath
51+
}
4952
}
50-
}
51-
return SqlNormalizedCacheFactory(
52-
driver = AndroidSqliteDriver(
53-
schema = synchronousSchema,
54-
context = context.applicationContext,
55-
name = filePath,
56-
factory = factory,
57-
callback = object : AndroidSqliteDriver.Callback(synchronousSchema) {
58-
override fun onConfigure(db: SupportSQLiteDatabase) {
59-
super.onConfigure(db)
60-
configure?.invoke(db)
61-
}
53+
val driver = AndroidSqliteDriver(
54+
schema = synchronousSchema,
55+
context = context.applicationContext,
56+
name = filePath,
57+
factory = factory,
58+
callback = object : AndroidSqliteDriver.Callback(synchronousSchema) {
59+
override fun onConfigure(db: SupportSQLiteDatabase) {
60+
super.onConfigure(db)
61+
configure?.invoke(db)
62+
}
6263

63-
override fun onCorruption(db: SupportSQLiteDatabase) {
64-
apolloExceptionHandler(Exception("Corruption detected, recreating the database"))
65-
super.onCorruption(db)
66-
}
64+
override fun onCorruption(db: SupportSQLiteDatabase) {
65+
apolloExceptionHandler(Exception("Corruption detected, recreating the database"))
66+
super.onCorruption(db)
67+
}
6768

68-
override fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
69-
// Treat downgrades as corruption, which results in a clean database
70-
apolloExceptionHandler(Exception("onDowngrade from $oldVersion to $newVersion, treating as database corruption"))
71-
super.onCorruption(db)
72-
}
73-
},
74-
useNoBackupDirectory = useNoBackupDirectory,
75-
windowSizeBytes = windowSizeBytes,
76-
),
77-
name = filePath,
78-
)
69+
override fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
70+
// Treat downgrades as corruption, which results in a clean database
71+
apolloExceptionHandler(Exception("onDowngrade from $oldVersion to $newVersion, treating as database corruption"))
72+
super.onCorruption(db)
73+
}
74+
},
75+
useNoBackupDirectory = useNoBackupDirectory,
76+
windowSizeBytes = windowSizeBytes,
77+
)
78+
return SqlNormalizedCache(driver, filePath)
79+
}
7980
}

normalized-cache-sqlite/src/appleMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCacheFactory.apple.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import app.cash.sqldelight.db.SqlDriver
55
import app.cash.sqldelight.driver.native.NativeSqliteDriver
66
import app.cash.sqldelight.driver.native.wrapConnection
77
import co.touchlab.sqliter.DatabaseConfiguration
8+
import com.apollographql.cache.normalized.api.NormalizedCache
89
import com.apollographql.cache.normalized.api.NormalizedCacheFactory
910
import com.apollographql.cache.normalized.sql.internal.record.SqlRecordDatabase
1011

@@ -17,7 +18,11 @@ import com.apollographql.cache.normalized.sql.internal.record.SqlRecordDatabase
1718
fun SqlNormalizedCacheFactory(
1819
name: String?,
1920
baseDir: String?,
20-
): NormalizedCacheFactory = SqlNormalizedCacheFactory(createDriver(name, baseDir), name)
21+
): NormalizedCacheFactory = object : NormalizedCacheFactory() {
22+
override fun create(): NormalizedCache {
23+
return SqlNormalizedCache(createDriver(name, baseDir), name)
24+
}
25+
}
2126

2227
actual fun SqlNormalizedCacheFactory(name: String?): NormalizedCacheFactory = SqlNormalizedCacheFactory(name, null)
2328

normalized-cache-sqlite/src/commonMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCache.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.apollographql.cache.normalized.sql
22

3+
import app.cash.sqldelight.db.SqlDriver
34
import com.apollographql.apollo.exception.apolloExceptionHandler
45
import com.apollographql.cache.normalized.api.ApolloCacheHeaders
56
import com.apollographql.cache.normalized.api.CacheHeaders
@@ -19,8 +20,11 @@ import kotlinx.coroutines.flow.toList
1920
import kotlin.reflect.KClass
2021

2122
class SqlNormalizedCache internal constructor(
22-
private val recordDatabase: RecordDatabase,
23+
driver: SqlDriver,
24+
name: String?,
2325
) : NormalizedCache {
26+
private val recordDatabase = RecordDatabase(driver, name)
27+
2428

2529
override suspend fun loadRecord(key: CacheKey, cacheHeaders: CacheHeaders): Record? {
2630
return loadRecords(keys = listOf(key), cacheHeaders = cacheHeaders).firstOrNull()

normalized-cache-sqlite/src/commonMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCacheFactory.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.apollographql.cache.normalized.sql
33
import app.cash.sqldelight.db.SqlDriver
44
import com.apollographql.cache.normalized.api.NormalizedCache
55
import com.apollographql.cache.normalized.api.NormalizedCacheFactory
6-
import com.apollographql.cache.normalized.sql.internal.RecordDatabase
76

87
/**
98
* Creates a new [NormalizedCacheFactory] that uses a persistent cache based on Sqlite
@@ -24,6 +23,6 @@ fun SqlNormalizedCacheFactory(driver: SqlDriver): NormalizedCacheFactory = SqlNo
2423

2524
fun SqlNormalizedCacheFactory(driver: SqlDriver, name: String?): NormalizedCacheFactory = object : NormalizedCacheFactory() {
2625
override fun create(): NormalizedCache {
27-
return SqlNormalizedCache(RecordDatabase(driver, name))
26+
return SqlNormalizedCache(driver, name)
2827
}
2928
}

normalized-cache-sqlite/src/commonTest/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCacheTest.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import com.apollographql.cache.normalized.api.CacheKey
1414
import com.apollographql.cache.normalized.api.DefaultRecordMerger
1515
import com.apollographql.cache.normalized.api.NormalizedCache
1616
import com.apollographql.cache.normalized.api.Record
17-
import com.apollographql.cache.normalized.sql.internal.RecordDatabase
1817
import com.apollographql.cache.normalized.testing.Platform
1918
import com.apollographql.cache.normalized.testing.fieldKey
2019
import com.apollographql.cache.normalized.testing.platform
@@ -183,7 +182,7 @@ class SqlNormalizedCacheTest {
183182

184183
@Test
185184
fun exceptionCallsExceptionHandler() = runTest(before = { setUp() }, after = { tearDown() }) {
186-
val badCache = SqlNormalizedCache(RecordDatabase(BadDriver, null))
185+
val badCache = SqlNormalizedCache(BadDriver, null)
187186
var throwable: Throwable? = null
188187
apolloExceptionHandler = {
189188
throwable = it

normalized-cache-sqlite/src/jsCommonMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCacheFactory.jsCommon.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.apollographql.cache.normalized.sql
22

33
import app.cash.sqldelight.driver.worker.createDefaultWebWorkerDriver
4+
import com.apollographql.cache.normalized.api.NormalizedCache
45
import com.apollographql.cache.normalized.api.NormalizedCacheFactory
56

67
/**
@@ -14,7 +15,9 @@ import com.apollographql.cache.normalized.api.NormalizedCacheFactory
1415
*
1516
* See [the SQLDelight documentation](https://sqldelight.github.io/sqldelight/2.1.0/js_sqlite/sqljs_worker/).
1617
*/
17-
actual fun SqlNormalizedCacheFactory(name: String?): NormalizedCacheFactory {
18-
return SqlNormalizedCacheFactory(createDefaultWebWorkerDriver(), name = null)
18+
actual fun SqlNormalizedCacheFactory(name: String?): NormalizedCacheFactory = object : NormalizedCacheFactory() {
19+
override fun create(): NormalizedCache {
20+
return SqlNormalizedCache(createDefaultWebWorkerDriver(), name = null)
21+
}
1922
}
2023

normalized-cache-sqlite/src/jvmMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCacheFactory.jvm.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.apollographql.cache.normalized.sql
22

33
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
4+
import com.apollographql.cache.normalized.api.NormalizedCache
45
import com.apollographql.cache.normalized.api.NormalizedCacheFactory
56
import java.io.File
67
import java.util.Properties
@@ -13,7 +14,11 @@ import java.util.Properties
1314
fun SqlNormalizedCacheFactory(
1415
url: String,
1516
properties: Properties,
16-
): NormalizedCacheFactory = SqlNormalizedCacheFactory(JdbcSqliteDriver(url, properties), name = null)
17+
): NormalizedCacheFactory = object : NormalizedCacheFactory() {
18+
override fun create(): NormalizedCache {
19+
return SqlNormalizedCache(JdbcSqliteDriver(url, properties), name = null)
20+
}
21+
}
1722

1823
/**
1924
* @param name the name of the database or null for an in-memory database
@@ -24,7 +29,11 @@ fun SqlNormalizedCacheFactory(
2429
fun SqlNormalizedCacheFactory(
2530
name: String?,
2631
baseDir: String?,
27-
): NormalizedCacheFactory = SqlNormalizedCacheFactory(JdbcSqliteDriver(name.toUrl(baseDir), Properties()), name = name)
32+
): NormalizedCacheFactory = object : NormalizedCacheFactory() {
33+
override fun create(): NormalizedCache {
34+
return SqlNormalizedCache(JdbcSqliteDriver(name.toUrl(baseDir), Properties()), name = name)
35+
}
36+
}
2837

2938
actual fun SqlNormalizedCacheFactory(name: String?): NormalizedCacheFactory = SqlNormalizedCacheFactory(name, null)
3039

normalized-cache-sqlite/src/jvmTest/kotlin/com/apollographql/cache/normalized/sql/JvmSqlNormalizedCacheTest.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,35 @@ import com.apollographql.cache.normalized.api.CacheHeaders
99
import com.apollographql.cache.normalized.api.CacheKey
1010
import com.apollographql.cache.normalized.api.DefaultRecordMerger
1111
import com.apollographql.cache.normalized.api.Record
12-
import com.apollographql.cache.normalized.sql.internal.RecordDatabase
1312
import com.apollographql.cache.normalized.testing.runTest
13+
import java.io.File
1414
import java.util.Properties
1515
import kotlin.test.Test
1616
import kotlin.test.assertEquals
17+
import kotlin.test.assertFalse
18+
import kotlin.test.assertTrue
1719

1820
class JvmSqlNormalizedCacheTest {
21+
@Test
22+
fun theDatabaseIsNotCreatedUntilTheCacheIs() {
23+
val baseDir = File(System.getProperty("java.io.tmpdir"), "apollo-test-${System.nanoTime()}")
24+
try {
25+
val factory = SqlNormalizedCacheFactory(name = "test.db", baseDir = baseDir.path)
26+
27+
// Creating the factory must not touch the disk.
28+
assertFalse(baseDir.exists(), "The base directory must not be created before NormalizedCacheFactory.create()")
29+
30+
factory.create()
31+
assertTrue(baseDir.exists(), "The base directory must be created by NormalizedCacheFactory.create()")
32+
} finally {
33+
baseDir.deleteRecursively()
34+
}
35+
}
36+
1937
@Test
2038
fun mergingIdenticalRecordIsANoOp() = runTest {
2139
val driver = SpyDriver(JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY, Properties()))
22-
val cache = SqlNormalizedCache(RecordDatabase(driver, null))
40+
val cache = SqlNormalizedCache(driver, null)
2341
// Initial insert
2442
cache.merge(
2543
record = Record(

0 commit comments

Comments
 (0)