Skip to content

Commit 607d1bc

Browse files
committed
Share settings cache between running processes (#6788)
With the multi-process data store change, all processes will read the settings cache from the same file safely. This means if a second process started, it would read the cache the first process persisted. But if 2 processes were already running, and one fetched and cached new settings, it wouldn't automatically share it with the other running process. This change fixes that by having all processes watch the file. Also cleaned up settings a bit, and made everything in seconds to avoid converting units. Cleaned up unit tests. Also removed the need to lazy load the cache by doing a double check in the getter instead. There is more potential to clean up, but let's do it later.
1 parent 25f3b8b commit 607d1bc

File tree

10 files changed

+365
-388
lines changed

10 files changed

+365
-388
lines changed

firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@ import androidx.test.core.app.ApplicationProvider
2020
import androidx.test.ext.junit.runners.AndroidJUnit4
2121
import com.google.common.truth.Truth.assertThat
2222
import com.google.firebase.Firebase
23-
import com.google.firebase.FirebaseApp
2423
import com.google.firebase.FirebaseOptions
2524
import com.google.firebase.initialize
2625
import com.google.firebase.sessions.settings.SessionsSettings
27-
import org.junit.After
28-
import org.junit.Before
26+
import org.junit.BeforeClass
2927
import org.junit.Test
3028
import org.junit.runner.RunWith
3129

@@ -36,23 +34,6 @@ import org.junit.runner.RunWith
3634
*/
3735
@RunWith(AndroidJUnit4::class)
3836
class FirebaseSessionsTests {
39-
@Before
40-
fun setUp() {
41-
Firebase.initialize(
42-
ApplicationProvider.getApplicationContext(),
43-
FirebaseOptions.Builder()
44-
.setApplicationId(APP_ID)
45-
.setApiKey(API_KEY)
46-
.setProjectId(PROJECT_ID)
47-
.build()
48-
)
49-
}
50-
51-
@After
52-
fun cleanUp() {
53-
FirebaseApp.clearInstancesForTest()
54-
}
55-
5637
@Test
5738
fun firebaseSessionsDoesInitialize() {
5839
assertThat(FirebaseSessions.instance).isNotNull()
@@ -69,5 +50,18 @@ class FirebaseSessionsTests {
6950
private const val APP_ID = "1:1:android:1a"
7051
private const val API_KEY = "API-KEY-API-KEY-API-KEY-API-KEY-API-KEY"
7152
private const val PROJECT_ID = "PROJECT-ID"
53+
54+
@BeforeClass
55+
@JvmStatic
56+
fun setUp() {
57+
Firebase.initialize(
58+
ApplicationProvider.getApplicationContext(),
59+
FirebaseOptions.Builder()
60+
.setApplicationId(APP_ID)
61+
.setApiKey(API_KEY)
62+
.setProjectId(PROJECT_ID)
63+
.build(),
64+
)
65+
}
7266
}
7367
}

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ import com.google.firebase.installations.FirebaseInstallationsApi
2323
import com.google.firebase.sessions.ApplicationInfo
2424
import com.google.firebase.sessions.InstallationId
2525
import com.google.firebase.sessions.TimeProvider
26-
import dagger.Lazy
2726
import javax.inject.Inject
2827
import javax.inject.Singleton
2928
import kotlin.time.Duration
3029
import kotlin.time.Duration.Companion.hours
3130
import kotlin.time.Duration.Companion.seconds
32-
import kotlinx.coroutines.runBlocking
3331
import kotlinx.coroutines.sync.Mutex
3432
import kotlinx.coroutines.sync.withLock
3533
import org.json.JSONException
@@ -43,11 +41,8 @@ constructor(
4341
private val firebaseInstallationsApi: FirebaseInstallationsApi,
4442
private val appInfo: ApplicationInfo,
4543
private val configsFetcher: CrashlyticsSettingsFetcher,
46-
private val lazySettingsCache: Lazy<SettingsCache>,
44+
private val settingsCache: SettingsCache,
4745
) : SettingsProvider {
48-
private val settingsCache: SettingsCache
49-
get() = lazySettingsCache.get()
50-
5146
private val fetchInProgress = Mutex()
5247

5348
override val sessionEnabled: Boolean?
@@ -133,7 +128,7 @@ constructor(
133128
sessionTimeoutSeconds = sessionTimeoutSeconds,
134129
sessionSamplingRate = sessionSamplingRate,
135130
cacheDurationSeconds = cacheDuration ?: defaultCacheDuration,
136-
cacheUpdatedTimeMs = timeProvider.currentTime().ms,
131+
cacheUpdatedTimeSeconds = timeProvider.currentTime().seconds,
137132
)
138133
)
139134
},
@@ -148,7 +143,7 @@ constructor(
148143
override fun isSettingsStale(): Boolean = settingsCache.hasCacheExpired()
149144

150145
@VisibleForTesting
151-
internal fun clearCachedSettings() = runBlocking {
146+
internal suspend fun clearCachedSettings() {
152147
settingsCache.updateConfigs(SessionConfigsSerializer.defaultValue)
153148
}
154149

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ internal data class SessionConfigs(
3030
val sessionSamplingRate: Double?,
3131
val sessionTimeoutSeconds: Int?,
3232
val cacheDurationSeconds: Int?,
33-
val cacheUpdatedTimeMs: Long?,
33+
val cacheUpdatedTimeSeconds: Long?,
3434
)
3535

3636
/** DataStore json [Serializer] for [SessionConfigs]. */
@@ -41,7 +41,7 @@ internal object SessionConfigsSerializer : Serializer<SessionConfigs> {
4141
sessionSamplingRate = null,
4242
sessionTimeoutSeconds = null,
4343
cacheDurationSeconds = null,
44-
cacheUpdatedTimeMs = null,
44+
cacheUpdatedTimeSeconds = null,
4545
)
4646

4747
override suspend fun readFrom(input: InputStream): SessionConfigs =

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@
1717
package com.google.firebase.sessions.settings
1818

1919
import android.util.Log
20+
import androidx.annotation.VisibleForTesting
2021
import androidx.datastore.core.DataStore
22+
import com.google.firebase.annotations.concurrent.Background
2123
import com.google.firebase.sessions.TimeProvider
2224
import java.io.IOException
25+
import java.util.concurrent.atomic.AtomicReference
2326
import javax.inject.Inject
2427
import javax.inject.Singleton
28+
import kotlin.coroutines.CoroutineContext
29+
import kotlinx.coroutines.CoroutineScope
2530
import kotlinx.coroutines.flow.first
31+
import kotlinx.coroutines.launch
2632
import kotlinx.coroutines.runBlocking
2733

2834
internal interface SettingsCache {
@@ -41,23 +47,38 @@ internal interface SettingsCache {
4147
internal class SettingsCacheImpl
4248
@Inject
4349
constructor(
50+
@Background private val backgroundDispatcher: CoroutineContext,
4451
private val timeProvider: TimeProvider,
4552
private val sessionConfigsDataStore: DataStore<SessionConfigs>,
4653
) : SettingsCache {
47-
private var sessionConfigs: SessionConfigs
54+
private val sessionConfigsAtomicReference = AtomicReference<SessionConfigs>()
55+
56+
private val sessionConfigs: SessionConfigs
57+
get() {
58+
// Ensure configs are loaded from disk before the first access
59+
if (sessionConfigsAtomicReference.get() == null) {
60+
// Double check to avoid the `runBlocking` unless necessary
61+
sessionConfigsAtomicReference.compareAndSet(
62+
null,
63+
runBlocking { sessionConfigsDataStore.data.first() },
64+
)
65+
}
66+
67+
return sessionConfigsAtomicReference.get()
68+
}
4869

4970
init {
50-
// Block until the cache is loaded from disk to ensure cache
51-
// values are valid and readable from the main thread on init.
52-
runBlocking { sessionConfigs = sessionConfigsDataStore.data.first() }
71+
CoroutineScope(backgroundDispatcher).launch {
72+
sessionConfigsDataStore.data.collect(sessionConfigsAtomicReference::set)
73+
}
5374
}
5475

5576
override fun hasCacheExpired(): Boolean {
56-
val cacheUpdatedTimeMs = sessionConfigs.cacheUpdatedTimeMs
77+
val cacheUpdatedTimeSeconds = sessionConfigs.cacheUpdatedTimeSeconds
5778
val cacheDurationSeconds = sessionConfigs.cacheDurationSeconds
5879

59-
if (cacheUpdatedTimeMs != null && cacheDurationSeconds != null) {
60-
val timeDifferenceSeconds = (timeProvider.currentTime().ms - cacheUpdatedTimeMs) / 1000
80+
if (cacheUpdatedTimeSeconds != null && cacheDurationSeconds != null) {
81+
val timeDifferenceSeconds = timeProvider.currentTime().seconds - cacheUpdatedTimeSeconds
6182
if (timeDifferenceSeconds < cacheDurationSeconds) {
6283
return false
6384
}
@@ -74,12 +95,12 @@ constructor(
7495
override suspend fun updateConfigs(sessionConfigs: SessionConfigs) {
7596
try {
7697
sessionConfigsDataStore.updateData { sessionConfigs }
77-
this.sessionConfigs = sessionConfigs
7898
} catch (ex: IOException) {
7999
Log.w(TAG, "Failed to update config values: $ex")
80100
}
81101
}
82102

103+
@VisibleForTesting
83104
internal suspend fun removeConfigs() =
84105
try {
85106
sessionConfigsDataStore.updateData { SessionConfigsSerializer.defaultValue }

firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class SessionDatastoreTest {
4444
DataStoreFactory.create(
4545
serializer = SessionDataSerializer,
4646
scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
47-
produceFile = { appContext.dataStoreFile("sessionDataTestDataStore.data") },
47+
produceFile = { appContext.dataStoreFile("sessionDataStore.data") },
4848
),
4949
)
5050

0 commit comments

Comments
 (0)