From e9575a0ff998c916c0a61ed226a032bd2b82f578 Mon Sep 17 00:00:00 2001 From: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:07:12 -0600 Subject: [PATCH] fix: improving performance of FlagSynchronizer creation (#420) **Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/v9/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** SDK-1030 **Describe the solution you've provided** Reduces total parsing done to retrieve last updated cache value. --- LaunchDarkly/LaunchDarkly/LDClient.swift | 6 ++--- .../Cache/FeatureFlagCache.swift | 22 +++++++++++++++++++ .../LaunchDarklyTests/LDClientSpec.swift | 4 ++-- .../Cache/FeatureFlagCacheSpec.swift | 18 +++++++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 9d905db3..afa93fc1 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -207,9 +207,7 @@ public class LDClient { os_log("%s runMode aborted. Old runMode equals new runMode", log: config.logger, type: .debug, typeName(and: #function)) return } - - let cachedData = self.flagCache.getCachedData(cacheKey: self.context.fullyQualifiedHashedKey(), contextHash: self.context.contextHash()) - + let lastUpdated = self.flagCache.getCachedDataLastUpdatedDate(cacheKey: self.context.fullyQualifiedHashedKey(), contextHash: self.context.contextHash()) let willSetSynchronizerOnline = isOnline && isInSupportedRunMode flagSynchronizer.isOnline = false let streamingModeVar = ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self) @@ -217,7 +215,7 @@ public class LDClient { flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: streamingModeVar, pollingInterval: config.flagPollingInterval(runMode: runMode), useReport: config.useReport, - lastUpdated: cachedData.lastUpdated, + lastUpdated: lastUpdated, service: service, onSyncComplete: onFlagSyncComplete) flagSynchronizer.isOnline = willSetSynchronizerOnline diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift index 43265f11..e86fe2e8 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/FeatureFlagCache.swift @@ -33,6 +33,15 @@ protocol FeatureFlagCaching { /// func getCachedData(cacheKey: String, contextHash: String) -> (items: StoredItems?, etag: String?, lastUpdated: Date?) + /// Retrieve the date the cache for the given key was last updated. See getCachedData for more information. + /// + /// - parameter cacheKey: The index key into the local cache store. + /// - parameter contextHash: A hash value representing a fully unique context. + /// + /// - returns: The date the cache was last considered up-to-date. If there are no cached + /// values, this should return nil. + func getCachedDataLastUpdatedDate(cacheKey: String, contextHash: String) -> Date? + // When we update the cache, we save the flag data and if we have it, an // etag. For polling, we should always have the flag data and an etag // value. This is not the case for streaming. @@ -100,6 +109,19 @@ final class FeatureFlagCache: FeatureFlagCaching { return (items: cachedFlags.flags, etag: etag, lastUpdated: Date(timeIntervalSince1970: TimeInterval(lastUpdated / 1_000))) } + func getCachedDataLastUpdatedDate(cacheKey: String, contextHash: String) -> Date? { + + var cachedContexts: [String: Int64] = [:] + if let cacheMetadata = keyedValueCache.data(forKey: "cached-contexts") { + cachedContexts = (try? JSONDecoder().decode([String: Int64].self, from: cacheMetadata)) ?? [:] + } + + guard let lastUpdated = cachedContexts[cacheKey] + else { return nil } + + return Date(timeIntervalSince1970: TimeInterval(lastUpdated / 1_000)) + } + func saveCachedData(_ storedItems: StoredItems, cacheKey: String, contextHash: String, lastUpdated: Date, etag: String?) { guard self.maxCachedContexts != 0, let encoded = try? JSONEncoder().encode(StoredItemCollection(storedItems)) diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 9268eab8..7590ae2e 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -382,7 +382,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } it("uncaches the new contexts flags") { - expect(testContext.featureFlagCachingMock.getCachedDataCallCount) == 2 + expect(testContext.featureFlagCachingMock.getCachedDataCallCount) == 1 expect(testContext.featureFlagCachingMock.getCachedDataReceivedArguments?.cacheKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { @@ -421,7 +421,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.context) == testContext.context } it("uncaches the new contexts flags") { - expect(testContext.featureFlagCachingMock.getCachedDataCallCount) == 2 + expect(testContext.featureFlagCachingMock.getCachedDataCallCount) == 1 expect(testContext.featureFlagCachingMock.getCachedDataReceivedArguments?.cacheKey) == testContext.context.fullyQualifiedHashedKey() } it("records an identify event") { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift index d76417af..4f5d2794 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -178,4 +178,22 @@ final class FeatureFlagCacheSpec: XCTestCase { let setMetadata = try JSONDecoder().decode([String: Int64].self, from: mockValueCache.setReceivedArguments!.value) XCTAssertEqual(setMetadata, [hashedContextKey: now.millisSince1970]) } + + func testGetCachedDataLastUpdatedDate() { + let now = Date() + let flagCache = FeatureFlagCache(serviceFactory: ClientServiceFactory(logger: .disabled), mobileKey: "abc", maxCachedContexts: 5) + flagCache.saveCachedData(testFlagCollection.flags, cacheKey: "key", contextHash: "hash", lastUpdated: now, etag: "example-etag") + + let lastUpdated = flagCache.getCachedDataLastUpdatedDate(cacheKey: "key", contextHash: "hash") + XCTAssertEqual(lastUpdated!.millisSince1970, now.millisSince1970, accuracy: 1_000) + } + + func testGetCachedDataLastUpdatedDateKeyDoesntExist() { + let now = Date() + let flagCache = FeatureFlagCache(serviceFactory: ClientServiceFactory(logger: .disabled), mobileKey: "abc", maxCachedContexts: 5) + flagCache.saveCachedData(testFlagCollection.flags, cacheKey: "key", contextHash: "hash", lastUpdated: now, etag: "example-etag") + + let lastUpdated = flagCache.getCachedDataLastUpdatedDate(cacheKey: "bogus", contextHash: "bogusHash") + XCTAssertEqual(lastUpdated, nil) + } }