Skip to content

Commit 62587ad

Browse files
authored
feat: Update caching strategy to allow for greater cache use (#404)
Instead of indexing by the full context's hash, we are going to revert to indexing by canonical key. The cache will store the hash alongside the flag values. This stored hash will be compared with the active context hash when the cache is read. If the hashes are different, the SDK will fetch updated values. If they haven't changed, then the SDK is free to wait until the cache freshness has exceeded the configured polling interval. As a result, the SDK should have a smoother transition from default -> last known values -> fresh values as the context changes while also minimizing unnecessary API requests.
1 parent 90bf896 commit 62587ad

File tree

7 files changed

+126
-56
lines changed

7 files changed

+126
-56
lines changed

LaunchDarkly/GeneratedCode/mocks.generated.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -246,21 +246,21 @@ final class FeatureFlagCachingMock: FeatureFlagCaching {
246246

247247
var getCachedDataCallCount = 0
248248
var getCachedDataCallback: (() throws -> Void)?
249-
var getCachedDataReceivedCacheKey: String?
249+
var getCachedDataReceivedArguments: (cacheKey: String, contextHash: String)?
250250
var getCachedDataReturnValue: (items: StoredItems?, etag: String?, lastUpdated: Date?)!
251-
func getCachedData(cacheKey: String) -> (items: StoredItems?, etag: String?, lastUpdated: Date?) {
251+
func getCachedData(cacheKey: String, contextHash: String) -> (items: StoredItems?, etag: String?, lastUpdated: Date?) {
252252
getCachedDataCallCount += 1
253-
getCachedDataReceivedCacheKey = cacheKey
253+
getCachedDataReceivedArguments = (cacheKey: cacheKey, contextHash: contextHash)
254254
try! getCachedDataCallback?()
255255
return getCachedDataReturnValue
256256
}
257257

258258
var saveCachedDataCallCount = 0
259259
var saveCachedDataCallback: (() throws -> Void)?
260-
var saveCachedDataReceivedArguments: (storedItems: StoredItems, cacheKey: String, lastUpdated: Date, etag: String?)?
261-
func saveCachedData(_ storedItems: StoredItems, cacheKey: String, lastUpdated: Date, etag: String?) {
260+
var saveCachedDataReceivedArguments: (storedItems: StoredItems, cacheKey: String, contextHash: String, lastUpdated: Date, etag: String?)?
261+
func saveCachedData(_ storedItems: StoredItems, cacheKey: String, contextHash: String, lastUpdated: Date, etag: String?) {
262262
saveCachedDataCallCount += 1
263-
saveCachedDataReceivedArguments = (storedItems: storedItems, cacheKey: cacheKey, lastUpdated: lastUpdated, etag: etag)
263+
saveCachedDataReceivedArguments = (storedItems: storedItems, cacheKey: cacheKey, contextHash: contextHash, lastUpdated: lastUpdated, etag: etag)
264264
try! saveCachedDataCallback?()
265265
}
266266
}

LaunchDarkly/LaunchDarkly/LDClient.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ public class LDClient {
208208
return
209209
}
210210

211-
let cachedData = self.flagCache.getCachedData(cacheKey: self.context.contextHash())
211+
let cachedData = self.flagCache.getCachedData(cacheKey: self.context.fullyQualifiedHashedKey(), contextHash: self.context.contextHash())
212212

213213
let willSetSynchronizerOnline = isOnline && isInSupportedRunMode
214214
flagSynchronizer.isOnline = false
@@ -393,7 +393,7 @@ public class LDClient {
393393
let wasOnline = self.isOnline
394394
self.internalSetOnline(false)
395395

396-
let cachedData = self.flagCache.getCachedData(cacheKey: self.context.contextHash())
396+
let cachedData = self.flagCache.getCachedData(cacheKey: self.context.fullyQualifiedHashedKey(), contextHash: self.context.contextHash())
397397
let cachedContextFlags = cachedData.items ?? [:]
398398
let oldItems = flagStore.storedItems.featureFlags
399399

@@ -629,7 +629,7 @@ public class LDClient {
629629

630630
private func updateCacheAndReportChanges(context: LDContext,
631631
oldStoredItems: StoredItems, etag: String?) {
632-
flagCache.saveCachedData(flagStore.storedItems, cacheKey: context.contextHash(), lastUpdated: Date(), etag: etag)
632+
flagCache.saveCachedData(flagStore.storedItems, cacheKey: context.fullyQualifiedHashedKey(), contextHash: context.contextHash(), lastUpdated: Date(), etag: etag)
633633
flagChangeNotifier.notifyObservers(oldFlags: oldStoredItems.featureFlags, newFlags: flagStore.storedItems.featureFlags)
634634
}
635635

@@ -641,7 +641,7 @@ public class LDClient {
641641
In other words, if we get confirmation our cache is still fresh, then we shouldn't poll again for another <pollingInterval> seconds. If we didn't update this, we would poll immediately on restart.
642642
*/
643643
private func updateCacheFreshness(context: LDContext) {
644-
flagCache.saveCachedData(flagStore.storedItems, cacheKey: context.contextHash(), lastUpdated: Date(), etag: nil)
644+
flagCache.saveCachedData(flagStore.storedItems, cacheKey: context.fullyQualifiedHashedKey(), contextHash: context.contextHash(), lastUpdated: Date(), etag: nil)
645645
}
646646

647647
// MARK: Events
@@ -881,7 +881,7 @@ public class LDClient {
881881
diagnosticReporter = self.serviceFactory.makeDiagnosticReporter(service: service, environmentReporter: environmentReporter)
882882
eventReporter = self.serviceFactory.makeEventReporter(service: service)
883883
connectionInformation = self.serviceFactory.makeConnectionInformation()
884-
let cachedData = flagCache.getCachedData(cacheKey: context.contextHash())
884+
let cachedData = flagCache.getCachedData(cacheKey: context.fullyQualifiedHashedKey(), contextHash: context.contextHash())
885885
flagSynchronizer = self.serviceFactory.makeFlagSynchronizer(streamingMode: config.allowStreamingMode ? config.streamingMode : .polling,
886886
pollingInterval: config.flagPollingInterval(runMode: runMode),
887887
useReport: config.useReport,

LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -364,12 +364,12 @@ public struct LDContext: Encodable, Equatable {
364364
encoder.userInfo[UserInfoKeys.redactAttributes] = false
365365

366366
guard let json = try? encoder.encode(self)
367-
else { return fullyQualifiedKey() }
367+
else { return fullyQualifiedHashedKey() }
368368

369369
if let jsonStr = String(data: json, encoding: .utf8) {
370370
return Util.sha256base64(jsonStr)
371371
}
372-
return fullyQualifiedKey()
372+
return fullyQualifiedHashedKey()
373373
}
374374

375375
/// - Returns: true if the `LDContext` is a multi-context; false otherwise.

LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift

+40-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,22 @@ protocol FeatureFlagCaching {
77

88
/// Retrieve all cached data for the given cache key.
99
///
10-
/// - parameter cacheKey: The unique key into the local cache store.
10+
/// The cache key is used as the index into the cache. Values retrieved
11+
/// using this cache key should be loaded into the store and favored over
12+
/// the default values.
13+
///
14+
/// The context hash value is used to determine if the cache is considered
15+
/// out of date. If the hash saved alongside the cached value does not
16+
/// match, then the cache's etag and lastUpdated responses should be nil as
17+
/// they are invalid.
18+
///
19+
/// If the hash hasn't changed, then the cache is still considered accurate
20+
/// and the associated etag and last updated values are meaningful and can
21+
/// be returned.
22+
///
23+
/// - parameter cacheKey: The index key into the local cache store.
24+
/// - parameter contextHash: A hash value representing a fully unique context.
25+
///
1126
/// - returns: Returns a tuple of cached value information.
1227
/// items: This is the associated flag evaluation results associated with this context.
1328
/// etag: The last known e-tag value from a polling request (see saveCachedData
@@ -16,7 +31,7 @@ protocol FeatureFlagCaching {
1631
/// values, this should return nil.
1732
///
1833
///
19-
func getCachedData(cacheKey: String) -> (items: StoredItems?, etag: String?, lastUpdated: Date?)
34+
func getCachedData(cacheKey: String, contextHash: String) -> (items: StoredItems?, etag: String?, lastUpdated: Date?)
2035

2136
// When we update the cache, we save the flag data and if we have it, an
2237
// etag. For polling, we should always have the flag data and an etag
@@ -35,7 +50,11 @@ protocol FeatureFlagCaching {
3550
//
3651
// 2. Updates have been made at which point the e-tag will be ignored
3752
// upstream and we will still receive updated information as expected.
38-
func saveCachedData(_ storedItems: StoredItems, cacheKey: String, lastUpdated: Date, etag: String?)
53+
//
54+
// The context hash is stored alongside the stored items. This is used as a
55+
// marker to determine when the values are useful but not potentially
56+
// accurate.
57+
func saveCachedData(_ storedItems: StoredItems, cacheKey: String, contextHash: String, lastUpdated: Date, etag: String?)
3958
}
4059

4160
final class FeatureFlagCache: FeatureFlagCaching {
@@ -53,11 +72,19 @@ final class FeatureFlagCache: FeatureFlagCaching {
5372
self.maxCachedContexts = maxCachedContexts
5473
}
5574

56-
func getCachedData(cacheKey: String) -> (items: StoredItems?, etag: String?, lastUpdated: Date?) {
75+
func getCachedData(cacheKey: String, contextHash: String) -> (items: StoredItems?, etag: String?, lastUpdated: Date?) {
76+
5777
guard let cachedFlagsData = keyedValueCache.data(forKey: "flags-\(cacheKey)"),
5878
let cachedFlags = try? JSONDecoder().decode(StoredItemCollection.self, from: cachedFlagsData)
5979
else { return (items: nil, etag: nil, lastUpdated: nil) }
6080

81+
guard let cachedContextHashData = keyedValueCache.data(forKey: "fingerprint-\(cacheKey)"),
82+
let cachedContextHash = try? JSONDecoder().decode(String.self, from: cachedContextHashData)
83+
else { return (items: cachedFlags.flags, etag: nil, lastUpdated: nil) }
84+
85+
guard cachedContextHash == contextHash
86+
else { return (items: cachedFlags.flags, etag: nil, lastUpdated: nil) }
87+
6188
guard let cachedETagData = keyedValueCache.data(forKey: "etag-\(cacheKey)"),
6289
let etag = try? JSONDecoder().decode(String.self, from: cachedETagData)
6390
else { return (items: cachedFlags.flags, etag: nil, lastUpdated: nil) }
@@ -73,14 +100,19 @@ final class FeatureFlagCache: FeatureFlagCaching {
73100
return (items: cachedFlags.flags, etag: etag, lastUpdated: Date(timeIntervalSince1970: TimeInterval(lastUpdated / 1_000)))
74101
}
75102

76-
func saveCachedData(_ storedItems: StoredItems, cacheKey: String, lastUpdated: Date, etag: String?) {
103+
func saveCachedData(_ storedItems: StoredItems, cacheKey: String, contextHash: String, lastUpdated: Date, etag: String?) {
104+
77105
guard self.maxCachedContexts != 0, let encoded = try? JSONEncoder().encode(StoredItemCollection(storedItems))
78106
else { return }
79107

80108
self.keyedValueCache.set(encoded, forKey: "flags-\(cacheKey)")
81109

82-
if let tag = etag, let encodedCachedData = try? JSONEncoder().encode(tag) {
83-
self.keyedValueCache.set(encodedCachedData, forKey: "etag-\(cacheKey)")
110+
if let encodedContextHashData = try? JSONEncoder().encode(contextHash) {
111+
self.keyedValueCache.set(encodedContextHashData, forKey: "fingerprint-\(cacheKey)")
112+
113+
if let tag = etag, let encodedCachedData = try? JSONEncoder().encode(tag) {
114+
self.keyedValueCache.set(encodedCachedData, forKey: "etag-\(cacheKey)")
115+
}
84116
}
85117

86118
var cachedContexts: [String: Int64] = [:]
@@ -94,6 +126,7 @@ final class FeatureFlagCache: FeatureFlagCaching {
94126
cachedContexts.removeValue(forKey: sha)
95127
self.keyedValueCache.removeObject(forKey: "flags-\(sha)")
96128
self.keyedValueCache.removeObject(forKey: "etag-\(sha)")
129+
self.keyedValueCache.removeObject(forKey: "fingerprint-\(sha)")
97130
}
98131
}
99132
if let encoded = try? JSONEncoder().encode(cachedContexts) {

0 commit comments

Comments
 (0)