Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement shedding identity queue #343

Merged
merged 1 commit into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/ci/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ runs:

- name: Lint the podspec
shell: bash
run: pod lib lint LaunchDarkly.podspec
run: pod lib lint LaunchDarkly.podspec --allow-warnings

- name: Run swiftlint
shell: bash
Expand Down
24 changes: 24 additions & 0 deletions LaunchDarkly.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,15 @@
A380B09A2B60178D00AB64A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */; };
A380B09B2B60178D00AB64A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */; };
A380B09C2B60178D00AB64A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */; };
A3A8BCD22B7EAA89009A77E4 /* SheddingQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A8BCD12B7EAA89009A77E4 /* SheddingQueue.swift */; };
A3A8BCD32B7EAA89009A77E4 /* SheddingQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A8BCD12B7EAA89009A77E4 /* SheddingQueue.swift */; };
A3A8BCD42B7EAA89009A77E4 /* SheddingQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A8BCD12B7EAA89009A77E4 /* SheddingQueue.swift */; };
A3A8BCD52B7EAA89009A77E4 /* SheddingQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A8BCD12B7EAA89009A77E4 /* SheddingQueue.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 */; };
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 @@ -476,6 +485,9 @@
A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDContext.swift; sourceTree = "<group>"; };
A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDApplicationInfo.swift; sourceTree = "<group>"; };
A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
A3A8BCD12B7EAA89009A77E4 /* SheddingQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheddingQueue.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>"; };
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 @@ -565,6 +577,7 @@
837406D321F760640087B22B /* LDTimerSpec.swift */,
831AAE2F20A9E75D00B46DBA /* ThrottlerSpec.swift */,
8354AC75224316C700CDE602 /* Cache */,
A3C6F7612B7FA803005B3B61 /* SheddingQueueSpec.swift */,
);
path = ServiceObjects;
sourceTree = "<group>";
Expand Down Expand Up @@ -681,6 +694,7 @@
8354EFDE1F26380700C05156 /* Event.swift */,
83EBCB9D20D9A0A1003A7142 /* FeatureFlag */,
8354EFDD1F26380700C05156 /* LDConfig.swift */,
A3C6F7632B84EF0C005B3B61 /* IdentifyResult.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -806,6 +820,7 @@
83FEF8D91F2666BF001CF12C /* ServiceObjects */ = {
isa = PBXGroup;
children = (
A3A8BCD12B7EAA89009A77E4 /* SheddingQueue.swift */,
A358D6CF2A4DD45000270C60 /* EnvironmentReporting */,
8354AC742243168800CDE602 /* Cache */,
838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */,
Expand Down Expand Up @@ -1256,6 +1271,7 @@
A3470C3A2B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */,
C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */,
831188592113AE1200D77CB5 /* FlagStore.swift in Sources */,
A3A8BCD52B7EAA89009A77E4 /* SheddingQueue.swift in Sources */,
C443A40D2315AA4D00145710 /* NetworkReporter.swift in Sources */,
A358D6EF2A4DE9A600270C60 /* TVOSEnvironmentReporter.swift in Sources */,
29FE129B280413D4008CC918 /* Util.swift in Sources */,
Expand All @@ -1274,6 +1290,7 @@
B4C9D43B2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */,
C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */,
B468E71324B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */,
A3C6F7672B84EF0C005B3B61 /* IdentifyResult.swift in Sources */,
8354AC732243166900CDE602 /* FeatureFlagCache.swift in Sources */,
8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */,
8311884E2113ADE500D77CB5 /* Event.swift in Sources */,
Expand Down Expand Up @@ -1301,6 +1318,7 @@
B468E71224B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */,
A36EDFCF2853C50B00D91B05 /* ObjcLDContext.swift in Sources */,
831EF34320655E730001C643 /* LDCommon.swift in Sources */,
A3C6F7662B84EF0C005B3B61 /* IdentifyResult.swift in Sources */,
831EF34420655E730001C643 /* LDConfig.swift in Sources */,
A31088212837DC0400184942 /* LDContext.swift in Sources */,
831EF34520655E730001C643 /* LDClient.swift in Sources */,
Expand Down Expand Up @@ -1357,6 +1375,7 @@
83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */,
831EF36620655E730001C643 /* ObjcLDClient.swift in Sources */,
831EF36720655E730001C643 /* ObjcLDConfig.swift in Sources */,
A3A8BCD42B7EAA89009A77E4 /* SheddingQueue.swift in Sources */,
B4C9D43A2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */,
831EF36A20655E730001C643 /* ObjcLDChangedFlag.swift in Sources */,
);
Expand Down Expand Up @@ -1391,6 +1410,7 @@
A3470C372B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */,
C43C37E1236BA050003C1624 /* LDEvaluationDetail.swift in Sources */,
831AAE2C20A9E4F600B46DBA /* Throttler.swift in Sources */,
A3A8BCD22B7EAA89009A77E4 /* SheddingQueue.swift in Sources */,
8354EFE11F26380700C05156 /* LDConfig.swift in Sources */,
29FE1298280413D4008CC918 /* Util.swift in Sources */,
C443A40F23186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */,
Expand All @@ -1409,6 +1429,7 @@
83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */,
B4C9D4382489E20A004A9B03 /* DiagnosticReporter.swift in Sources */,
8347BB0C21F147E100E56BCD /* LDTimer.swift in Sources */,
A3C6F7642B84EF0C005B3B61 /* IdentifyResult.swift in Sources */,
B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */,
8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */,
A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */,
Expand Down Expand Up @@ -1456,6 +1477,7 @@
83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */,
83CFE7D11F7AD8DC0010544E /* DarklyServiceMock.swift in Sources */,
832307AA1F7ECA630029815A /* LDConfigStub.swift in Sources */,
A3C6F7622B7FA803005B3B61 /* SheddingQueueSpec.swift in Sources */,
A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */,
8354AC77224316F800CDE602 /* FeatureFlagCacheSpec.swift in Sources */,
A3047D652A606B6000F568E0 /* IOSEnvironmentReporterSpec.swift in Sources */,
Expand Down Expand Up @@ -1509,6 +1531,7 @@
A3470C382B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */,
C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */,
83D9EC872062DEAB004D7FA6 /* FlagSynchronizer.swift in Sources */,
A3A8BCD32B7EAA89009A77E4 /* SheddingQueue.swift in Sources */,
C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */,
29FE1299280413D4008CC918 /* Util.swift in Sources */,
83D9EC882062DEAB004D7FA6 /* FlagChangeNotifier.swift in Sources */,
Expand All @@ -1527,6 +1550,7 @@
83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */,
8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */,
8354AC712243166900CDE602 /* FeatureFlagCache.swift in Sources */,
A3C6F7652B84EF0C005B3B61 /* IdentifyResult.swift in Sources */,
C443A40323145FB700145710 /* ConnectionInformation.swift in Sources */,
B4C9D4392489E20A004A9B03 /* DiagnosticReporter.swift in Sources */,
B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */,
Expand Down
51 changes: 45 additions & 6 deletions LaunchDarkly/LaunchDarkly/LDClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,53 @@ public class LDClient {
- parameter context: The LDContext set with the desired context.
- parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. (Optional)
*/
@available(*, deprecated, message: "Use LDClient.identify(context: completion:) with non-optional completion parameter")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecating this method so that customers will start moving over to the callback that informs them of success or failure.

public func identify(context: LDContext, completion: (() -> Void)? = nil) {
let dispatch = DispatchGroup()
LDClient.instances?.forEach { _, instance in
dispatch.enter()
instance.internalIdentify(newContext: context, completion: dispatch.leave)
_identify(context: context, sheddable: false) { _ in
if let completion = completion {
completion()
}
}
if let completion = completion {
dispatch.notify(queue: DispatchQueue.global(), execute: completion)
}

/**
The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained.
Normally, the client app should create and set the LDContext and pass that into `start(config: context: completion:)`.
The client app can change the active `context` by calling identify with a new or updated LDContext. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information.
When a new context is set, the LDClient goes offline and sets the new context. If the client was online when the new context was set, it goes online again, subject to a throttling delay if in force (see `setOnline(_: completion:)` for details). A completion may be passed to the identify method to allow a client app to know when fresh flag values for the new context are ready.
While only a single identify request can be active at a time, consumers of this SDK can call this method multiple times. To prevent unnecessary network traffic, these requests are placed
into a sheddable queue. Identify requests will be shed if 1) an existing identify request is in flight, and 2) a third identify has been requested which can be replace the one being shed.
- parameter context: The LDContext set with the desired context.
- parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays.
*/
public func identify(context: LDContext, completion: @escaping (_ result: IdentifyResult) -> Void) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other identify method has an optional completion method. This one isn't optional.

If I continue with the completion method being optional, then the method isn't fully disambiguated. Customers could have code like: LDClient.get()?.identify(context: context, completion: nil) and the compiler wouldn't know which one we meant.

Given that these identify requests might never actually complete, it seems kind of reasonable to force customers listen for those results, just as a measure of good practice for how to use this SDK. What do you think?

If we decide to make it optional, how do we deal with the backwards compatibility naming convention? Keep in mind that if we name it something like identifyWithShedding then in the next major release, customers who took action based on the deprecation notice are going to have to change their code AGAIN.

We could consider making it non-nullable in this. version and loosen that restriction in the next. Or we could wait to see what customer feedback is and add the nullability back at that time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am interested to see what customer feedback is. It doesn't seem unreasonable at this point to require it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can always pass a no-op and ignore it.

_identify(context: context, sheddable: true, 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) {
let work: TaskHandler = { taskCompletion in
let dispatch = DispatchGroup()

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

dispatch.notify(queue: DispatchQueue.global(), execute: taskCompletion)
}

let identifyTask = Task(work: work, sheddable: sheddable) { [self] result in
os_log("%s identity completion with result %s", log: config.logger, type: .debug, typeName(and: #function), String(describing: result))
completion(IdentifyResult(from: result))
}
identifyQueue.enqueue(request: identifyTask)
}

func internalIdentify(newContext: LDContext, completion: (() -> Void)? = nil) {
Expand Down Expand Up @@ -711,6 +749,7 @@ public class LDClient {
}
private var _initialized = false
private var initializedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.initializedQueue")
private var identifyQueue = SheddingQueue()

private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startContext: LDContext?, completion: (() -> Void)? = nil) {
self.serviceFactory = serviceFactory
Expand Down
30 changes: 30 additions & 0 deletions LaunchDarkly/LaunchDarkly/Models/IdentifyResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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. See `LDClient.identify(context: completion:)` for more details.
*/
case shed

init(from: TaskResult) {
switch from {
case .complete:
self = .complete
case .error:
self = .error
case .shed:
self = .shed
}
}
}
68 changes: 68 additions & 0 deletions LaunchDarkly/LaunchDarkly/ServiceObjects/SheddingQueue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Foundation

enum TaskResult {
case complete
case error
case shed
}

typealias TaskHandlerCompletion = () -> Void
typealias TaskHandler = (_ completion: @escaping TaskHandlerCompletion) -> Void
typealias TaskCompletion = (_ result: TaskResult) -> Void

struct Task {
let work: TaskHandler
let sheddable: Bool
let completion: TaskCompletion
}

class SheddingQueue {
private let stateQueue: DispatchQueue = DispatchQueue(label: "StateQueue")
private let identifyQueue: DispatchQueue = DispatchQueue(label: "IdentifyQueue")

private var inFlight: Task?
private var queue: [Task] = []

func enqueue(request: Task) {
stateQueue.async { [self] in
guard inFlight != nil else {
inFlight = request
identifyQueue.async { self.execute() }
return
}

if let lastTask = queue.last, lastTask.sheddable {
queue.removeLast()
lastTask.completion(.shed)
}

queue.append(request)
}
}

private func execute() {
var nextTask: Task?

stateQueue.sync {
nextTask = inFlight
}

if nextTask == nil {
return
}

guard let request = nextTask else { return }

request.work() { [self] in
request.completion(.complete)

stateQueue.sync {
inFlight = queue.first
if inFlight != nil {
queue.remove(at: 0)
identifyQueue.async { self.execute() }
}
}
}
}
}
Loading