diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index 91ba0e80f24..9368af6b4ca 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased +- [fixed] Fixed a potential deadlock in an internal dependency that could occur + during app foregrounding when reading cached settings. (#15394) + # 12.3.0 - [fixed] Add missing GoogleUtilities dependency to fix SwiftPM builds when building dynamically linked libraries. (#15276) diff --git a/FirebaseSessions/Sources/Settings/SettingsCacheClient.swift b/FirebaseSessions/Sources/Settings/SettingsCacheClient.swift index 79a2e1ffa20..27acce21fb2 100644 --- a/FirebaseSessions/Sources/Settings/SettingsCacheClient.swift +++ b/FirebaseSessions/Sources/Settings/SettingsCacheClient.swift @@ -22,6 +22,8 @@ import Foundation @preconcurrency internal import GoogleUtilities #endif // SWIFT_PACKAGE +internal import FirebaseCoreInternal + /// CacheKey is like a "key" to a "safe". It provides necessary metadata about the current cache to /// know if it should be expired. struct CacheKey: Codable { @@ -57,53 +59,84 @@ final class SettingsCache: SettingsCacheClient { static let forCacheKey = "firebase-sessions-cache-key" } - /// UserDefaults holds values in memory, making access O(1) and synchronous within the app, while - /// abstracting away async disk IO. - private let cache: GULUserDefaults = .standard() + private let userDefaults: GULUserDefaults + private let persistenceQueue = + DispatchQueue(label: "com.google.firebase.sessions.settings.persistence") + + // This lock protects the in-memory properties. + private let inMemoryCacheLock: UnfairLock + + private struct CacheData { + var content: [String: Any] + var key: CacheKey? + } + + init(userDefaults: GULUserDefaults = .standard()) { + self.userDefaults = userDefaults + let content = (userDefaults.object(forKey: UserDefaultsKeys.forContent) as? [String: Any]) ?? + [:] + var key: CacheKey? + if let data = userDefaults.object(forKey: UserDefaultsKeys.forCacheKey) as? Data { + do { + key = try JSONDecoder().decode(CacheKey.self, from: data) + } catch { + Logger.logError("[Settings] Decoding CacheKey failed with error: \(error)") + } + } + inMemoryCacheLock = UnfairLock(CacheData(content: content, key: key)) + } /// Converting to dictionary is O(1) because object conversion is O(1) var cacheContent: [String: Any] { get { - return (cache.object(forKey: UserDefaultsKeys.forContent) as? [String: Any]) ?? [:] + return inMemoryCacheLock.value().content } set { - cache.setObject(newValue, forKey: UserDefaultsKeys.forContent) + inMemoryCacheLock.withLock { $0.content = newValue } + persistenceQueue.async { + self.userDefaults.setObject(newValue, forKey: UserDefaultsKeys.forContent) + } } } /// Casting to Codable from Data is O(n) var cacheKey: CacheKey? { get { - if let data = cache.object(forKey: UserDefaultsKeys.forCacheKey) as? Data { + return inMemoryCacheLock.value().key + } + set { + inMemoryCacheLock.withLock { $0.key = newValue } + persistenceQueue.async { do { - return try JSONDecoder().decode(CacheKey.self, from: data) + try self.userDefaults.setObject(JSONEncoder().encode(newValue), + forKey: UserDefaultsKeys.forCacheKey) } catch { - Logger.logError("[Settings] Decoding CacheKey failed with error: \(error)") + Logger.logError("[Settings] Encoding CacheKey failed with error: \(error)") } } - return nil - } - set { - do { - try cache.setObject(JSONEncoder().encode(newValue), forKey: UserDefaultsKeys.forCacheKey) - } catch { - Logger.logError("[Settings] Encoding CacheKey failed with error: \(error)") - } } } /// Removes stored cache func removeCache() { - cache.setObject(nil, forKey: UserDefaultsKeys.forContent) - cache.setObject(nil, forKey: UserDefaultsKeys.forCacheKey) + inMemoryCacheLock.withLock { + $0.content = [:] + $0.key = nil + } + persistenceQueue.async { + self.userDefaults.setObject(nil, forKey: UserDefaultsKeys.forContent) + self.userDefaults.setObject(nil, forKey: UserDefaultsKeys.forCacheKey) + } } func isExpired(for appInfo: ApplicationInfoProtocol, time: Date) -> Bool { - guard !cacheContent.isEmpty else { + let (content, key) = inMemoryCacheLock.withLock { ($0.content, $0.key) } + + guard !content.isEmpty else { removeCache() return true } - guard let cacheKey = cacheKey else { + guard let cacheKey = key else { Logger.logError("[Settings] Could not load settings cache key") removeCache() return true @@ -126,7 +159,8 @@ final class SettingsCache: SettingsCacheClient { } private func cacheDuration() -> TimeInterval { - guard let duration = cacheContent[Self.flagCacheDuration] as? Double else { + let content = inMemoryCacheLock.value().content + guard let duration = content[Self.flagCacheDuration] as? Double else { return Self.cacheDurationSecondsDefault } Logger.logDebug("[Settings] Cache duration: \(duration)")