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 2a4ac75
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 6 deletions.
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
49 changes: 43 additions & 6 deletions LaunchDarkly/LaunchDarkly/LDClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,51 @@ 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. (Optional)
*/
public func identify(context: LDContext, completion: @escaping (_ result: IdentifyResult) -> Void) {
_identify(context: context, sheddable: true, completion: completion)
}

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 +747,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 2a4ac75

Please sign in to comment.