Skip to content

Commit

Permalink
feat: Add cache usage option for identify calls (#408)
Browse files Browse the repository at this point in the history
The cache handling option allows users to control the flag store
transitions while identification network requests asynchronously
resolve.
  • Loading branch information
keelerm84 authored Nov 6, 2024
1 parent 2fbca8f commit b928345
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 80 deletions.
21 changes: 11 additions & 10 deletions LaunchDarkly.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,11 @@
A3BA7D022BD192240000DB28 /* LDClientHookSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BA7D012BD192240000DB28 /* LDClientHookSpec.swift */; };
A3BA7D042BD2BD620000DB28 /* TestContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BA7D032BD2BD620000DB28 /* TestContext.swift */; };
A3C6F7622B7FA803005B3B61 /* SheddingQueueSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C6F7612B7FA803005B3B61 /* SheddingQueueSpec.swift */; };
A3C6F7642B84EF0C005B3B61 /* IdentifyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C6F7632B84EF0C005B3B61 /* IdentifyResult.swift */; };
A3C6F7652B84EF0C005B3B61 /* IdentifyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C6F7632B84EF0C005B3B61 /* IdentifyResult.swift */; };
A3C6F7662B84EF0C005B3B61 /* IdentifyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C6F7632B84EF0C005B3B61 /* IdentifyResult.swift */; };
A3C6F7672B84EF0C005B3B61 /* IdentifyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C6F7632B84EF0C005B3B61 /* IdentifyResult.swift */; };
A3C6F7642B84EF0C005B3B61 /* IdentifyTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C6F7632B84EF0C005B3B61 /* IdentifyTypes.swift */; };
A3C6F7652B84EF0C005B3B61 /* IdentifyTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C6F7632B84EF0C005B3B61 /* IdentifyTypes.swift */; };
A3C6F7662B84EF0C005B3B61 /* IdentifyTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C6F7632B84EF0C005B3B61 /* IdentifyTypes.swift */; };
A3C6F7672B84EF0C005B3B61 /* IdentifyTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C6F7632B84EF0C005B3B61 /* IdentifyTypes.swift */; };
A3F4A4812CC2F640006EF480 /* CwlPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; productRef = A3F4A4802CC2F640006EF480 /* CwlPreconditionTesting */; };
A3FFE1132B7D4BA2009EF93F /* LDValueDecoderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */; };
B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; };
B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; };
Expand Down Expand Up @@ -511,7 +512,7 @@
A3BA7D012BD192240000DB28 /* LDClientHookSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDClientHookSpec.swift; sourceTree = "<group>"; };
A3BA7D032BD2BD620000DB28 /* TestContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestContext.swift; sourceTree = "<group>"; };
A3C6F7612B7FA803005B3B61 /* SheddingQueueSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheddingQueueSpec.swift; sourceTree = "<group>"; };
A3C6F7632B84EF0C005B3B61 /* IdentifyResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyResult.swift; sourceTree = "<group>"; };
A3C6F7632B84EF0C005B3B61 /* IdentifyTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyTypes.swift; sourceTree = "<group>"; };
A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDValueDecoderSpec.swift; sourceTree = "<group>"; };
B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = "<group>"; };
B4265EB024E7390C001CFD2C /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -726,7 +727,7 @@
8354EFDE1F26380700C05156 /* Event.swift */,
83EBCB9D20D9A0A1003A7142 /* FeatureFlag */,
8354EFDD1F26380700C05156 /* LDConfig.swift */,
A3C6F7632B84EF0C005B3B61 /* IdentifyResult.swift */,
A3C6F7632B84EF0C005B3B61 /* IdentifyTypes.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -1346,7 +1347,7 @@
B4C9D43B2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */,
C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */,
B468E71324B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */,
A3C6F7672B84EF0C005B3B61 /* IdentifyResult.swift in Sources */,
A3C6F7672B84EF0C005B3B61 /* IdentifyTypes.swift in Sources */,
8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */,
8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */,
8311884E2113ADE500D77CB5 /* Event.swift in Sources */,
Expand Down Expand Up @@ -1375,7 +1376,7 @@
B468E71224B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */,
A36EDFCF2853C50B00D91B05 /* ObjcLDContext.swift in Sources */,
831EF34320655E730001C643 /* LDCommon.swift in Sources */,
A3C6F7662B84EF0C005B3B61 /* IdentifyResult.swift in Sources */,
A3C6F7662B84EF0C005B3B61 /* IdentifyTypes.swift in Sources */,
831EF34420655E730001C643 /* LDConfig.swift in Sources */,
A31088212837DC0400184942 /* LDContext.swift in Sources */,
831EF34520655E730001C643 /* LDClient.swift in Sources */,
Expand Down Expand Up @@ -1491,7 +1492,7 @@
83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */,
B4C9D4382489E20A004A9B03 /* DiagnosticReporter.swift in Sources */,
8347BB0C21F147E100E56BCD /* LDTimer.swift in Sources */,
A3C6F7642B84EF0C005B3B61 /* IdentifyResult.swift in Sources */,
A3C6F7642B84EF0C005B3B61 /* IdentifyTypes.swift in Sources */,
B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */,
8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */,
A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */,
Expand Down Expand Up @@ -1617,7 +1618,7 @@
83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */,
8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */,
8354AC712243166900CDE602 /* FeatureFlagCache.swift in Sources */,
A3C6F7652B84EF0C005B3B61 /* IdentifyResult.swift in Sources */,
A3C6F7652B84EF0C005B3B61 /* IdentifyTypes.swift in Sources */,
C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */,
B4C9D4392489E20A004A9B03 /* DiagnosticReporter.swift in Sources */,
B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */,
Expand Down
87 changes: 60 additions & 27 deletions LaunchDarkly/LaunchDarkly/LDClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ public class LDClient {
*/
@available(*, deprecated, message: "Use LDClient.identify(context: completion:) with non-optional completion parameter")
public func identify(context: LDContext, completion: (() -> Void)? = nil) {
_identify(context: context, sheddable: false) { _ in
_identify(context: context, sheddable: false, useCache: .yes) { _ in
if let completion = completion {
completion()
}
Expand All @@ -315,19 +315,33 @@ public class LDClient {
- parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays.
*/
public func identify(context: LDContext, completion: @escaping (_ result: IdentifyResult) -> Void) {
_identify(context: context, sheddable: true, completion: completion)
_identify(context: context, sheddable: true, useCache: .yes, completion: completion)
}

/**
Sets the LDContext into the LDClient inline with the behavior detailed on `LDClient.identify(context: completion:)`. Additionally,
this method allows specifying how the flag cache should be handled when transitioning between contexts through the `useCache` parameter.

To learn more about these cache transitions, refer to the `IdentifyCacheUsage` documentation.

- parameter context: The LDContext set with the desired context.
- parameter useCache: How to handle flag caches during identify transition.
- parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays.
*/
public func identify(context: LDContext, useCache: IdentifyCacheUsage, completion: @escaping (_ result: IdentifyResult) -> Void) {
_identify(context: context, sheddable: true, useCache: useCache, completion: completion)
}

// Temporary helper method to allow code sharing between the sheddable and unsheddable identify methods. In the next major release, we will remove the deprecated identify method and inline
// this implementation in the other one.
private func _identify(context: LDContext, sheddable: Bool, completion: @escaping (_ result: IdentifyResult) -> Void) {
private func _identify(context: LDContext, sheddable: Bool, useCache: IdentifyCacheUsage, completion: @escaping (_ result: IdentifyResult) -> Void) {
let work: TaskHandler = { taskCompletion in
let dispatch = DispatchGroup()

LDClient.instancesQueue.sync(flags: .barrier) {
LDClient.instances?.forEach { _, instance in
dispatch.enter()
instance.internalIdentify(newContext: context, completion: dispatch.leave)
instance.internalIdentify(newContext: context, useCache: useCache, completion: dispatch.leave)
}
}

Expand All @@ -354,6 +368,21 @@ public class LDClient {
- parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays.
*/
public func identify(context: LDContext, timeout: TimeInterval, completion: @escaping ((_ result: IdentifyResult) -> Void)) {
identify(context: context, timeout: timeout, useCache: .yes, completion: completion)
}

/**
Sets the LDContext into the LDClient inline with the behavior detailed on `LDClient.identify(context: timeout: completion:)`. Additionally,
this method allows specifying how the flag cache should be handled when transitioning between contexts through the `useCache` parameter.

To learn more about these cache transitions, refer to the `IdentifyCacheUsage` documentation.

- parameter context: The LDContext set with the desired context.
- parameter timeout: The upper time limit before the `completion` callback will be invoked.
- parameter useCache: How to handle flag caches during identify transition.
- parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays.
*/
public func identify(context: LDContext, timeout: TimeInterval, useCache: IdentifyCacheUsage, completion: @escaping ((_ result: IdentifyResult) -> Void)) {
if timeout > LDClient.longTimeoutInterval {
os_log("%s LDClient.identify was called with a timeout greater than %f seconds. We recommend a timeout of less than %f seconds.", log: config.logger, type: .info, self.typeName(and: #function), LDClient.longTimeoutInterval, LDClient.longTimeoutInterval)
}
Expand All @@ -367,15 +396,15 @@ public class LDClient {
completion(.timeout)
}

identify(context: context) { result in
identify(context: context, useCache: useCache) { result in
guard !cancel else { return }

cancel = true
completion(result)
}
}

func internalIdentify(newContext: LDContext, completion: (() -> Void)? = nil) {
func internalIdentify(newContext: LDContext, useCache: IdentifyCacheUsage, completion: (() -> Void)? = nil) {
var updatedContext = newContext
if config.autoEnvAttributes {
updatedContext = AutoEnvContextModifier(environmentReporter: environmentReporter, logger: config.logger).modifyContext(updatedContext)
Expand All @@ -394,27 +423,31 @@ public class LDClient {
self.internalSetOnline(false)

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

// Here we prime the store with the last known values from the
// cache.
//
// Once the flag sync. process finishes, the new payload is
// compared to this, and if they are different, change listeners
// will be notified; otherwise, they aren't.
//
// This is problematic since the flag values really did change. So
// we should trigger the change listener when we set these cache
// values.
//
// However, if there are no cached values, we don't want to inform
// customers that we set their store to nothing. In that case, we
// will not trigger the change listener and instead relay on the
// payload comparsion to do that when the request has completed.
flagStore.replaceStore(newStoredItems: cachedContextFlags)
if !cachedContextFlags.featureFlags.isEmpty {
flagChangeNotifier.notifyObservers(oldFlags: oldItems, newFlags: flagStore.storedItems.featureFlags)

if useCache != .no {
let oldItems = flagStore.storedItems
let fallback = useCache == .yes ? [:] : oldItems
let cachedContextFlags = cachedData.items ?? fallback

// Here we prime the store with the last known values from the
// cache.
//
// Once the flag sync. process finishes, the new payload is
// compared to this, and if they are different, change listeners
// will be notified; otherwise, they aren't.
//
// This is problematic since the flag values really did change. So
// we should trigger the change listener when we set these cache
// values.
//
// However, if there are no cached values, we don't want to inform
// customers that we set their store to nothing. In that case, we
// will not trigger the change listener and instead relay on the
// payload comparsion to do that when the request has completed.
flagStore.replaceStore(newStoredItems: cachedContextFlags)
if !cachedContextFlags.featureFlags.isEmpty {
flagChangeNotifier.notifyObservers(oldFlags: oldItems.featureFlags, newFlags: flagStore.storedItems.featureFlags)
}
}

self.service.context = self.context
Expand Down
34 changes: 0 additions & 34 deletions LaunchDarkly/LaunchDarkly/Models/IdentifyResult.swift

This file was deleted.

61 changes: 61 additions & 0 deletions LaunchDarkly/LaunchDarkly/Models/IdentifyTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Foundation

/**
Denotes the result of an identify request made through the `LDClient.identify(context: completion:)` method.
*/
public enum IdentifyResult {
/**
The identify request has completed successfully.
*/
case complete
/**
The identify request has received an unrecoverable failure.
*/
case error
/**
The identify request has been replaced with a subsequent request. Read `LDClient.identify(context: completion:)` for more details.
*/
case shed
/**
The identify request exceeded some time out parameter. Read `LDClient.identify(context: timeout: completion)` for more details.
*/
case timeout

init(from: TaskResult) {
switch from {
case .complete:
self = .complete
case .error:
self = .error
case .shed:
self = .shed
}
}
}

/**
When a new `LDContext` is being identified, the SDK has a few choices it can make on how to handle intermediate flag evaluations
until fresh values have been retrieved from the LaunchDarkly APIs.
*/
public enum IdentifyCacheUsage {
/**
`no` will not load any flag values from the cache. Instead it will maintain the current in memory state from the previously identified context.

This method ensures the greatest continuity of experience until the identify network communication resolves.
*/
case no

/**
`yes` will clear the in memory state of any previously known flag values. The SDK will attempt to load cached flag values for the newly identified
context. If no cache is found, the state remains empty until the network request resolves.
*/
case yes

/**
`ifAvailable` will attempt to load cached flag values for the newly identified context. If cached values are found, the in memory state is fully
replaced with those values.

If no cached values are found, the existing in memory state is retained until the network request resolves.
*/
case ifAvailable
}
Loading

0 comments on commit b928345

Please sign in to comment.