Skip to content

Commit fd187ab

Browse files
committed
fix for unit test (+2 squashed commits)
Squashed commits: [6d854db8] TimeoutExecutor [09d316e] added comment
1 parent 9f4d149 commit fd187ab

File tree

2 files changed

+92
-30
lines changed

2 files changed

+92
-30
lines changed

LaunchDarkly/LaunchDarkly/LDClient.swift

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -384,22 +384,18 @@ public class LDClient {
384384
if timeout > LDClient.longTimeoutInterval {
385385
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)
386386
}
387-
388-
var cancel = false
389-
390-
DispatchQueue.global().asyncAfter(deadline: .now() + timeout) {
391-
guard !cancel else { return }
392-
393-
cancel = true
394-
completion(.timeout)
395-
}
396-
397-
identify(context: context, useCache: useCache) { result in
398-
guard !cancel else { return }
399-
400-
cancel = true
401-
completion(result)
402-
}
387+
388+
TimeoutExecutor.run(
389+
timeout: timeout,
390+
queue: .global(),
391+
operation: { done in
392+
self.identify(context: context, useCache: useCache) { result in
393+
done(result)
394+
}
395+
},
396+
timeoutValue: .timeout,
397+
completion: completion
398+
)
403399
}
404400

405401
func internalIdentify(newContext: LDContext, useCache: IdentifyCacheUsage, completion: (() -> Void)? = nil) {
@@ -854,27 +850,25 @@ public class LDClient {
854850
}
855851

856852
static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, context: LDContext? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) {
857-
var completed = false
858853
let internalCompletedQueue: DispatchQueue = DispatchQueue(label: "TimeOutQueue")
859854
if !config.startOnline {
860855
start(serviceFactory: serviceFactory, config: config, context: context)
861856
completion?(true) // offline is considered a short circuited timed out case
862857
} else {
863858
let startTime = Date().timeIntervalSince1970
864-
start(serviceFactory: serviceFactory, config: config, context: context) {
865-
internalCompletedQueue.async {
866-
if startTime + startWaitSeconds > Date().timeIntervalSince1970 && !completed {
867-
completed = true
868-
completion?(false) // false for not timedOut
859+
860+
TimeoutExecutor.run(
861+
timeout: startWaitSeconds,
862+
queue: internalCompletedQueue,
863+
operation: { done in
864+
Self.start(serviceFactory: serviceFactory, config: config, context: context) {
865+
let onTime = startWaitSeconds > Date().timeIntervalSince1970 - startTime
866+
done(!onTime)
869867
}
870-
}
871-
}
872-
internalCompletedQueue.asyncAfter(deadline: .now() + startWaitSeconds) {
873-
if !completed {
874-
completed = true
875-
completion?(true) // true for timedOut
876-
}
877-
}
868+
},
869+
timeoutValue: true,
870+
completion: completion
871+
)
878872
}
879873
}
880874

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Foundation
2+
3+
/// A lightweight utility for executing asynchronous operations with a timeout fallback.
4+
///
5+
/// `TimeoutExecutor` guarantees that the provided `completion` closure is called **exactly once**,
6+
/// either with the result of the asynchronous operation or with a timeout value if the operation
7+
/// does not complete in time.
8+
///
9+
/// ### Typical Usage
10+
/// ```swift
11+
/// TimeoutExecutor.run(
12+
/// timeout: 2.0,
13+
/// queue: .main,
14+
/// operation: { done in
15+
/// service.action() {
16+
/// done("Success")
17+
/// }
18+
/// },
19+
/// timeoutValue: "Timeout",
20+
/// completion: { result in
21+
/// print("Result:", result)
22+
/// }
23+
/// )
24+
/// ```
25+
///
26+
final class TimeoutExecutor {
27+
private init() {}
28+
29+
static func run<T>(
30+
timeout: TimeInterval,
31+
queue: DispatchQueue = .global(),
32+
operation: (@escaping (T) -> Void) -> Void,
33+
timeoutValue: @autoclosure @escaping () -> T,
34+
completion: ((T) -> Void)?
35+
) {
36+
guard let completion = completion else {
37+
operation { _ in
38+
/* ignore result */
39+
}
40+
return
41+
}
42+
43+
let lockQueue = DispatchQueue(label: "launchdarkly.timeout.executor.lock")
44+
var finished = false
45+
46+
func finish(_ value: @autoclosure () -> T) {
47+
var shouldCall = false
48+
lockQueue.sync {
49+
if !finished {
50+
finished = true
51+
shouldCall = true
52+
}
53+
}
54+
guard shouldCall else { return }
55+
queue.async { completion(timeoutValue()) }
56+
}
57+
58+
// Start the user operation (they can call `done` from any queue)
59+
operation { value in
60+
finish(value)
61+
}
62+
63+
// Timeout fallback scheduled
64+
queue.asyncAfter(deadline: .now() + timeout) {
65+
finish(timeoutValue())
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)