Skip to content

Commit 8cc95c9

Browse files
test: Polish concurrency tests
Signed-off-by: Fabrizio Demaria <fabrizio.f.demaria@gmail.com>
1 parent 14fa5e7 commit 8cc95c9

File tree

2 files changed

+17
-70
lines changed

2 files changed

+17
-70
lines changed

Sources/OpenFeature/OpenFeatureAPI.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Foundation
33

44
/// Simple serial async task queue for serializing operations
55
private actor AsyncSerialQueue {
6-
private var last: Task<Void, Never>? = nil
6+
private var last: Task<Void, Never>?
77

88
/// Runs the given operation after previously enqueued work completes.
99
func run(_ operation: @Sendable @escaping () async -> Void) async {
Lines changed: 16 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
21
import XCTest
32
import Combine
43
@testable import OpenFeature
54

65
class ConcurrencyRaceConditionTests: XCTestCase {
7-
86
override func setUp() {
97
super.setUp()
108
OpenFeatureAPI.shared.clearProvider()
@@ -23,41 +21,39 @@ class ConcurrencyRaceConditionTests: XCTestCase {
2321

2422
let concurrentOperations = 100
2523
let expectedTargetingKeys = Set((0..<concurrentOperations).map { "user\($0)" })
26-
24+
2725
await withTaskGroup(of: Void.self) { group in
2826
for i in 0..<concurrentOperations {
2927
group.addTask {
3028
let ctx = ImmutableContext(
3129
targetingKey: "user\(i)",
3230
structure: ImmutableStructure(attributes: [
3331
"id": .integer(Int64(i)),
34-
"timestamp": .string("\(Date().timeIntervalSince1970)")
32+
"timestamp": .string("\(Date().timeIntervalSince1970)"),
3533
])
3634
)
37-
38-
// This should trigger the race condition in updateContext
35+
3936
await OpenFeatureAPI.shared.setEvaluationContextAndWait(evaluationContext: ctx)
4037
}
4138
}
4239
}
43-
40+
4441
cancellable.cancel()
45-
46-
// Verify final state is consistent and correct
42+
4743
let finalContext = OpenFeatureAPI.shared.getEvaluationContext()
4844
XCTAssertNotNil(finalContext, "Final evaluation context should not be nil after concurrent operations")
49-
45+
5046
if let context = finalContext {
5147
let targetingKey = context.getTargetingKey()
5248
XCTAssertTrue(
5349
expectedTargetingKeys.contains(targetingKey),
5450
"Final targeting key '\(targetingKey)' should be one of the expected keys from concurrent operations"
5551
)
56-
52+
5753
let contextMap = context.asObjectMap()
5854
XCTAssertTrue(contextMap.keys.contains("id"), "Context should contain 'id' attribute")
5955
XCTAssertTrue(contextMap.keys.contains("timestamp"), "Context should contain 'timestamp' attribute")
60-
56+
6157
if let idValue = contextMap["id"] as? Int64 {
6258
let expectedId = Int64(targetingKey.replacingOccurrences(of: "user", with: ""))!
6359
XCTAssertEqual(idValue, expectedId, "Context 'id' should match the targeting key number")
@@ -67,15 +63,11 @@ class ConcurrencyRaceConditionTests: XCTestCase {
6763
}
6864
}
6965

70-
/// Test the specific race condition between setProvider and setEvaluationContext
71-
/// This was the main issue identified by the external reviewer
7266
func testSetProviderVsSetEvaluationContextRaceCondition() async throws {
7367
let concurrentOperations = 50
7468

7569
await withTaskGroup(of: Void.self) { group in
76-
// Concurrently set providers and evaluation contexts
7770
for i in 0..<concurrentOperations {
78-
// Set provider operations
7971
group.addTask {
8072
let provider = MockProvider()
8173
let ctx = ImmutableContext(
@@ -85,7 +77,6 @@ class ConcurrencyRaceConditionTests: XCTestCase {
8577
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: ctx)
8678
}
8779

88-
// Set evaluation context operations
8980
group.addTask {
9081
let ctx = ImmutableContext(
9182
targetingKey: "context-user\(i)",
@@ -96,20 +87,18 @@ class ConcurrencyRaceConditionTests: XCTestCase {
9687
}
9788
}
9889

99-
// Verify the API is in a consistent state
10090
let finalState = OpenFeatureAPI.shared.getState()
10191
XCTAssertNotNil(finalState.provider, "Provider should not be nil after concurrent operations")
10292
XCTAssertNotNil(finalState.evaluationContext, "Evaluation context should not be nil after concurrent operations")
103-
XCTAssertTrue([.ready, .error, .fatal].contains(finalState.providerStatus), "Provider status should be in a valid final state")
104-
105-
// Verify the final context has expected structure from one of the operations
93+
XCTAssertTrue([.ready].contains(finalState.providerStatus), "Provider status should be in a valid final state")
94+
10695
if let context = finalState.evaluationContext {
10796
let targetingKey = context.getTargetingKey()
10897
XCTAssertTrue(
10998
targetingKey.hasPrefix("provider-user") || targetingKey.hasPrefix("context-user"),
11099
"Final targeting key '\(targetingKey)' should be from one of the concurrent operations"
111100
)
112-
101+
113102
let contextMap = context.asObjectMap()
114103
let hasProviderAttribute = contextMap.keys.contains("provider")
115104
let hasContextAttribute = contextMap.keys.contains("context")
@@ -120,62 +109,21 @@ class ConcurrencyRaceConditionTests: XCTestCase {
120109
}
121110
}
122111

123-
/// Test the race condition between provider initialization and context updates
124-
func testProviderInitializationVsContextUpdateRaceCondition() async throws {
125-
let concurrentOperations = 30
126-
127-
await withTaskGroup(of: Void.self) { group in
128-
for i in 0..<concurrentOperations {
129-
// Provider initialization with context
130-
group.addTask {
131-
let provider = MockProvider()
132-
let initialCtx = ImmutableContext(
133-
targetingKey: "init-user\(i)",
134-
structure: ImmutableStructure(attributes: ["init": .integer(Int64(i))])
135-
)
136-
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: initialCtx)
137-
}
138-
139-
// Immediate context updates
140-
group.addTask {
141-
let updateCtx = ImmutableContext(
142-
targetingKey: "update-user\(i)",
143-
structure: ImmutableStructure(attributes: ["update": .integer(Int64(i))])
144-
)
145-
await OpenFeatureAPI.shared.setEvaluationContextAndWait(evaluationContext: updateCtx)
146-
}
147-
148-
// Clear provider operations
149-
group.addTask {
150-
OpenFeatureAPI.shared.clearProvider()
151-
}
152-
}
153-
}
154-
155-
// Verify the API ends in a consistent state
156-
let finalState = OpenFeatureAPI.shared.getState()
157-
XCTAssertTrue([.notReady, .ready, .error, .fatal].contains(finalState.providerStatus))
158-
}
159-
160-
/// Test high-frequency state changes to stress test synchronization
161112
func testHighFrequencyStateChangesRaceCondition() async throws {
162113
let highFrequencyOperations = 200
163114
let startTime = Date()
164115

165116
await withTaskGroup(of: Void.self) { group in
166117
for i in 0..<highFrequencyOperations {
167118
group.addTask {
168-
// Rapid fire operations
169119
let provider = MockProvider()
170120
let ctx = ImmutableContext(
171121
targetingKey: "rapid-user\(i)",
172122
structure: ImmutableStructure(attributes: [
173123
"iteration": .integer(Int64(i)),
174-
"timestamp": .string("\(Date().timeIntervalSince1970)")
124+
"timestamp": .string("\(Date().timeIntervalSince1970)"),
175125
])
176126
)
177-
178-
// Alternate between different operations
179127
switch i % 4 {
180128
case 0:
181129
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)
@@ -198,26 +146,25 @@ class ConcurrencyRaceConditionTests: XCTestCase {
198146
// Verify operations completed in reasonable time (no deadlocks)
199147
XCTAssertLessThan(duration, 10.0, "Operations took too long, possible deadlock")
200148

201-
// Verify final state is consistent
202149
let finalState = OpenFeatureAPI.shared.getState()
203150
XCTAssertTrue(
204-
[ProviderStatus.notReady, .ready, .error, .fatal].contains(finalState.providerStatus),
151+
[ProviderStatus.ready].contains(finalState.providerStatus),
205152
"Provider status '\(finalState.providerStatus)' should be in a valid state after high-frequency operations"
206153
)
207-
208-
// If we have a provider and context, verify they're consistent
209154
if finalState.provider != nil && finalState.evaluationContext != nil {
210155
let context = finalState.evaluationContext!
211156
let targetingKey = context.getTargetingKey()
212157
XCTAssertTrue(
213158
targetingKey.hasPrefix("rapid-user"),
214159
"Final targeting key '\(targetingKey)' should be from the rapid operations if context exists"
215160
)
216-
161+
217162
let contextMap = context.asObjectMap()
218163
if contextMap.keys.contains("iteration") {
219164
XCTAssertTrue(contextMap.keys.contains("timestamp"), "Context with iteration should also have timestamp")
220165
}
166+
} else {
167+
XCTFail()
221168
}
222169
}
223170
}

0 commit comments

Comments
 (0)