Skip to content

Commit 2deb22b

Browse files
williambaileypaulofaria
authored andcommitted
Implement ConcurrentDispatchFieldExecutionStrategy (#13)
1 parent 513c473 commit 2deb22b

File tree

7 files changed

+457
-44
lines changed

7 files changed

+457
-44
lines changed

README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# GraphQL
1+
# GraphQL
22

33
The Swift implementation for GraphQL, a query language for APIs created by Facebook.
44

@@ -36,7 +36,7 @@ import PackageDescription
3636

3737
let package = Package(
3838
dependencies: [
39-
.Package(url: "https://github.com/GraphQLSwift/GraphQL.git", majorVersion: 0, minor: 2),
39+
.Package(url: "https://github.com/GraphQLSwift/GraphQL.git", majorVersion: 0),
4040
]
4141
)
4242
```
@@ -104,13 +104,36 @@ Output:
104104
"line": 1,
105105
"column": 3
106106
}
107-
],
107+
],
108108
"message": "Cannot query field \"boyhowdy\" on type \"RootQueryType\"."
109109
}
110110
]
111111
}
112112
```
113113

114+
### Field Execution Strategies
115+
116+
Depending on your needs you can alter the field execution strategies used for field value resolution.
117+
118+
By default the `SerialFieldExecutionStrategy` is used for all operation types (`query`, `mutation`, `subscription`).
119+
120+
To use a different strategy simply provide it to the `graphql` function:
121+
122+
```swift
123+
try graphql(
124+
queryStrategy: ConcurrentDispatchFieldExecutionStrategy(),
125+
schema: schema,
126+
request: query
127+
)
128+
```
129+
130+
The following strategies are available:
131+
132+
* `SerialFieldExecutionStrategy`
133+
* `ConcurrentDispatchFieldExecutionStrategy`
134+
135+
**Please note:** Not all strategies are applicable for all operation types.
136+
114137
## License
115138

116139
This project is released under the MIT license. See [LICENSE](LICENSE) for details.
@@ -127,4 +150,4 @@ This project is released under the MIT license. See [LICENSE](LICENSE) for detai
127150
[codecov-badge]: https://codecov.io/gh/GraphQLSwift/GraphQL/branch/master/graph/badge.svg
128151
[codecov-url]: https://codecov.io/gh/GraphQLSwift/GraphQL
129152
[codebeat-badge]: https://codebeat.co/badges/13293962-d1d8-4906-8e62-30a2cbb66b38
130-
[codebeat-url]: https://codebeat.co/projects/github-com-graphqlswift-graphql
153+
[codebeat-url]: https://codebeat.co/projects/github-com-graphqlswift-graphql

Sources/GraphQL/Execution/Execute.swift

Lines changed: 123 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Dispatch
2+
13
/**
24
* Terminology
35
*
@@ -26,21 +28,33 @@
2628
*/
2729
public final class ExecutionContext {
2830

29-
let queryStrategy: FieldExecutionStrategy
30-
let mutationStrategy: FieldExecutionStrategy
31-
let subscriptionStrategy: FieldExecutionStrategy
32-
let schema: GraphQLSchema
33-
let fragments: [String: FragmentDefinition]
34-
let rootValue: Any
35-
let contextValue: Any
36-
let operation: OperationDefinition
37-
let variableValues: [String: Map]
38-
var errors: [GraphQLError]
31+
let queryStrategy: QueryFieldExecutionStrategy
32+
let mutationStrategy: MutationFieldExecutionStrategy
33+
let subscriptionStrategy: SubscriptionFieldExecutionStrategy
34+
public let schema: GraphQLSchema
35+
public let fragments: [String: FragmentDefinition]
36+
public let rootValue: Any
37+
public let contextValue: Any
38+
public let operation: OperationDefinition
39+
public let variableValues: [String: Map]
40+
41+
private var errorsSemaphore = DispatchSemaphore(value: 1)
42+
private var _errors: [GraphQLError]
43+
44+
var errors: [GraphQLError] {
45+
get {
46+
errorsSemaphore.wait()
47+
defer {
48+
errorsSemaphore.signal()
49+
}
50+
return _errors
51+
}
52+
}
3953

4054
init(
41-
queryStrategy: FieldExecutionStrategy,
42-
mutationStrategy: FieldExecutionStrategy,
43-
subscriptionStrategy: FieldExecutionStrategy,
55+
queryStrategy: QueryFieldExecutionStrategy,
56+
mutationStrategy: MutationFieldExecutionStrategy,
57+
subscriptionStrategy: SubscriptionFieldExecutionStrategy,
4458
schema: GraphQLSchema,
4559
fragments: [String: FragmentDefinition],
4660
rootValue: Any,
@@ -58,9 +72,17 @@ public final class ExecutionContext {
5872
self.contextValue = contextValue
5973
self.operation = operation
6074
self.variableValues = variableValues
61-
self.errors = errors
75+
self._errors = errors
76+
}
6277

78+
public func append(error: GraphQLError) {
79+
errorsSemaphore.wait()
80+
defer {
81+
errorsSemaphore.signal()
82+
}
83+
_errors.append(error)
6384
}
85+
6486
}
6587

6688
public protocol FieldExecutionStrategy {
@@ -73,10 +95,17 @@ public protocol FieldExecutionStrategy {
7395
) throws -> [String: Any]
7496
}
7597

98+
public protocol MutationFieldExecutionStrategy: FieldExecutionStrategy {}
99+
public protocol QueryFieldExecutionStrategy: FieldExecutionStrategy {}
100+
public protocol SubscriptionFieldExecutionStrategy: FieldExecutionStrategy {}
101+
76102
/**
77103
* Serial field execution strategy that's suitable for the "Evaluating selection sets" section of the spec for "write" mode.
78104
*/
79-
public struct SerialFieldExecutionStrategy: FieldExecutionStrategy {
105+
public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, MutationFieldExecutionStrategy, SubscriptionFieldExecutionStrategy {
106+
107+
public init () {}
108+
80109
public func executeFields(
81110
exeContext: ExecutionContext,
82111
parentType: GraphQLObjectType,
@@ -103,16 +132,88 @@ public struct SerialFieldExecutionStrategy: FieldExecutionStrategy {
103132
}
104133
}
105134

135+
/**
136+
* Serial field execution strategy that's suitable for the "Evaluating selection sets" section of the spec for "read" mode.
137+
*
138+
* Each field is resolved as an individual task on a concurrent dispatch queue.
139+
*/
140+
public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrategy, SubscriptionFieldExecutionStrategy {
141+
142+
let dispatchQueue: DispatchQueue
143+
144+
public init(dispatchQueue: DispatchQueue) {
145+
self.dispatchQueue = dispatchQueue
146+
}
147+
148+
public init(queueLabel: String = "GraphQL field execution", queueQoS: DispatchQoS = .userInitiated) {
149+
self.dispatchQueue = DispatchQueue(
150+
label: queueLabel,
151+
qos: queueQoS,
152+
attributes: .concurrent
153+
)
154+
}
155+
156+
public func executeFields(
157+
exeContext: ExecutionContext,
158+
parentType: GraphQLObjectType,
159+
sourceValue: Any,
160+
path: [IndexPathElement],
161+
fields: [String: [Field]]
162+
) throws -> [String: Any] {
163+
164+
let resultsQueue = DispatchQueue(
165+
label: "\(dispatchQueue.label) results",
166+
qos: dispatchQueue.qos
167+
)
168+
let group = DispatchGroup()
169+
var results: [String: Any] = [:]
170+
var err: Error? = nil
171+
172+
fields.forEach { field in
173+
let fieldASTs = field.value
174+
let fieldKey = field.key
175+
let fieldPath = path + [fieldKey] as [IndexPathElement]
176+
dispatchQueue.async(group: group) {
177+
guard err == nil else {
178+
return
179+
}
180+
do {
181+
let result = try resolveField(
182+
exeContext: exeContext,
183+
parentType: parentType,
184+
source: sourceValue,
185+
fieldASTs: fieldASTs,
186+
path: fieldPath
187+
)
188+
resultsQueue.async(group: group) {
189+
results[fieldKey] = result ?? Map.null
190+
}
191+
} catch {
192+
resultsQueue.async(group: group) {
193+
err = error
194+
}
195+
}
196+
}
197+
}
198+
group.wait()
199+
if let error = err {
200+
throw error
201+
}
202+
return results
203+
}
204+
205+
}
206+
106207
/**
107208
* Implements the "Evaluating requests" section of the GraphQL specification.
108209
*
109210
* If the arguments to this func do not result in a legal execution context,
110211
* a GraphQLError will be thrown immediately explaining the invalid input.
111212
*/
112213
func execute(
113-
queryStrategy: FieldExecutionStrategy,
114-
mutationStrategy: FieldExecutionStrategy,
115-
subscriptionStrategy: FieldExecutionStrategy,
214+
queryStrategy: QueryFieldExecutionStrategy,
215+
mutationStrategy: MutationFieldExecutionStrategy,
216+
subscriptionStrategy: SubscriptionFieldExecutionStrategy,
116217
schema: GraphQLSchema,
117218
documentAST: Document,
118219
rootValue: Any,
@@ -166,9 +267,9 @@ func execute(
166267
* Throws a GraphQLError if a valid execution context cannot be created.
167268
*/
168269
func buildExecutionContext(
169-
queryStrategy: FieldExecutionStrategy,
170-
mutationStrategy: FieldExecutionStrategy,
171-
subscriptionStrategy: FieldExecutionStrategy,
270+
queryStrategy: QueryFieldExecutionStrategy,
271+
mutationStrategy: MutationFieldExecutionStrategy,
272+
subscriptionStrategy: SubscriptionFieldExecutionStrategy,
172273
schema: GraphQLSchema,
173274
documentAST: Document,
174275
rootValue: Any,
@@ -608,7 +709,7 @@ func completeValueCatchingError(
608709
} catch let error as GraphQLError {
609710
// If `completeValueWithLocatedError` returned abruptly (threw an error),
610711
// log the error and return .null.
611-
exeContext.errors.append(error)
712+
exeContext.append(error: error)
612713
return nil
613714
} catch {
614715
fatalError()

Sources/GraphQL/GraphQL.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
///
2121
/// - returns: returns a `Map` dictionary containing the result of the query inside the key `data` and any validation or execution errors inside the key `errors`. The value of `data` might be `null` if, for example, the query is invalid. It's possible to have both `data` and `errors` if an error occurs only in a specific field. If that happens the value of that field will be `null` and there will be an error inside `errors` specifying the reason for the failure and the path of the failed field.
2222
public func graphql(
23-
queryStrategy: FieldExecutionStrategy = SerialFieldExecutionStrategy(),
24-
mutationStrategy: FieldExecutionStrategy = SerialFieldExecutionStrategy(),
25-
subscriptionStrategy: FieldExecutionStrategy = SerialFieldExecutionStrategy(),
23+
queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(),
24+
mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(),
25+
subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(),
2626
schema: GraphQLSchema,
2727
request: String,
2828
rootValue: Any = Void(),

Sources/GraphQL/Language/AST.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -289,16 +289,16 @@ func == (lhs: Definition, rhs: Definition) -> Bool {
289289
return false
290290
}
291291

292-
enum OperationType : String {
292+
public enum OperationType : String {
293293
case query = "query"
294294
case mutation = "mutation"
295295
// Note: subscription is an experimental non-spec addition.
296296
case subscription = "subscription"
297297
}
298298

299-
final class OperationDefinition {
300-
let kind: Kind = .operationDefinition
301-
let loc: Location?
299+
public final class OperationDefinition {
300+
public let kind: Kind = .operationDefinition
301+
public let loc: Location?
302302
let operation: OperationType
303303
let name: Name?
304304
let variableDefinitions: [VariableDefinition]
@@ -314,7 +314,7 @@ final class OperationDefinition {
314314
self.selectionSet = selectionSet
315315
}
316316

317-
func get(key: String) -> NodeResult? {
317+
public func get(key: String) -> NodeResult? {
318318
switch key {
319319
case "name":
320320
return name.map({ .node($0) })
@@ -337,11 +337,11 @@ final class OperationDefinition {
337337
}
338338

339339
extension OperationDefinition : Hashable {
340-
var hashValue: Int {
340+
public var hashValue: Int {
341341
return ObjectIdentifier(self).hashValue
342342
}
343343

344-
static func == (lhs: OperationDefinition, rhs: OperationDefinition) -> Bool {
344+
public static func == (lhs: OperationDefinition, rhs: OperationDefinition) -> Bool {
345345
return lhs.operation == rhs.operation &&
346346
lhs.name == rhs.name &&
347347
lhs.variableDefinitions == rhs.variableDefinitions &&
@@ -673,9 +673,9 @@ extension InlineFragment : Equatable {
673673
}
674674
}
675675

676-
final class FragmentDefinition {
677-
let kind: Kind = .fragmentDefinition
678-
let loc: Location?
676+
public final class FragmentDefinition {
677+
public let kind: Kind = .fragmentDefinition
678+
public let loc: Location?
679679
let name: Name
680680
let typeCondition: NamedType
681681
let directives: [Directive]
@@ -689,7 +689,7 @@ final class FragmentDefinition {
689689
self.selectionSet = selectionSet
690690
}
691691

692-
func get(key: String) -> NodeResult? {
692+
public func get(key: String) -> NodeResult? {
693693
switch key {
694694
case "name":
695695
return .node(name)
@@ -709,11 +709,11 @@ final class FragmentDefinition {
709709
}
710710

711711
extension FragmentDefinition : Hashable {
712-
var hashValue: Int {
712+
public var hashValue: Int {
713713
return ObjectIdentifier(self).hashValue
714714
}
715715

716-
static func == (lhs: FragmentDefinition, rhs: FragmentDefinition) -> Bool {
716+
public static func == (lhs: FragmentDefinition, rhs: FragmentDefinition) -> Bool {
717717
return lhs.name == rhs.name &&
718718
lhs.typeCondition == rhs.typeCondition &&
719719
lhs.directives == rhs.directives &&

Sources/GraphQL/Type/Definition.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,11 +452,11 @@ public typealias GraphQLFieldResolve = (
452452
) throws -> Any?
453453

454454
public struct GraphQLResolveInfo {
455-
let fieldName: String
455+
public let fieldName: String
456456
let fieldASTs: [Field]
457457
let returnType: GraphQLOutputType
458458
let parentType: GraphQLCompositeType
459-
let path: [IndexPathElement]
459+
public let path: [IndexPathElement]
460460
let schema: GraphQLSchema
461461
let fragments: [String: FragmentDefinition]
462462
let rootValue: Any

0 commit comments

Comments
 (0)