Skip to content

Commit ff3b459

Browse files
docs: Add docs about the ProviderOperationsQueue
Signed-off-by: Fabrizio Demaria <fabrizio.f.demaria@gmail.com>
1 parent 735ed28 commit ff3b459

File tree

1 file changed

+170
-0
lines changed

1 file changed

+170
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# AsyncProviderOperationsQueue
2+
3+
## Overview
4+
5+
`AsyncProviderOperationsQueue` is a specialized serial async task queue that provides **operation-type-aware last-wins semantics** for handling OpenFeature provider operations. It ensures thread-safe, ordered execution of async operations while optimizing performance by coalescing redundant operations.
6+
7+
## Key Characteristics
8+
9+
- **Serial Execution**: Operations execute one at a time, preserving order
10+
- **Actor-based**: Thread-safe through Swift's actor isolation
11+
- **Smart Coalescing**: Automatically skips redundant operations based on last-wins semantics
12+
- **Continuation Management**: All callers receive completion notification, even if their operation was skipped
13+
14+
## Core Concepts
15+
16+
### Operation Types
17+
18+
The queue distinguishes between two types of operations:
19+
20+
1. **Non-Last-Wins (`lastWins: false`)**
21+
- Always executes
22+
- Processes in strict FIFO order
23+
- Used for critical state changes that must not be skipped
24+
- Examples: `setProvider()`, `clearProvider()`
25+
26+
2. **Last-Wins (`lastWins: true`)**
27+
- May be skipped if superseded by newer last-wins operations
28+
- Optimizes away intermediate states
29+
- Used for operations where only the final state matters
30+
- Examples: `setEvaluationContext()`
31+
32+
### Batching Logic
33+
34+
When processing the queue, operations are grouped into "batches":
35+
36+
- **Batch 1**: A single non-last-wins operation
37+
- **Batch 2**: Consecutive last-wins operations → only the last one executes
38+
39+
## How It Works
40+
41+
### Architecture
42+
43+
```
44+
┌─────────────────────────────────────────┐
45+
│ AsyncProviderOperationsQueue (Actor) │
46+
├─────────────────────────────────────────┤
47+
│ - queue: [QueuedOperation] │
48+
│ - currentTask: Task<Void, Never>? │
49+
├─────────────────────────────────────────┤
50+
│ + run(lastWins:operation:) async │
51+
│ - processNext() │
52+
└─────────────────────────────────────────┘
53+
54+
QueuedOperation {
55+
operation: () async -> Void
56+
continuation: CheckedContinuation<Void, Never>
57+
lastWins: Bool
58+
}
59+
```
60+
61+
### Execution Flow
62+
63+
```
64+
1. Caller invokes run(lastWins:operation:)
65+
66+
2. Operation wrapped with continuation and enqueued
67+
68+
3. If no task running → processNext()
69+
70+
4. Determine batch type
71+
├─ Non-last-wins: Execute single operation
72+
└─ Last-wins: Find consecutive last-wins ops
73+
→ Execute only the LAST one
74+
→ Skip all others
75+
76+
5. Resume ALL continuations (skipped + executed)
77+
78+
6. Recursively processNext() until queue empty
79+
```
80+
81+
### Example Scenarios
82+
83+
#### Scenario 1: Non-Last-Wins Operations
84+
85+
```swift
86+
// Queue: Empty, currentTask: nil
87+
88+
await queue.run(lastWins: false) { setProvider(A) } // Op1
89+
await queue.run(lastWins: false) { setProvider(B) } // Op2
90+
await queue.run(lastWins: false) { clearProvider() } // Op3
91+
92+
// Execution order:
93+
// 1. setProvider(A) ✓ Executed
94+
// 2. setProvider(B) ✓ Executed
95+
// 3. clearProvider() ✓ Executed
96+
// All three operations execute in order
97+
```
98+
99+
#### Scenario 2: Last-Wins Coalescing
100+
101+
```swift
102+
// Queue: Empty, currentTask: nil
103+
104+
await queue.run(lastWins: true) { setContext(ctx1) } // Op1
105+
await queue.run(lastWins: true) { setContext(ctx2) } // Op2
106+
await queue.run(lastWins: true) { setContext(ctx3) } // Op3
107+
108+
// Assume Op1 starts executing before Op2/Op3 are enqueued:
109+
// 1. setContext(ctx1) ✓ Executed (already running)
110+
// 2. setContext(ctx2) ✗ Skipped (superseded by ctx3)
111+
// 3. setContext(ctx3) ✓ Executed (last in batch)
112+
113+
// Result: Only ctx1 and ctx3 execute
114+
// Op2's continuation still resumes immediately when Op3 completes
115+
```
116+
117+
#### Scenario 3: Mixed Operations
118+
119+
```swift
120+
// Queue: Empty, currentTask: nil
121+
122+
await queue.run(lastWins: false) { setProvider(A) } // Op1
123+
await queue.run(lastWins: true) { setContext(ctx1) } // Op2
124+
await queue.run(lastWins: true) { setContext(ctx2) } // Op3
125+
await queue.run(lastWins: false) { setProvider(B) } // Op4
126+
await queue.run(lastWins: true) { setContext(ctx3) } // Op5
127+
128+
// Execution flow:
129+
// Batch 1: [Op1] non-last-wins
130+
// → setProvider(A) ✓ Executed
131+
132+
// Batch 2: [Op2, Op3] consecutive last-wins
133+
// → setContext(ctx1) ✗ Skipped
134+
// → setContext(ctx2) ✓ Executed (last in batch)
135+
136+
// Batch 3: [Op4] non-last-wins
137+
// → setProvider(B) ✓ Executed
138+
139+
// Batch 4: [Op5] last-wins
140+
// → setContext(ctx3) ✓ Executed
141+
142+
// Total executions: Op1, Op2(skipped), Op3, Op4, Op5
143+
```
144+
145+
## Implementation Details
146+
147+
### Actor Isolation
148+
149+
The queue is implemented as a Swift `actor`, providing:
150+
- Automatic serialization of all property access
151+
- Thread-safe state management
152+
- No manual locking required
153+
154+
### Continuation Management
155+
156+
```swift
157+
await withCheckedContinuation { continuation in
158+
queue.append(QueuedOperation(
159+
operation: operation,
160+
continuation: continuation,
161+
lastWins: lastWins
162+
))
163+
// ...
164+
}
165+
```
166+
167+
**Key Points:**
168+
- Each caller gets a continuation that suspends their async context
169+
- Continuations resume when the operation completes OR is skipped
170+
- This ensures all callers receive notification, preventing deadlocks

0 commit comments

Comments
 (0)