Skip to content

Commit

Permalink
feat: Implement shedding identity queue
Browse files Browse the repository at this point in the history
Previously, customers could queue a boundless limit of identify
requests. The SDK would work its way through this FIFO queue, processing
all intermediate but unnecessary requests.

With this change, intermediate identify requests will be shed from the
processing queue.

NOTE: To preserve backwards compatibility, the original identify method
will queue up "unsheddable" tasks which will continue to queue as
before. Usage of the new `identify` method will allow developers to
opt-in to this new behavior.
  • Loading branch information
keelerm84 committed Feb 20, 2024
1 parent 7ff2ffb commit 9054985
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 7 deletions.
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")
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) {
_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

0 comments on commit 9054985

Please sign in to comment.