Skip to content

Commit

Permalink
allow updateUser to load from cache synchronously
Browse files Browse the repository at this point in the history
  • Loading branch information
kenny-statsig committed Jul 14, 2023
1 parent e6524eb commit 3e991dd
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 19 deletions.
27 changes: 20 additions & 7 deletions src/main/java/com/statsig/androidsdk/StatsigClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
}
Expand All @@ -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()
Expand Down
73 changes: 61 additions & 12 deletions src/test/java/com/statsig/androidsdk/AsyncInitVsUpdateTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,12 +51,24 @@ class AsyncInitVsUpdateTest {
val user = thirdArg<StatsigUser>()
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")

Expand All @@ -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()

Expand All @@ -94,7 +111,7 @@ class AsyncInitVsUpdateTest {
}

@Test
fun testWithCache() {
fun testLoadFromCache() {
val userA = StatsigUser("user-a")
val userB = StatsigUser("user-b")

Expand All @@ -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",
),
Expand All @@ -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)
}
}

0 comments on commit 3e991dd

Please sign in to comment.