Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class DevCycleClient private constructor(

private val configRequestQueue = ConcurrentLinkedQueue<UserAndCallback>()
private val configRequestMutex = Mutex()
private val defaultCacheTTL = 7 * 24 * 3600000L // 7 days
private val defaultCacheTTL = 30 * 24 * 3600000L // 30 days
private val configCacheTTL = options?.configCacheTTL ?: defaultCacheTTL
private val disableConfigCache = options?.disableConfigCache ?: false
private val disableRealtimeUpdates = options?.disableRealtimeUpdates ?: false
Expand All @@ -77,6 +77,11 @@ class DevCycleClient private constructor(
private val variableInstanceMap: MutableMap<String, MutableMap<Any, WeakReference<Variable<*>>>> = mutableMapOf()

init {
// Clean up old configs that may have exceeded TTL
if (!disableConfigCache) {
dvcSharedPrefs.cleanupOldConfigs(configCacheTTL)
}

val cachedConfig = if (disableConfigCache) null else dvcSharedPrefs.getConfig(user, configCacheTTL)
if (cachedConfig != null) {
config = cachedConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ internal class DVCSharedPrefs(context: Context) {
const val AnonUserIdKey = "ANONYMOUS_USER_ID"
const val IdentifiedConfigKey = "IDENTIFIED_CONFIG"
const val AnonymousConfigKey = "ANONYMOUS_CONFIG"
const val ConfigKeyPrefix = "USER_CONFIG_"
const val FetchDateSuffix = "_FETCH_DATE"
}

@Synchronized
Expand Down Expand Up @@ -65,12 +67,19 @@ internal class DVCSharedPrefs(context: Context) {
@Synchronized
fun saveConfig(configToSave: BucketedUserConfig, user: PopulatedUser) {
try {
val key = if (user.isAnonymous) AnonymousConfigKey else IdentifiedConfigKey
// Generate the user-specific key
val userKey = ConfigKeyPrefix + user.userId
val editor = preferences.edit()
val jsonString = JSONMapper.mapper.writeValueAsString(configToSave)
editor.putString(key, jsonString)
editor.putString("$key.USER_ID", user.userId)
editor.putLong("$key.FETCH_DATE", Calendar.getInstance().timeInMillis)
editor.putString(userKey, jsonString)
editor.putLong(userKey + FetchDateSuffix, Calendar.getInstance().timeInMillis)

// For backward compatibility, also save in the old format
val legacyKey = if (user.isAnonymous) AnonymousConfigKey else IdentifiedConfigKey
editor.putString(legacyKey, jsonString)
editor.putString("$legacyKey.USER_ID", user.userId)
editor.putLong("$legacyKey.FETCH_DATE", Calendar.getInstance().timeInMillis)

editor.apply()
} catch (e: JsonProcessingException) {
DevCycleLogger.e(e, e.message)
Expand All @@ -80,31 +89,88 @@ internal class DVCSharedPrefs(context: Context) {
@Synchronized
fun getConfig(user: PopulatedUser, ttlMs: Long): BucketedUserConfig? {
try {
val key = if (user.isAnonymous) AnonymousConfigKey else IdentifiedConfigKey
val userId = preferences.getString("$key.USER_ID", null)
val fetchDateMs = preferences.getLong("$key.FETCH_DATE", 0)
// Try to fetch config using the user-specific key first
val userKey = ConfigKeyPrefix + user.userId
val userConfigString = preferences.getString(userKey, null)
val userFetchDateMs = preferences.getLong(userKey + FetchDateSuffix, 0)

val oldestValidDateMs = Calendar.getInstance().timeInMillis - ttlMs

// If we found a valid user-specific config, use it
if (userConfigString != null && userFetchDateMs >= oldestValidDateMs) {
DevCycleLogger.d("Loaded config from cache for user ID ${user.userId}")
return JSONMapper.mapper.readValue(userConfigString)
}

// Fall back to the legacy approach for backward compatibility
val legacyKey = if (user.isAnonymous) AnonymousConfigKey else IdentifiedConfigKey
val userId = preferences.getString("$legacyKey.USER_ID", null)
val fetchDateMs = preferences.getLong("$legacyKey.FETCH_DATE", 0)

if (userId != user.userId) {
DevCycleLogger.d("Skipping cached config: no config for user ID ${user.userId}")
return null
}

val oldestValidDateMs = Calendar.getInstance().timeInMillis - ttlMs
if (fetchDateMs < oldestValidDateMs) {
DevCycleLogger.d("Skipping cached config: last fetched date is too old")
return null
}

val configString = preferences.getString(key, null)
val configString = preferences.getString(legacyKey, null)
if (configString == null) {
DevCycleLogger.d("Skipping cached config: no config found")
return null
}


DevCycleLogger.d("Loaded legacy config from cache for user ID ${user.userId}")
return JSONMapper.mapper.readValue(configString)
} catch (e: JsonProcessingException) {
DevCycleLogger.e(e, e.message)
return null
}
}

/**
* Cleans up old configs that exceed the TTL
* This is an internal method that can be called periodically
*/
@Synchronized
fun cleanupOldConfigs(ttlMs: Long) {
try {
val allPrefs = preferences.all
val currentTime = Calendar.getInstance().timeInMillis
val oldestValidDateMs = currentTime - ttlMs
val editor = preferences.edit()

// Find and remove expired user-specific configs
allPrefs.keys
.filter { it.startsWith(ConfigKeyPrefix) && !it.endsWith(FetchDateSuffix) }
.forEach { key ->
val fetchDateKey = key + FetchDateSuffix
val fetchDate = preferences.getLong(fetchDateKey, 0)
if (fetchDate < oldestValidDateMs) {
editor.remove(key)
editor.remove(fetchDateKey)
DevCycleLogger.d("Removed expired config for key: $key")
}
}

// Check legacy keys as well
val legacyKeys = listOf(IdentifiedConfigKey, AnonymousConfigKey)
for (key in legacyKeys) {
val fetchDateMs = preferences.getLong("$key.FETCH_DATE", 0)
if (fetchDateMs < oldestValidDateMs) {
editor.remove(key)
editor.remove("$key.USER_ID")
editor.remove("$key.FETCH_DATE")
DevCycleLogger.d("Removed expired legacy config for key: $key")
}
}

editor.apply()
} catch (e: Exception) {
DevCycleLogger.e(e, "Error cleaning up old configs: ${e.message}")
}
}
}
Loading