Skip to content

Introduce remote config cache for storing configs fetched remotely. #4917

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

Merged
merged 4 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion firebase-sessions/firebase-sessions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ firebaseLibrary {

android {
val targetSdkVersion: Int by rootProject
compileSdk = targetSdkVersion
compileSdk = 33
defaultConfig {
minSdk = 16
targetSdk = targetSdkVersion
Expand All @@ -48,6 +48,7 @@ dependencies {
implementation("com.google.firebase:firebase-encoders:17.0.0")
implementation("com.google.firebase:firebase-installations-interop:17.1.0")
implementation("com.google.android.datatransport:transport-api:3.0.0")
implementation ("androidx.datastore:datastore-preferences:1.0.0")
implementation(libs.androidx.annotation)

runtimeOnly("com.google.firebase:firebase-installations:17.1.3")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,32 @@ internal class LocalOverrideSettings(val context: Context) : SettingsProvider {

override val sessionEnabled: Boolean?
get() {
if (metadata != null && metadata.containsKey(sessions_metadata_flag_sessionsEnabled)) {
return metadata.getBoolean(sessions_metadata_flag_sessionsEnabled)
metadata?.let {
if (it.containsKey(sessions_metadata_flag_sessionsEnabled)) {
return it.getBoolean(sessions_metadata_flag_sessionsEnabled)
}
}
return null
}

override val sessionRestartTimeout: Duration?
get() {
if (metadata != null && metadata.containsKey(sessions_metadata_flag_sessionRestartTimeout)) {
val timeoutInSeconds = metadata.getInt(sessions_metadata_flag_sessionRestartTimeout)
val duration = timeoutInSeconds!!.toDuration(DurationUnit.SECONDS)
return duration
metadata?.let {
if (it.containsKey(sessions_metadata_flag_sessionRestartTimeout)) {
val timeoutInSeconds = it.getInt(sessions_metadata_flag_sessionRestartTimeout)
val duration = timeoutInSeconds.toDuration(DurationUnit.SECONDS)
return duration
}
}
return null
}

override val samplingRate: Double?
get() {
if (metadata != null && metadata.containsKey(sessions_metadata_flag_samplingRate)) {
return metadata.getDouble(sessions_metadata_flag_samplingRate)
metadata?.let {
if (it.containsKey(sessions_metadata_flag_samplingRate)) {
return it.getDouble(sessions_metadata_flag_samplingRate)
}
}
return null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.sessions.settings

import android.content.Context
import android.net.Uri
import androidx.datastore.preferences.preferencesDataStore
import java.net.URL
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

internal class RemoteSettings(val context: Context) : SettingsProvider {
private val Context.dataStore by preferencesDataStore(name = SESSION_CONFIGS_NAME)
private val settingsCache = SettingsCache(context.dataStore)

override val sessionEnabled: Boolean?
get() {
return settingsCache.sessionsEnabled()
}

override val sessionRestartTimeout: Duration?
get() {
val durationInSeconds = settingsCache.sessionRestartTimeout()
if (durationInSeconds != null) {
return durationInSeconds.toLong().seconds
}
return null
}

override val samplingRate: Double?
get() {
return settingsCache.sessionSamplingRate()
}

override fun updateSettings() {
fetchConfigs()
}

override fun isSettingsStale(): Boolean {
return settingsCache.hasCacheExpired()
}

companion object SettingsFetcher {
private const val SESSION_CONFIGS_NAME = "firebase_session_settings"
private const val FIREBASE_SESSIONS_BASE_URL_STRING =
"https://firebase-settings.crashlytics.com"
private const val FIREBASE_PLATFORM = "android"
private const val fetchInProgress = false
private val settingsUrl: URL = run {
var uri =
Uri.Builder()
.scheme("https")
.authority(FIREBASE_SESSIONS_BASE_URL_STRING)
.appendPath("spi/v2/platforms")
.appendPath(FIREBASE_PLATFORM)
.appendPath("gmp")
// TODO(visum) Replace below with the GMP APPId
.appendPath("GMP_APP_ID")
.appendPath("settings")
.appendQueryParameter("build_version", "")
.appendQueryParameter("display_version", "")

URL(uri.build().toString())
}

fun fetchConfigs() {
// Check if a fetch is in progress. If yes, return
if (fetchInProgress) {
return
}

// Check if cache is expired. If not, return
// Initiate a fetch. On successful response cache the fetched values

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import kotlin.time.Duration.Companion.minutes
*/
internal class SessionsSettings(val context: Context) {

var localOverrideSettings = LocalOverrideSettings(context)
private var localOverrideSettings = LocalOverrideSettings(context)
private var remoteSettings = RemoteSettings(context)

// Order of preference for all the configs below:
// 1. Honor local overrides
Expand All @@ -37,40 +38,46 @@ internal class SessionsSettings(val context: Context) {
// Setting to qualify if sessions service is enabled.
val sessionsEnabled: Boolean
get() {
if (localOverrideSettings.sessionEnabled != null) {
return localOverrideSettings.sessionEnabled!!
localOverrideSettings.sessionEnabled?.let {
return it
}
remoteSettings.sessionEnabled?.let {
return it
}

// SDK Default
return true
}

// Setting that provides the sessions sampling rate.
val samplingRate: Double
get() {
if (localOverrideSettings.samplingRate != null) {
return localOverrideSettings.samplingRate!!
localOverrideSettings.samplingRate?.let {
return it
}
remoteSettings.samplingRate?.let {
return it
}

// SDK Default
return 1.0
}

// Background timeout config value before which a new session is generated
val sessionRestartTimeout: Duration
get() {
if (localOverrideSettings.sessionRestartTimeout != null) {
return localOverrideSettings.sessionRestartTimeout!!
localOverrideSettings.sessionRestartTimeout?.let {
return it
}
remoteSettings.sessionRestartTimeout?.let {
return it
}

// SDK Default
return 30.minutes
}

// Update the settings for all the settings providers
fun updateSettings() {
// Placeholder to initiate settings update on different sources
// Pending sources: RemoteSettings
localOverrideSettings.updateSettings()
remoteSettings.updateSettings()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.sessions.settings

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import kotlinx.coroutines.flow.first

internal data class SessionConfigs(
val sessionEnabled: Boolean? = null,
val sessionSamplingRate: Double? = null,
val cacheDuration: Int? = null,
val sessionRestartTimeout: Int? = null,
val cacheUpdatedTime: Long? = null
)

internal class SettingsCache(private val store: DataStore<Preferences>) {
private var sessionConfigs = SessionConfigs()

private object SettingsCacheKeys {
val SETTINGS_CACHE_SESSIONS_ENABLED = booleanPreferencesKey("firebase_sessions_enabled")
val SETTINGS_CACHE_SAMPLING_RATE = doublePreferencesKey("firebase_sessions_sampling_rate")
val SETTINGS_CACHE_SESSIONS_RESTART_TIMEOUT_SECONDS =
intPreferencesKey("firebase_sessions_restart_timeout")
val SETTINGS_CACHE_SESSIONS_CACHE_DURATION_SECONDS =
intPreferencesKey("firebase_sessions_cache_duration")
val SETTINGS_CACHE_SESSIONS_CACHE_UPDATED_TIME =
longPreferencesKey("firebase_sessions_cache_updated_time")
}

private suspend fun updateSessionConfigs() = mapSessionConfigs(store.data.first().toPreferences())

private fun mapSessionConfigs(settings: Preferences): SessionConfigs {
val sessionEnabled = settings[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_ENABLED]
val sessionSamplingRate = settings[SettingsCacheKeys.SETTINGS_CACHE_SAMPLING_RATE]
val sessionRestartTimeout =
settings[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_RESTART_TIMEOUT_SECONDS]
val cacheDuration = settings[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_DURATION_SECONDS]
val cacheUpdatedTime = settings[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_UPDATED_TIME]

sessionConfigs =
SessionConfigs(
sessionEnabled = sessionEnabled,
sessionSamplingRate = sessionSamplingRate,
sessionRestartTimeout = sessionRestartTimeout,
cacheDuration = cacheDuration,
cacheUpdatedTime = cacheUpdatedTime
)
return sessionConfigs
}

internal fun hasCacheExpired(): Boolean {
if (sessionConfigs.cacheUpdatedTime != null) {
val currentTimestamp = System.currentTimeMillis()
val timeDifferenceSeconds = (currentTimestamp - sessionConfigs.cacheUpdatedTime!!) / 1000
if (timeDifferenceSeconds < sessionConfigs.cacheDuration!!) return false
}
return true
}

fun sessionsEnabled(): Boolean? = sessionConfigs.sessionEnabled

fun sessionSamplingRate(): Double? = sessionConfigs.sessionSamplingRate

fun sessionRestartTimeout(): Int? = sessionConfigs.sessionRestartTimeout

suspend fun updateSettingsEnabled(enabled: Boolean?) {
store.edit { preferences ->
enabled?.run { preferences[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_ENABLED] = enabled }
?: run { preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_ENABLED) }
}
updateSessionConfigs()
}

suspend fun updateSamplingRate(rate: Double?) {
store.edit { preferences ->
rate?.run { preferences[SettingsCacheKeys.SETTINGS_CACHE_SAMPLING_RATE] = rate }
?: run { preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SAMPLING_RATE) }
}
updateSessionConfigs()
}

suspend fun updateSessionRestartTimeout(timeoutInSeconds: Int?) {
store.edit { preferences ->
timeoutInSeconds?.run {
preferences[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_RESTART_TIMEOUT_SECONDS] =
timeoutInSeconds
}
?: run {
preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_RESTART_TIMEOUT_SECONDS)
}
}
updateSessionConfigs()
}

suspend fun updateSessionCacheDuration(cacheDurationInSeconds: Int?) {
store.edit { preferences ->
cacheDurationInSeconds?.run {
preferences[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_DURATION_SECONDS] =
cacheDurationInSeconds
}
?: run {
preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_DURATION_SECONDS)
}
}
updateSessionConfigs()
}

suspend fun updateSessionCacheUpdatedTime(cacheUpdatedTime: Long?) {
store.edit { preferences ->
cacheUpdatedTime?.run {
preferences[SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_UPDATED_TIME] = cacheUpdatedTime
}
?: run { preferences.remove(SettingsCacheKeys.SETTINGS_CACHE_SESSIONS_CACHE_UPDATED_TIME) }
}
updateSessionConfigs()
}

suspend fun removeConfigs() {
store.edit { preferences ->
preferences.clear()
updateSessionConfigs()
}
}
}
Loading