Skip to content

Commit c2748e9

Browse files
committed
fix(sessions): Fix deadlock
1 parent 0e5a4e0 commit c2748e9

File tree

1 file changed

+55
-21
lines changed

1 file changed

+55
-21
lines changed

FirebaseSessions/Sources/Settings/SettingsCacheClient.swift

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import Foundation
2222
@preconcurrency internal import GoogleUtilities
2323
#endif // SWIFT_PACKAGE
2424

25+
internal import FirebaseCoreInternal
26+
2527
/// CacheKey is like a "key" to a "safe". It provides necessary metadata about the current cache to
2628
/// know if it should be expired.
2729
struct CacheKey: Codable {
@@ -57,53 +59,84 @@ final class SettingsCache: SettingsCacheClient {
5759
static let forCacheKey = "firebase-sessions-cache-key"
5860
}
5961

60-
/// UserDefaults holds values in memory, making access O(1) and synchronous within the app, while
61-
/// abstracting away async disk IO.
62-
private let cache: GULUserDefaults = .standard()
62+
private let userDefaults: GULUserDefaults
63+
private let persistenceQueue =
64+
DispatchQueue(label: "com.google.firebase.sessions.settings.persistence")
65+
66+
// This lock protects the in-memory properties.
67+
private let inMemoryCacheLock: UnfairLock<CacheData>
68+
69+
private struct CacheData {
70+
var content: [String: Any]
71+
var key: CacheKey?
72+
}
73+
74+
init(userDefaults: GULUserDefaults = .standard()) {
75+
self.userDefaults = userDefaults
76+
let content = (userDefaults.object(forKey: UserDefaultsKeys.forContent) as? [String: Any]) ??
77+
[:]
78+
var key: CacheKey?
79+
if let data = userDefaults.object(forKey: UserDefaultsKeys.forCacheKey) as? Data {
80+
do {
81+
key = try JSONDecoder().decode(CacheKey.self, from: data)
82+
} catch {
83+
Logger.logError("[Settings] Decoding CacheKey failed with error: \(error)")
84+
}
85+
}
86+
inMemoryCacheLock = UnfairLock(CacheData(content: content, key: key))
87+
}
6388

6489
/// Converting to dictionary is O(1) because object conversion is O(1)
6590
var cacheContent: [String: Any] {
6691
get {
67-
return (cache.object(forKey: UserDefaultsKeys.forContent) as? [String: Any]) ?? [:]
92+
return inMemoryCacheLock.value().content
6893
}
6994
set {
70-
cache.setObject(newValue, forKey: UserDefaultsKeys.forContent)
95+
inMemoryCacheLock.withLock { $0.content = newValue }
96+
persistenceQueue.async {
97+
self.userDefaults.setObject(newValue, forKey: UserDefaultsKeys.forContent)
98+
}
7199
}
72100
}
73101

74102
/// Casting to Codable from Data is O(n)
75103
var cacheKey: CacheKey? {
76104
get {
77-
if let data = cache.object(forKey: UserDefaultsKeys.forCacheKey) as? Data {
105+
return inMemoryCacheLock.value().key
106+
}
107+
set {
108+
inMemoryCacheLock.withLock { $0.key = newValue }
109+
persistenceQueue.async {
78110
do {
79-
return try JSONDecoder().decode(CacheKey.self, from: data)
111+
try self.userDefaults.setObject(JSONEncoder().encode(newValue),
112+
forKey: UserDefaultsKeys.forCacheKey)
80113
} catch {
81-
Logger.logError("[Settings] Decoding CacheKey failed with error: \(error)")
114+
Logger.logError("[Settings] Encoding CacheKey failed with error: \(error)")
82115
}
83116
}
84-
return nil
85-
}
86-
set {
87-
do {
88-
try cache.setObject(JSONEncoder().encode(newValue), forKey: UserDefaultsKeys.forCacheKey)
89-
} catch {
90-
Logger.logError("[Settings] Encoding CacheKey failed with error: \(error)")
91-
}
92117
}
93118
}
94119

95120
/// Removes stored cache
96121
func removeCache() {
97-
cache.setObject(nil, forKey: UserDefaultsKeys.forContent)
98-
cache.setObject(nil, forKey: UserDefaultsKeys.forCacheKey)
122+
inMemoryCacheLock.withLock {
123+
$0.content = [:]
124+
$0.key = nil
125+
}
126+
persistenceQueue.async {
127+
self.userDefaults.setObject(nil, forKey: UserDefaultsKeys.forContent)
128+
self.userDefaults.setObject(nil, forKey: UserDefaultsKeys.forCacheKey)
129+
}
99130
}
100131

101132
func isExpired(for appInfo: ApplicationInfoProtocol, time: Date) -> Bool {
102-
guard !cacheContent.isEmpty else {
133+
let (content, key) = inMemoryCacheLock.withLock { ($0.content, $0.key) }
134+
135+
guard !content.isEmpty else {
103136
removeCache()
104137
return true
105138
}
106-
guard let cacheKey = cacheKey else {
139+
guard let cacheKey = key else {
107140
Logger.logError("[Settings] Could not load settings cache key")
108141
removeCache()
109142
return true
@@ -126,7 +159,8 @@ final class SettingsCache: SettingsCacheClient {
126159
}
127160

128161
private func cacheDuration() -> TimeInterval {
129-
guard let duration = cacheContent[Self.flagCacheDuration] as? Double else {
162+
let content = inMemoryCacheLock.value().content
163+
guard let duration = content[Self.flagCacheDuration] as? Double else {
130164
return Self.cacheDurationSecondsDefault
131165
}
132166
Logger.logDebug("[Settings] Cache duration: \(duration)")

0 commit comments

Comments
 (0)