-
Notifications
You must be signed in to change notification settings - Fork 6
feat: use cached config if avail on init with openfeature provider #287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d52c004
76a7189
be70b10
cdabaf0
1fb5e0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -77,19 +77,19 @@ class DevCycleClient private constructor( | |||||||||||||||||||||||||||||||||||||||
| private var latestIdentifiedUser: PopulatedUser = user | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private val variableInstanceMap: MutableMap<String, MutableMap<Any, WeakReference<Variable<*>>>> = mutableMapOf() | ||||||||||||||||||||||||||||||||||||||||
| private val configUpdatedCallbacks = java.util.concurrent.CopyOnWriteArrayList<DevCycleCallback<Map<String, BaseConfigVariable>>>() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| init { | ||||||||||||||||||||||||||||||||||||||||
| useCachedConfigForUser(user) | ||||||||||||||||||||||||||||||||||||||||
| val cacheHit = useCachedConfigForUser(user) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| initializeJob = coroutineScope.async(coroutineContext) { | ||||||||||||||||||||||||||||||||||||||||
| isExecuting.set(true) | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| fetchConfig(user) | ||||||||||||||||||||||||||||||||||||||||
| isInitialized.set(true) | ||||||||||||||||||||||||||||||||||||||||
| withContext(Dispatchers.IO){ | ||||||||||||||||||||||||||||||||||||||||
| initEventSource() | ||||||||||||||||||||||||||||||||||||||||
| val application : Application = context.applicationContext as Application | ||||||||||||||||||||||||||||||||||||||||
| if (cacheHit) { | ||||||||||||||||||||||||||||||||||||||||
| isInitialized.set(true) | ||||||||||||||||||||||||||||||||||||||||
| initializeJob = CompletableDeferred(Unit) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| coroutineScope.launch(coroutineContext) { | ||||||||||||||||||||||||||||||||||||||||
| withContext(Dispatchers.IO) { | ||||||||||||||||||||||||||||||||||||||||
| initEventSource() | ||||||||||||||||||||||||||||||||||||||||
| val application: Application = context.applicationContext as Application | ||||||||||||||||||||||||||||||||||||||||
| val lifecycleCallbacks = DVCLifecycleCallbacks( | ||||||||||||||||||||||||||||||||||||||||
| onPauseApplication, | ||||||||||||||||||||||||||||||||||||||||
| onResumeApplication, | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -98,17 +98,38 @@ class DevCycleClient private constructor( | |||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| application.registerActivityLifecycleCallbacks(lifecycleCallbacks) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| } catch (t: Throwable) { | ||||||||||||||||||||||||||||||||||||||||
| DevCycleLogger.e(t, "DevCycle SDK Failed to Initialize!") | ||||||||||||||||||||||||||||||||||||||||
| throw t | ||||||||||||||||||||||||||||||||||||||||
| // Fetch fresh config in background (ADR 0009). SSE only fires on | ||||||||||||||||||||||||||||||||||||||||
| // server-side changes, so an explicit fetch is needed to verify the cache. | ||||||||||||||||||||||||||||||||||||||||
| performBackgroundRefresh() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| initializeJob = coroutineScope.async(coroutineContext) { | ||||||||||||||||||||||||||||||||||||||||
| isExecuting.set(true) | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| fetchConfig(user) | ||||||||||||||||||||||||||||||||||||||||
| isInitialized.set(true) | ||||||||||||||||||||||||||||||||||||||||
| withContext(Dispatchers.IO) { | ||||||||||||||||||||||||||||||||||||||||
| initEventSource() | ||||||||||||||||||||||||||||||||||||||||
| val application: Application = context.applicationContext as Application | ||||||||||||||||||||||||||||||||||||||||
| val lifecycleCallbacks = DVCLifecycleCallbacks( | ||||||||||||||||||||||||||||||||||||||||
| onPauseApplication, | ||||||||||||||||||||||||||||||||||||||||
| onResumeApplication, | ||||||||||||||||||||||||||||||||||||||||
| config?.sse?.inactivityDelay?.toLong(), | ||||||||||||||||||||||||||||||||||||||||
| customLifecycleHandler | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| application.registerActivityLifecycleCallbacks(lifecycleCallbacks) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } catch (t: Throwable) { | ||||||||||||||||||||||||||||||||||||||||
| DevCycleLogger.e(t, "DevCycle SDK Failed to Initialize!") | ||||||||||||||||||||||||||||||||||||||||
| throw t | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| initializeJob.invokeOnCompletion { | ||||||||||||||||||||||||||||||||||||||||
| coroutineScope.launch(coroutineContext) { | ||||||||||||||||||||||||||||||||||||||||
| handleQueuedConfigRequests() | ||||||||||||||||||||||||||||||||||||||||
| isExecuting.set(false) | ||||||||||||||||||||||||||||||||||||||||
| initializeJob.invokeOnCompletion { | ||||||||||||||||||||||||||||||||||||||||
| coroutineScope.launch(coroutineContext) { | ||||||||||||||||||||||||||||||||||||||||
| handleQueuedConfigRequests() | ||||||||||||||||||||||||||||||||||||||||
| isExecuting.set(false) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -193,6 +214,8 @@ class DevCycleClient private constructor( | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| internal fun hasUsableCachedConfig(): Boolean = config != null && isConfigCached.get() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Updates or builds a new User and fetches the latest config for that User | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -233,12 +256,21 @@ class DevCycleClient private constructor( | |||||||||||||||||||||||||||||||||||||||
| override fun onError(error: Throwable) { | ||||||||||||||||||||||||||||||||||||||||
| DevCycleLogger.d("Error fetching config for user_id %s: %s", updatedUser.userId, error.message) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (error is DVCRequestException && (error.isAuthError || error.statusCode == 400)) { | ||||||||||||||||||||||||||||||||||||||||
| dvcSharedPrefs.clearConfigForUser(updatedUser) | ||||||||||||||||||||||||||||||||||||||||
| DevCycleLogger.w("Config error during identifyUser (${error.statusCode}). Persisted cache cleared.") | ||||||||||||||||||||||||||||||||||||||||
| latestIdentifiedUser = previousUser | ||||||||||||||||||||||||||||||||||||||||
| callback?.onError(error) | ||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
luxscious marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // In the event that the config request fails (i.e. the device is offline) | ||||||||||||||||||||||||||||||||||||||||
| // Fallback to using a Cached Configuration for the User if available | ||||||||||||||||||||||||||||||||||||||||
| val hasCachedConfig = tryLoadCachedConfigForUser(updatedUser) | ||||||||||||||||||||||||||||||||||||||||
| if (hasCachedConfig) { | ||||||||||||||||||||||||||||||||||||||||
| // Successfully used cached config, return success | ||||||||||||||||||||||||||||||||||||||||
| config?.variables?.let { callback?.onSuccess(it) } | ||||||||||||||||||||||||||||||||||||||||
| performBackgroundRefresh() | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| // No cached config available, restore previous state and return error | ||||||||||||||||||||||||||||||||||||||||
| latestIdentifiedUser = previousUser | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -372,6 +404,11 @@ class DevCycleClient private constructor( | |||||||||||||||||||||||||||||||||||||||
| Variable.getAndValidateType(defaultValue) | ||||||||||||||||||||||||||||||||||||||||
| val variable = this.getCachedVariable(key, defaultValue) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| val currentEval = variable.eval | ||||||||||||||||||||||||||||||||||||||||
| if (isConfigCached.get() && currentEval != null) { | ||||||||||||||||||||||||||||||||||||||||
| variable.eval = EvalReason.withCachedSource(currentEval) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| val tmpConfig = config | ||||||||||||||||||||||||||||||||||||||||
| if(!disableAutomaticEventLogging){ | ||||||||||||||||||||||||||||||||||||||||
| val event: Event = Event.fromInternalEvent( | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -501,11 +538,15 @@ class DevCycleClient private constructor( | |||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||
| val result = request.getConfigJson(sdkKey, user, enableEdgeDB, sse, lastModified, etag) | ||||||||||||||||||||||||||||||||||||||||
| config = result | ||||||||||||||||||||||||||||||||||||||||
| observable.configUpdated(config) | ||||||||||||||||||||||||||||||||||||||||
| dvcSharedPrefs.saveConfig(result, user) | ||||||||||||||||||||||||||||||||||||||||
| isConfigCached.set(false) | ||||||||||||||||||||||||||||||||||||||||
| observable.configUpdated(config) | ||||||||||||||||||||||||||||||||||||||||
| DevCycleLogger.d("A new config has been fetched for $user") | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (isInitialized.get()) { | ||||||||||||||||||||||||||||||||||||||||
| notifyConfigUpdated(result.variables) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| this@DevCycleClient.user = user | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (checkIfEdgeDBEnabled(result, enableEdgeDB)) { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -582,14 +623,16 @@ class DevCycleClient private constructor( | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private fun useCachedConfigForUser(user: PopulatedUser) { | ||||||||||||||||||||||||||||||||||||||||
| private fun useCachedConfigForUser(user: PopulatedUser): Boolean { | ||||||||||||||||||||||||||||||||||||||||
| val cachedConfig = if (disableConfigCache) null else dvcSharedPrefs.getConfig(user) | ||||||||||||||||||||||||||||||||||||||||
| if (cachedConfig != null) { | ||||||||||||||||||||||||||||||||||||||||
| config = cachedConfig | ||||||||||||||||||||||||||||||||||||||||
| isConfigCached.set(true) | ||||||||||||||||||||||||||||||||||||||||
| DevCycleLogger.d("Loaded config from cache for user_id %s", user.userId) | ||||||||||||||||||||||||||||||||||||||||
| observable.configUpdated(config) | ||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private fun tryLoadCachedConfigForUser(user: PopulatedUser): Boolean { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -606,6 +649,56 @@ class DevCycleClient private constructor( | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| internal fun onConfigUpdated(callback: DevCycleCallback<Map<String, BaseConfigVariable>>) { | ||||||||||||||||||||||||||||||||||||||||
| configUpdatedCallbacks.add(callback) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private fun notifyConfigUpdated(variables: Map<String, BaseConfigVariable>?) { | ||||||||||||||||||||||||||||||||||||||||
| variables?.let { vars -> | ||||||||||||||||||||||||||||||||||||||||
| configUpdatedCallbacks.forEach { callback -> | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| callback.onSuccess(vars) | ||||||||||||||||||||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||||||||||||||||||||
| DevCycleLogger.e(e, "Error in config updated callback") | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private fun notifyConfigError(error: Throwable) { | ||||||||||||||||||||||||||||||||||||||||
| configUpdatedCallbacks.forEach { callback -> | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| callback.onError(error) | ||||||||||||||||||||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||||||||||||||||||||
| DevCycleLogger.e(e, "Error in config error callback") | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private fun performBackgroundRefresh() { | ||||||||||||||||||||||||||||||||||||||||
| val userAtRefreshStart = latestIdentifiedUser | ||||||||||||||||||||||||||||||||||||||||
| refetchConfig(false, null, null, object : DevCycleCallback<Map<String, BaseConfigVariable>> { | ||||||||||||||||||||||||||||||||||||||||
| override fun onSuccess(result: Map<String, BaseConfigVariable>) { | ||||||||||||||||||||||||||||||||||||||||
| DevCycleLogger.d("Background refresh succeeded") | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| override fun onError(error: Throwable) { | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+681
to
+685
|
||||||||||||||||||||||||||||||||||||||||
| override fun onSuccess(result: Map<String, BaseConfigVariable>) { | |
| DevCycleLogger.d("Background refresh succeeded") | |
| } | |
| override fun onError(error: Throwable) { | |
| override fun onSuccess(result: Map<String, BaseConfigVariable>) { | |
| if (latestIdentifiedUser != userAtRefreshStart) { | |
| DevCycleLogger.d("Background refresh completed after user changed; ignoring stale result") | |
| return | |
| } | |
| DevCycleLogger.d("Background refresh succeeded") | |
| } | |
| override fun onError(error: Throwable) { | |
| if (latestIdentifiedUser != userAtRefreshStart) { | |
| DevCycleLogger.w("Background refresh failed after user changed; skipping stale cache/error handling.") | |
| return | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is out of the scope of this PR. It's a pre-existing concurrency edge case in refetchConfig that exists independently of our changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description says a user switch "clears in-memory config immediately to prevent serving stale values for the wrong identity", but in
identifyUserthe existing in-memoryconfig(anduserused for evaluations/events) is kept until the async fetch completes. That means evaluations right afteridentifyUsercan still be served from the previous user's config. If the intent is to stop serving stale values immediately on user switch, consider clearing/invalidating the in-memory config state (or gating evaluations) at the start of the identify flow.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be fine, as we await for the config for the new identity to succeed before clearing cache