Skip to content

Commit 1b9d48c

Browse files
Merge pull request #124 from CrownedPhoenix/fix/serial-execution-strategy
fix: Serial Execution Strategy
2 parents 7c910f2 + d9379d0 commit 1b9d48c

File tree

3 files changed

+56
-28
lines changed

3 files changed

+56
-28
lines changed

Sources/GraphQL/Execution/Execute.swift

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -118,24 +118,29 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy,
118118
path: IndexPath,
119119
fields: OrderedDictionary<String, [Field]>
120120
) throws -> Future<OrderedDictionary<String, Any>> {
121-
var results = OrderedDictionary<String, Future<Any>>()
122-
123-
try fields.forEach { field in
124-
let fieldASTs = field.value
125-
let fieldPath = path.appending(field.key)
126-
127-
let result = try resolveField(
128-
exeContext: exeContext,
129-
parentType: parentType,
130-
source: sourceValue,
131-
fieldASTs: fieldASTs,
132-
path: fieldPath
133-
)
134-
135-
results[field.key] = result.map { $0 ?? Map.null }
136-
}
137-
138-
return results.flatten(on: exeContext.eventLoopGroup)
121+
var results = OrderedDictionary<String, Any>()
122+
123+
return fields
124+
.reduce(exeContext.eventLoopGroup.next().makeSucceededVoidFuture()) { prev, field in
125+
// We use ``flatSubmit`` here to avoid a stack overflow issue with EventLoopFutures.
126+
// See: https://github.com/apple/swift-nio/issues/970
127+
exeContext.eventLoopGroup.next().flatSubmit {
128+
prev.tryFlatMap {
129+
let fieldASTs = field.value
130+
let fieldPath = path.appending(field.key)
131+
132+
return try resolveField(
133+
exeContext: exeContext,
134+
parentType: parentType,
135+
source: sourceValue,
136+
fieldASTs: fieldASTs,
137+
path: fieldPath
138+
).map { result in
139+
results[field.key] = result ?? Map.null
140+
}
141+
}
142+
}
143+
}.map { results }
139144
}
140145
}
141146

Sources/GraphQL/Utilities/NIO+Extensions.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,31 @@ public protocol FutureType {
101101
extension Future: FutureType {
102102
public typealias Expectation = Value
103103
}
104+
105+
// Copied from https://github.com/vapor/async-kit/blob/e2f741640364c1d271405da637029ea6a33f754e/Sources/AsyncKit/EventLoopFuture/Future%2BTry.swift
106+
// in order to avoid full package dependency.
107+
public extension EventLoopFuture {
108+
func tryFlatMap<NewValue>(
109+
file _: StaticString = #file, line _: UInt = #line,
110+
_ callback: @escaping (Value) throws -> EventLoopFuture<NewValue>
111+
) -> EventLoopFuture<NewValue> {
112+
/// When the current `EventLoopFuture<Value>` is fulfilled, run the provided callback,
113+
/// which will provide a new `EventLoopFuture`.
114+
///
115+
/// This allows you to dynamically dispatch new asynchronous tasks as phases in a
116+
/// longer series of processing steps. Note that you can use the results of the
117+
/// current `EventLoopFuture<Value>` when determining how to dispatch the next operation.
118+
///
119+
/// The key difference between this method and the regular `flatMap` is error handling.
120+
///
121+
/// With `tryFlatMap`, the provided callback _may_ throw Errors, causing the returned `EventLoopFuture<Value>`
122+
/// to report failure immediately after the completion of the original `EventLoopFuture`.
123+
flatMap { [eventLoop] value in
124+
do {
125+
return try callback(value)
126+
} catch {
127+
return eventLoop.makeFailedFuture(error)
128+
}
129+
}
130+
}
131+
}

Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,10 @@ class FieldExecutionStrategyTests: XCTestCase {
1515
"sleep": GraphQLField(
1616
type: GraphQLString,
1717
resolve: { _, _, _, eventLoopGroup, _ in
18-
let group = DispatchGroup()
19-
group.enter()
20-
21-
DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) {
22-
group.leave()
18+
eventLoopGroup.next().makeSucceededVoidFuture().map {
19+
Thread.sleep(forTimeInterval: 0.1)
20+
return "z"
2321
}
24-
25-
group.wait()
26-
return eventLoopGroup.next().makeSucceededFuture("z")
2722
}
2823
),
2924
"bang": GraphQLField(
@@ -251,7 +246,7 @@ class FieldExecutionStrategyTests: XCTestCase {
251246
eventLoopGroup: eventLoopGroup
252247
).wait())
253248
XCTAssertEqual(result.value, multiExpected)
254-
// XCTAssertEqualWithAccuracy(1.0, result.seconds, accuracy: 0.5)
249+
// XCTAssertEqualWithAccuracy(1.0, result.seconds, accuracy: 0.5)
255250
}
256251

257252
func testSerialFieldExecutionStrategyWithMultipleFieldErrors() throws {
@@ -300,7 +295,7 @@ class FieldExecutionStrategyTests: XCTestCase {
300295
eventLoopGroup: eventLoopGroup
301296
).wait())
302297
XCTAssertEqual(result.value, multiExpected)
303-
// XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25)
298+
// XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25)
304299
}
305300

306301
func testConcurrentDispatchFieldExecutionStrategyWithMultipleFieldErrors() throws {

0 commit comments

Comments
 (0)