Skip to content

Commit

Permalink
Make all IO async and use IO dispatcher (#72)
Browse files Browse the repository at this point in the history
* Make all IO async and use IO dispatcher

* fix async vs sync init test

* Use dispatcher provider and mock all dispatchers

* cleanup
  • Loading branch information
tore-statsig committed Nov 29, 2022
1 parent b51c3c5 commit a8642b1
Show file tree
Hide file tree
Showing 17 changed files with 454 additions and 325 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.statsig.androidsdk

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

data class CoroutineDispatcherProvider(
val main: CoroutineDispatcher = Dispatchers.Main,
val default: CoroutineDispatcher = Dispatchers.Default,
val io: CoroutineDispatcher = Dispatchers.IO
)
12 changes: 6 additions & 6 deletions src/main/java/com/statsig/androidsdk/Statsig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ object Statsig {
@JvmStatic
fun getStableID(): String {
enforceInitialized("getStableID")
var result = ""
var result: String = ""
errorBoundary.capture({
result = client.getStableID()
})
Expand All @@ -357,7 +357,7 @@ object Statsig {
@JvmStatic
fun overrideGate(gateName: String, value: Boolean) {
errorBoundary.capture({
client.getStore().overrideGate(gateName, value)
client.overrideGate(gateName, value)
})
}

Expand All @@ -368,7 +368,7 @@ object Statsig {
@JvmStatic
fun overrideConfig(configName: String, value: Map<String, Any>) {
errorBoundary.capture({
client.getStore().overrideConfig(configName, value)
client.overrideConfig(configName, value)
})
}

Expand All @@ -379,7 +379,7 @@ object Statsig {
@JvmStatic
fun overrideLayer(layerName: String, value: Map<String, Any>) {
errorBoundary.capture({
client.getStore().overrideLayer(layerName, value)
client.overrideLayer(layerName, value)
})
}

Expand All @@ -389,7 +389,7 @@ object Statsig {
@JvmStatic
fun removeOverride(name: String) {
errorBoundary.capture({
client.getStore().removeOverride(name)
client.removeOverride(name)
})
}

Expand All @@ -399,7 +399,7 @@ object Statsig {
@JvmStatic
fun removeAllOverrides() {
errorBoundary.capture({
client.getStore().removeAllOverrides()
client.removeAllOverrides()
})
}

Expand Down
143 changes: 101 additions & 42 deletions src/main/java/com/statsig/androidsdk/StatsigClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ internal class StatsigClient() {

private var pollingJob: Job? = null
private val statsigJob = SupervisorJob()
private val statsigScope = CoroutineScope(statsigJob + Dispatchers.Main)
private val dispatcherProvider = CoroutineDispatcherProvider()
private val statsigScope = CoroutineScope(statsigJob + dispatcherProvider.main)

@VisibleForTesting
internal var statsigNetwork: StatsigNetwork = StatsigNetwork()
Expand All @@ -47,11 +48,11 @@ internal class StatsigClient() {
callback: IStatsigCallback? = null,
options: StatsigOptions = StatsigOptions(),
) {
setup(application, sdkKey, user, options)
val user = setup(application, sdkKey, user, options)
statsigScope.launch {
setupAsync()
setupAsync(user)
// The scope's dispatcher may change in the future. This "withContext" will ensure we keep true to the documentation above.
withContext(Dispatchers.Main.immediate) {
withContext(dispatcherProvider.main) {
callback?.onStatsigInitialize()
}
}
Expand All @@ -75,23 +76,29 @@ internal class StatsigClient() {
user: StatsigUser? = null,
options: StatsigOptions = StatsigOptions(),
) {
setup(application, sdkKey, user, options)
setupAsync()
val user = setup(application, sdkKey, user, options)
setupAsync(user)
}

private suspend fun setupAsync() {
withContext(Dispatchers.Main.immediate) {
val cacheKey = this@StatsigClient.user.getCacheKey()
private suspend fun setupAsync(user: StatsigUser) {
withContext(dispatcherProvider.io) {
val stableID = getLocalStorageStableID()
if (this@StatsigClient.statsigMetadata.stableID == null) {
this@StatsigClient.statsigMetadata.overrideStableID(stableID)
}
this@StatsigClient.store.loadFromLocalStorage(user)

val initResponse = statsigNetwork.initialize(
this@StatsigClient.options.api,
this@StatsigClient.sdkKey,
this@StatsigClient.user,
user,
this@StatsigClient.statsigMetadata,
this@StatsigClient.options.initTimeoutMs,
this@StatsigClient.getSharedPrefs()
this@StatsigClient.getSharedPrefs(),
)

if (initResponse != null) {
val cacheKey = user.getCacheKey()
this@StatsigClient.store.save(initResponse, cacheKey)
}

Expand All @@ -106,17 +113,17 @@ internal class StatsigClient() {
sdkKey: String,
user: StatsigUser? = null,
options: StatsigOptions = StatsigOptions(),
) {
): StatsigUser {
if (!sdkKey.startsWith("client-") && !sdkKey.startsWith("test-")) {
throw IllegalArgumentException("Invalid SDK Key provided. You must provide a client SDK Key from the API Key page of your Statsig console")
}
this.application = application
this.sdkKey = sdkKey
this.options = options
this.user = normalizeUser(user)
val normalizedUser = normalizeUser(user)
this.user = normalizedUser

val stableID = getLocalStorageStableID()
statsigMetadata = StatsigMetadata(stableID)
statsigMetadata = StatsigMetadata()
Statsig.errorBoundary.setMetadata(statsigMetadata)
populateStatsigMetadata()

Expand All @@ -129,7 +136,8 @@ internal class StatsigClient() {
statsigMetadata,
statsigNetwork
)
store = Store(getSharedPrefs(), this.user)
store = Store(getSharedPrefs(), normalizedUser)
return normalizedUser
}

/**
Expand Down Expand Up @@ -180,13 +188,16 @@ internal class StatsigClient() {
fun getExperiment(experimentName: String, keepDeviceValue: Boolean = false): DynamicConfig {
enforceInitialized("getExperiment")
val res = store.getExperiment(experimentName, keepDeviceValue)
updateStickyValues()
logExposure(experimentName, res)
return res
}

fun getExperimentWithExposureLoggingDisabled(experimentName: String, keepDeviceValue: Boolean = false): DynamicConfig {
enforceInitialized("getExperimentWithExposureLoggingDisabled")
return store.getExperiment(experimentName, keepDeviceValue)
val exp = store.getExperiment(experimentName, keepDeviceValue)
updateStickyValues()
return exp
}

/**
Expand All @@ -199,12 +210,16 @@ internal class StatsigClient() {
*/
fun getLayer(layerName: String, keepDeviceValue: Boolean = false): Layer {
enforceInitialized("getLayer")
return store.getLayer(this, layerName, keepDeviceValue)
val layer = store.getLayer(this, layerName, keepDeviceValue)
updateStickyValues()
return layer
}

fun getLayerWithExposureLoggingDisabled(layerName: String, keepDeviceValue: Boolean = false): Layer {
enforceInitialized("getLayer")
return store.getLayer(null, layerName, keepDeviceValue)
val layer = store.getLayer(null, layerName, keepDeviceValue)
updateStickyValues()
return layer
}

internal fun logLayerParameterExposure(layer: Layer, parameterName: String) {
Expand Down Expand Up @@ -307,7 +322,7 @@ internal class StatsigClient() {
fun updateUserAsync(user: StatsigUser?, callback: IStatsigCallback? = null) {
statsigScope.launch {
updateUser(user)
withContext(Dispatchers.Main.immediate) {
withContext(dispatcherProvider.main) {
callback?.onStatsigUpdateUser()
}
}
Expand All @@ -321,25 +336,27 @@ internal class StatsigClient() {
* @throws IllegalStateException if the SDK has not been initialized
*/
suspend fun updateUser(user: StatsigUser?) {
enforceInitialized("updateUser")
logger.onUpdateUser()
pollingJob?.cancel()
this.user = normalizeUser(user)
store.loadAndResetForUser(this.user)
withContext(dispatcherProvider.io) {
enforceInitialized("updateUser")
logger.onUpdateUser()
pollingJob?.cancel()
this@StatsigClient.user = normalizeUser(user)
store.loadAndResetForUser(this@StatsigClient.user)

val cacheKey = this.user.getCacheKey()
val initResponse = statsigNetwork.initialize(
options.api,
sdkKey,
this.user,
statsigMetadata,
options.initTimeoutMs,
getSharedPrefs(),
)
if (initResponse != null) {
store.save(initResponse, cacheKey)
val cacheKey = this@StatsigClient.user.getCacheKey()
val initResponse = statsigNetwork.initialize(
options.api,
sdkKey,
this@StatsigClient.user,
statsigMetadata,
options.initTimeoutMs,
getSharedPrefs(),
)
if (initResponse != null) {
store.save(initResponse, cacheKey)
}
pollForUpdates()
}
pollForUpdates()
}

suspend fun shutdownSuspend() {
Expand All @@ -354,17 +371,53 @@ internal class StatsigClient() {
*/
fun shutdown() {
runBlocking {
withContext(Dispatchers.Main.immediate) {
withContext(dispatcherProvider.main) {
shutdownSuspend()
}
}
}

fun overrideGate(gateName: String, value: Boolean) {
this.store.overrideGate(gateName, value)
statsigScope.launch {
this@StatsigClient.store.saveOverridesToLocalStorage()
}
}

fun overrideConfig(configName: String, value: Map<String, Any>) {
this.store.overrideConfig(configName, value)
statsigScope.launch {
this@StatsigClient.store.saveOverridesToLocalStorage()
}
}

fun overrideLayer(configName: String, value: Map<String, Any>) {
this.store.overrideLayer(configName, value)
statsigScope.launch {
this@StatsigClient.store.saveOverridesToLocalStorage()
}
}

fun removeOverride(name: String) {
this.store.removeOverride(name)
statsigScope.launch {
this@StatsigClient.store.saveOverridesToLocalStorage()
}
}

fun removeAllOverrides() {
this.store.removeAllOverrides()
statsigScope.launch {
this@StatsigClient.store.saveOverridesToLocalStorage()
}
}

/**
* @return the current Statsig stableID
* Null prior to completion of async initialization
*/
fun getStableID(): String {
return statsigMetadata.stableID
return statsigMetadata.stableID ?: statsigMetadata.stableID!!
}

internal fun getStore(): Store {
Expand All @@ -377,13 +430,19 @@ internal class StatsigClient() {
}
}

private fun updateStickyValues() {
statsigScope.launch {
store.persistStickyValues()
}
}

private fun logExposure(name: String, gate: FeatureGate) {
statsigScope.launch {
logger.logExposure(name, gate, user)
}
}

private fun getLocalStorageStableID(): String {
private suspend fun getLocalStorageStableID(): String {
var stableID = this@StatsigClient.getSharedPrefs().getString(STABLE_ID_KEY, null)
if (stableID == null) {
stableID = UUID.randomUUID().toString()
Expand Down Expand Up @@ -456,11 +515,11 @@ internal class StatsigClient() {
return application.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
}

internal fun saveStringToSharedPrefs(key: String, value: String) {
internal suspend fun saveStringToSharedPrefs(key: String, value: String) {
StatsigUtil.saveStringToSharedPrefs(getSharedPrefs(), key, value)
}

internal fun removeFromSharedPrefs(key: String) {
internal suspend fun removeFromSharedPrefs(key: String) {
StatsigUtil.removeFromSharedPrefs(getSharedPrefs(), key)
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/statsig/androidsdk/StatsigMetadata.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
import java.util.*

internal data class StatsigMetadata(
@SerializedName("stableID") var stableID: String,
@SerializedName("stableID") var stableID: String? = null,
@SerializedName("appIdentifier") var appIdentifier: String? = null,
@SerializedName("appVersion") var appVersion: String? = null,
@SerializedName("deviceModel") var deviceModel: String? = Build.MODEL,
Expand Down
Loading

0 comments on commit a8642b1

Please sign in to comment.