Skip to content

Commit 70568bf

Browse files
feat: Adds serial batch execution option to AsyncDataLoader
1 parent e043f6e commit 70568bf

3 files changed

Lines changed: 87 additions & 2 deletions

File tree

Sources/AsyncDataLoader/DataLoader.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,16 @@ public actor DataLoader<Key: Hashable & Sendable, Value: Sendable> {
175175
// If a maxBatchSize was provided and the queue is longer, then segment the
176176
// queue into multiple batches, otherwise treat the queue as a single batch.
177177
if let maxBatchSize = options.maxBatchSize, maxBatchSize > 0, maxBatchSize < batch.count {
178-
try await batch.chunks(ofCount: maxBatchSize).asyncForEach { slicedBatch in
179-
try await self.executeBatch(batch: Array(slicedBatch))
178+
let chunks = batch.chunks(ofCount: maxBatchSize)
179+
switch options.executionStrategy.option {
180+
case .parallel:
181+
try await chunks.asyncForEach { slicedBatch in
182+
try await self.executeBatch(batch: Array(slicedBatch))
183+
}
184+
case .serial:
185+
for chunk in chunks {
186+
try await self.executeBatch(batch: Array(chunk))
187+
}
180188
}
181189
} else {
182190
try await executeBatch(batch: batch)

Sources/AsyncDataLoader/DataLoaderOptions.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public struct DataLoaderOptions<Key: Hashable, Value>: Sendable {
2020
/// This is irrelevant if batching is disabled.
2121
public let executionPeriod: UInt64?
2222

23+
/// Default `parallel`. Defines the strategy for execution when
24+
/// the execution queue exceeds `maxBatchSize`.
25+
/// This is irrelevant if batching is disabled.
26+
public let executionStrategy: ExecutionStrategy
27+
2328
/// Default `nil`. Produces cache key for a given load key. Useful
2429
/// when objects are keys and two objects should be considered equivalent.
2530
public let cacheKeyFunction: (@Sendable (Key) -> Key)?
@@ -29,12 +34,38 @@ public struct DataLoaderOptions<Key: Hashable, Value>: Sendable {
2934
cachingEnabled: Bool = true,
3035
maxBatchSize: Int? = nil,
3136
executionPeriod: UInt64? = 2_000_000,
37+
executionStrategy: ExecutionStrategy = .parallel,
3238
cacheKeyFunction: (@Sendable (Key) -> Key)? = nil
3339
) {
3440
self.batchingEnabled = batchingEnabled
3541
self.cachingEnabled = cachingEnabled
3642
self.executionPeriod = executionPeriod
43+
self.executionStrategy = executionStrategy
3744
self.maxBatchSize = maxBatchSize
3845
self.cacheKeyFunction = cacheKeyFunction
3946
}
47+
48+
/// The strategy for execution when the execution queue exceeds `maxBatchSize`.
49+
public struct ExecutionStrategy: Sendable {
50+
let option: Option
51+
52+
private init(option: Option) {
53+
self.option = option
54+
}
55+
56+
/// Batches within a single execution will be executed simultaneously
57+
public static var parallel: Self {
58+
.init(option: .parallel)
59+
}
60+
61+
/// Batches within a single execution will be executed one-at-a-time
62+
public static var serial: Self {
63+
.init(option: .serial)
64+
}
65+
66+
enum Option {
67+
case parallel
68+
case serial
69+
}
70+
}
4071
}

Tests/AsyncDataLoaderTests/DataLoaderTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,52 @@ final class DataLoaderTests: XCTestCase {
140140
XCTAssertEqual(calls.last?.count, 1)
141141
}
142142

143+
func testSerialExecution() async throws {
144+
let loadCalls = Concurrent<[[Int]]>([])
145+
146+
let identityLoader = DataLoader<Int, Int>(
147+
options: DataLoaderOptions(
148+
batchingEnabled: true,
149+
maxBatchSize: 2,
150+
executionPeriod: nil,
151+
executionStrategy: .serial
152+
)
153+
) { keys in
154+
await loadCalls.mutating { $0.append(keys) }
155+
156+
return keys.map { DataLoaderValue.success($0) }
157+
}
158+
159+
async let value1 = identityLoader.load(key: 1)
160+
async let value2 = identityLoader.load(key: 2)
161+
async let value3 = identityLoader.load(key: 3)
162+
163+
try await Task.sleep(nanoseconds: sleepConstant)
164+
165+
var didFailWithError: Error?
166+
167+
do {
168+
_ = try await identityLoader.execute()
169+
} catch {
170+
didFailWithError = error
171+
}
172+
173+
XCTAssertNil(didFailWithError)
174+
175+
let result1 = try await value1
176+
let result2 = try await value2
177+
let result3 = try await value3
178+
179+
XCTAssertEqual(result1, 1)
180+
XCTAssertEqual(result2, 2)
181+
XCTAssertEqual(result3, 3)
182+
183+
let calls = await loadCalls.wrappedValue
184+
185+
XCTAssertEqual(calls.first?.count, 2)
186+
XCTAssertEqual(calls.last?.count, 1)
187+
}
188+
143189
/// Coalesces identical requests
144190
func testCoalescesIdenticalRequests() async throws {
145191
let loadCalls = Concurrent<[[Int]]>([])

0 commit comments

Comments
 (0)