From 2a4ac75e1087f146c4487bfab67ee4c0e7e69d70 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 15 Feb 2024 16:41:07 -0500 Subject: [PATCH] feat: Implement shedding identity queue 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. --- LaunchDarkly.xcodeproj/project.pbxproj | 24 +++ LaunchDarkly/LaunchDarkly/LDClient.swift | 49 ++++- .../LaunchDarkly/Models/IdentifyResult.swift | 30 +++ .../ServiceObjects/SheddingQueue.swift | 68 +++++++ .../ServiceObjects/SheddingQueueSpec.swift | 191 ++++++++++++++++++ 5 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/Models/IdentifyResult.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/SheddingQueue.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/SheddingQueueSpec.swift diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index ba8ef5a7..71ceaa97 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -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 */; }; @@ -476,6 +485,9 @@ A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDContext.swift; sourceTree = ""; }; A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDApplicationInfo.swift; sourceTree = ""; }; A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + A3A8BCD12B7EAA89009A77E4 /* SheddingQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheddingQueue.swift; sourceTree = ""; }; + A3C6F7612B7FA803005B3B61 /* SheddingQueueSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheddingQueueSpec.swift; sourceTree = ""; }; + A3C6F7632B84EF0C005B3B61 /* IdentifyResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyResult.swift; sourceTree = ""; }; A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDValueDecoderSpec.swift; sourceTree = ""; }; B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = ""; }; B4265EB024E7390C001CFD2C /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = ""; }; @@ -565,6 +577,7 @@ 837406D321F760640087B22B /* LDTimerSpec.swift */, 831AAE2F20A9E75D00B46DBA /* ThrottlerSpec.swift */, 8354AC75224316C700CDE602 /* Cache */, + A3C6F7612B7FA803005B3B61 /* SheddingQueueSpec.swift */, ); path = ServiceObjects; sourceTree = ""; @@ -681,6 +694,7 @@ 8354EFDE1F26380700C05156 /* Event.swift */, 83EBCB9D20D9A0A1003A7142 /* FeatureFlag */, 8354EFDD1F26380700C05156 /* LDConfig.swift */, + A3C6F7632B84EF0C005B3B61 /* IdentifyResult.swift */, ); path = Models; sourceTree = ""; @@ -806,6 +820,7 @@ 83FEF8D91F2666BF001CF12C /* ServiceObjects */ = { isa = PBXGroup; children = ( + A3A8BCD12B7EAA89009A77E4 /* SheddingQueue.swift */, A358D6CF2A4DD45000270C60 /* EnvironmentReporting */, 8354AC742243168800CDE602 /* Cache */, 838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */, @@ -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 */, @@ -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 */, @@ -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 */, @@ -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 */, ); @@ -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 */, @@ -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 */, @@ -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 */, @@ -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 */, @@ -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 */, diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index acfff012..54c7ddc6 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -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) { @@ -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 diff --git a/LaunchDarkly/LaunchDarkly/Models/IdentifyResult.swift b/LaunchDarkly/LaunchDarkly/Models/IdentifyResult.swift new file mode 100644 index 00000000..4cf2d2ee --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/IdentifyResult.swift @@ -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 + } + } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/SheddingQueue.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/SheddingQueue.swift new file mode 100644 index 00000000..5442a5c3 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/SheddingQueue.swift @@ -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() } + } + } + } + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SheddingQueueSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SheddingQueueSpec.swift new file mode 100644 index 00000000..86b82d91 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SheddingQueueSpec.swift @@ -0,0 +1,191 @@ +import Foundation +import XCTest +@testable import LaunchDarkly + +final class SheddingQueueSpec: XCTestCase { + let noop = { (completion: TaskHandlerCompletion) in completion() } + + func testQueueCanCompleteASingleTask() { + let semaphore = DispatchSemaphore(value: 0) + + let task = Task(work: noop, sheddable: true) { result in + XCTAssertEqual(result, .complete) + semaphore.signal() + } + + let queue = SheddingQueue() + queue.enqueue(request: task) + semaphore.wait() + } + + func testQueueCanCompleteTwoTasks() { + let blockFirstWork = DispatchSemaphore(value: 0) + let blockUntilFullyComplete = DispatchSemaphore(value: 0) + + let delayedWork = { (completion: TaskHandlerCompletion) in + blockFirstWork.wait() + completion() + } + + var executionCount = 0 + let firstTask = Task(work: delayedWork, sheddable: true) { result in + XCTAssertEqual(result, .complete) + executionCount += 1 + } + let finalTask = Task(work: noop, sheddable: true) { result in + XCTAssertEqual(result, .complete) + executionCount += 1 + blockUntilFullyComplete.signal() + } + + let queue = SheddingQueue() + queue.enqueue(request: firstTask) + queue.enqueue(request: finalTask) + blockFirstWork.signal() + blockUntilFullyComplete.wait() + + XCTAssertEqual(executionCount, 2) + } + + func testQueueCanShedSubsequentRequests() { + let blockFirstWork = DispatchSemaphore(value: 0) + let blockUntilFullyComplete = DispatchSemaphore(value: 0) + + let delayedWork = { (completion: TaskHandlerCompletion) in + blockFirstWork.wait() + completion() + } + + var sheddedExecutionCount = 0 + let sheddedWork = { (completion: TaskHandlerCompletion) in + sheddedExecutionCount += 1 + completion() + } + + let firstTask = Task(work: delayedWork, sheddable: true) { result in + XCTAssertEqual(result, .complete) + } + var sheddedCount = 0 + let sheddingTask = Task(work: sheddedWork, sheddable: true) { result in + sheddedCount += 1 + XCTAssertEqual(result, .shed) + } + let finalTask = Task(work: noop, sheddable: true) { result in + XCTAssertEqual(result, .complete) + blockUntilFullyComplete.signal() + } + + let queue = SheddingQueue() + queue.enqueue(request: firstTask) + + queue.enqueue(request: sheddingTask) + queue.enqueue(request: sheddingTask) + queue.enqueue(request: sheddingTask) + queue.enqueue(request: sheddingTask) + + queue.enqueue(request: finalTask) + blockFirstWork.signal() + + blockUntilFullyComplete.wait() + + XCTAssertEqual(sheddedExecutionCount, 0) + XCTAssertEqual(sheddedCount, 4) + } + + func testUnsheddableTasksDoNotShed() { + let blockUntilQueued = DispatchSemaphore(value: 0) + let blockUntilComplete = DispatchSemaphore(value: 0) + + let work = { (completion: TaskHandlerCompletion) in + blockUntilQueued.wait() + completion() + blockUntilQueued.signal() + } + + let group = DispatchGroup() + var shedCount = 0 + var completeCount = 0 + let task = Task(work: work, sheddable: false) { result in + switch result { + case .shed: + shedCount += 1 + case .complete: + completeCount += 1 + default: + XCTFail("Task should either shed or complete.") + } + group.leave() + } + + let queue = SheddingQueue() + + for _ in 0...4 { + group.enter() + queue.enqueue(request: task) + } + blockUntilQueued.signal() + + group.notify(queue: .global()) { + blockUntilComplete.signal() + } + + blockUntilComplete.wait() + + XCTAssertEqual(shedCount, 0) + XCTAssertEqual(completeCount, 5) + } + + func testCanMixShedAndUnsheddable() { + let blockUntilQueued = DispatchSemaphore(value: 0) + let blockUntilComplete = DispatchSemaphore(value: 0) + + let work = { (completion: TaskHandlerCompletion) in + blockUntilQueued.wait() + completion() + blockUntilQueued.signal() + } + + let group = DispatchGroup() + var shedCount = 0 + var completeCount = 0 + let completion: TaskCompletion = { result in + switch result { + case .shed: + shedCount += 1 + case .complete: + completeCount += 1 + default: + XCTFail("Task should either shed or complete.") + } + group.leave() + } + let sheddableTask = Task(work: work, sheddable: true, completion: completion) + let unsheddableTask = Task(work: work, sheddable: false, completion: completion) + + let queue = SheddingQueue() + + group.enter() + queue.enqueue(request: sheddableTask) // Will complete, first job + group.enter() + queue.enqueue(request: unsheddableTask) // Will complete, unsheddable + group.enter() + queue.enqueue(request: sheddableTask) // Will be shed + group.enter() + queue.enqueue(request: sheddableTask) // Will be shed + group.enter() + queue.enqueue(request: unsheddableTask) // Will complete, unsheddable + group.enter() + queue.enqueue(request: unsheddableTask) // Will complete, unsheddable + + blockUntilQueued.signal() + + group.notify(queue: .global()) { + blockUntilComplete.signal() + } + + blockUntilComplete.wait() + + XCTAssertEqual(shedCount, 2) + XCTAssertEqual(completeCount, 4) + } +}