diff --git a/src/main/java/com/statsig/androidsdk/StatsigClient.kt b/src/main/java/com/statsig/androidsdk/StatsigClient.kt index afb067c..61418dc 100644 --- a/src/main/java/com/statsig/androidsdk/StatsigClient.kt +++ b/src/main/java/com/statsig/androidsdk/StatsigClient.kt @@ -347,7 +347,9 @@ internal class StatsigClient() { /** * Update the Statsig SDK with Feature Gate and Dynamic Configs for a new user, or the same - * user with additional properties + * user with additional properties. + * Will make network call in a separate coroutine. + * But fetch cached values from memory synchronously. * * @param user the updated user * @param callback a callback to invoke upon update completion. Before this callback is @@ -356,8 +358,9 @@ internal class StatsigClient() { * @throws IllegalStateException if the SDK has not been initialized */ fun updateUserAsync(user: StatsigUser?, callback: IStatsigCallback? = null) { + updateUserCache(user) statsigScope.launch { - updateUser(user) + updateUserImpl(user) withContext(dispatcherProvider.main) { callback?.onStatsigUpdateUser() } @@ -372,13 +375,23 @@ internal class StatsigClient() { * @throws IllegalStateException if the SDK has not been initialized */ suspend fun updateUser(user: StatsigUser?) { + updateUserCache(user) + updateUserImpl(user) + } + + private fun updateUserCache(user: StatsigUser?) { + Statsig.errorBoundary.capture({ + enforceInitialized("updateUser") + logger.onUpdateUser() + pollingJob?.cancel() + this@StatsigClient.user = normalizeUser(user) + store.loadAndResetForUser(this@StatsigClient.user) + }) + } + + private suspend fun updateUserImpl(user: StatsigUser?) { withContext(dispatcherProvider.io) { Statsig.errorBoundary.captureAsync { - enforceInitialized("updateUser") - logger.onUpdateUser() - pollingJob?.cancel() - this@StatsigClient.user = normalizeUser(user) - store.loadAndResetForUser(this@StatsigClient.user) val sinceTime = store.getLastUpdateTime(this@StatsigClient.user) val cacheKey = this@StatsigClient.user.getCacheKey() diff --git a/src/test/java/com/statsig/androidsdk/AsyncInitVsUpdateTest.kt b/src/test/java/com/statsig/androidsdk/AsyncInitVsUpdateTest.kt index 5a9b344..06e14d1 100644 --- a/src/test/java/com/statsig/androidsdk/AsyncInitVsUpdateTest.kt +++ b/src/test/java/com/statsig/androidsdk/AsyncInitVsUpdateTest.kt @@ -4,6 +4,7 @@ import android.app.Application import com.google.gson.Gson import io.mockk.coEvery import io.mockk.mockk +import io.mockk.spyk import kotlinx.coroutines.* import org.junit.Assert.assertEquals import org.junit.Before @@ -50,12 +51,24 @@ class AsyncInitVsUpdateTest { val user = thirdArg() getResponseForUser(user) } - Statsig.client = StatsigClient() + Statsig.client = spyk() + coEvery { + Statsig.client.updateUserAsync(any()) + } coAnswers { + delay(500) + callOriginal() + } + coEvery { + Statsig.client.updateUser(any()) + } coAnswers { + delay(500) + callOriginal() + } Statsig.client.statsigNetwork = network } @Test - fun testWithDefault() { + fun testNoCache() { val userA = StatsigUser("user-a") userA.customIDs = mapOf("workID" to "employee-a") @@ -78,14 +91,18 @@ class AsyncInitVsUpdateTest { Statsig.initializeAsync(app, "client-key", userA, callback) Statsig.updateUserAsync(userB, callback) - var value = Statsig.getConfig("a_config").getString("key", "default") + // Since updateUserAsync has been called, we void values for user_a + var config = Statsig.getConfig("a_config") + var value = config.getString("key", "default") assertEquals("default", value) + assertEquals(EvaluationReason.Uninitialized, config.getEvaluationDetails().reason) didInitializeUserA.await() - // Since updateUserAsync has been called, we void values for user_a - value = Statsig.getConfig("a_config").getString("key", "default") + config = Statsig.getConfig("a_config") + value = config.getString("key", "default") assertEquals("default", value) + assertEquals(EvaluationReason.Uninitialized, config.getEvaluationDetails().reason) didInitializeUserB.await() @@ -94,7 +111,7 @@ class AsyncInitVsUpdateTest { } @Test - fun testWithCache() { + fun testLoadFromCache() { val userA = StatsigUser("user-a") val userB = StatsigUser("user-b") @@ -120,7 +137,7 @@ class AsyncInitVsUpdateTest { "a_config!" to APIDynamicConfig( "a_config!", mutableMapOf( - "key" to "user_b_value", + "key" to "user_b_value_cache", ), "default", ), @@ -137,17 +154,49 @@ class AsyncInitVsUpdateTest { // Since updateUserAsync has been called, we void values for user_a and serve cache // values for user_b - var value = Statsig.getConfig("a_config").getString("key", "default") - assertEquals("user_b_value", value) + var config = Statsig.getConfig("a_config") + var value = config.getString("key", "default") + assertEquals("user_b_value_cache", value) + assertEquals(EvaluationReason.Cache, config.getEvaluationDetails().reason) didInitializeUserA.await() - value = Statsig.getConfig("a_config").getString("key", "default") - assertEquals("user_b_value", value) + config = Statsig.getConfig("a_config") + value = config.getString("key", "default") + assertEquals("user_b_value_cache", value) + assertEquals(EvaluationReason.Cache, config.getEvaluationDetails().reason) didInitializeUserB.await() - value = Statsig.getConfig("a_config").getString("key", "default") + config = Statsig.getConfig("a_config") + value = config.getString("key", "default") assertEquals("user_b_value", value) + assertEquals(EvaluationReason.Network, config.getEvaluationDetails().reason) + } + + @Test + fun testNoAwait() { + val userA = StatsigUser("user-a") + val userB = StatsigUser("user-b") + + val didInitializeUserA = CountDownLatch(1) + val callback = object : IStatsigCallback { + override fun onStatsigInitialize() { + didInitializeUserA.countDown() + } + + override fun onStatsigUpdateUser() {} + } + Statsig.initializeAsync(app, "client-key", userA, callback) + didInitializeUserA.await() + GlobalScope.async { + Statsig.updateUser(userB) + } + + // Calling updateUser without suspending will not guarantee synchronous load from cache + var config = Statsig.getConfig("a_config") + var value = config.getString("key", "default") + assertEquals("user_a_value", value) + assertEquals(EvaluationReason.Network, config.getEvaluationDetails().reason) } }