Skip to content

Commit 5c1e0be

Browse files
feat!: Improves thread safety of unchecked Sendables
1 parent bc807e3 commit 5c1e0be

File tree

2 files changed

+101
-57
lines changed

2 files changed

+101
-57
lines changed

Sources/GraphQL/Execution/Execute.swift

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,24 @@ public final class ExecutionContext: @unchecked Sendable {
3838
public let operation: OperationDefinition
3939
public let variableValues: [String: Map]
4040

41-
private var errorsSemaphore = DispatchSemaphore(value: 1)
4241
private var _errors: [GraphQLError]
43-
42+
private let errorsQueue = DispatchQueue(
43+
label: "graphql.schema.validationerrors",
44+
attributes: .concurrent
45+
)
4446
public var errors: [GraphQLError] {
45-
errorsSemaphore.wait()
46-
defer {
47-
errorsSemaphore.signal()
47+
get {
48+
// Reads can occur concurrently.
49+
return errorsQueue.sync {
50+
_errors
51+
}
52+
}
53+
set {
54+
// Writes occur sequentially.
55+
return errorsQueue.async(flags: .barrier) {
56+
self._errors = newValue
57+
}
4858
}
49-
return _errors
5059
}
5160

5261
init(
@@ -74,11 +83,7 @@ public final class ExecutionContext: @unchecked Sendable {
7483
}
7584

7685
public func append(error: GraphQLError) {
77-
errorsSemaphore.wait()
78-
defer {
79-
errorsSemaphore.signal()
80-
}
81-
_errors.append(error)
86+
errors.append(error)
8287
}
8388
}
8489

Sources/GraphQL/Type/Schema.swift

Lines changed: 85 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Dispatch
12
import OrderedCollections
23

34
/**
@@ -34,15 +35,53 @@ public final class GraphQLSchema: @unchecked Sendable {
3435
let extensionASTNodes: [SchemaExtensionDefinition]
3536

3637
// Used as a cache for validateSchema().
37-
var validationErrors: [GraphQLError]?
38+
private var _validationErrors: [GraphQLError]?
39+
private let validationErrorQueue = DispatchQueue(
40+
label: "graphql.schema.validationerrors",
41+
attributes: .concurrent
42+
)
43+
var validationErrors: [GraphQLError]? {
44+
get {
45+
// Reads can occur concurrently.
46+
return validationErrorQueue.sync {
47+
_validationErrors
48+
}
49+
}
50+
set {
51+
// Writes occur sequentially.
52+
return validationErrorQueue.async(flags: .barrier) {
53+
self._validationErrors = newValue
54+
}
55+
}
56+
}
3857

3958
public let queryType: GraphQLObjectType?
4059
public let mutationType: GraphQLObjectType?
4160
public let subscriptionType: GraphQLObjectType?
4261
public let directives: [GraphQLDirective]
4362
public let typeMap: TypeMap
44-
public internal(set) var implementations: [String: InterfaceImplementations]
45-
private var subTypeMap: [String: [String: Bool]] = [:]
63+
public let implementations: [String: InterfaceImplementations]
64+
65+
// Used as a cache for validateSchema().
66+
private var _subTypeMap: [String: [String: Bool]] = [:]
67+
private let subTypeMapQueue = DispatchQueue(
68+
label: "graphql.schema.subtypeMap",
69+
attributes: .concurrent
70+
)
71+
var subTypeMap: [String: [String: Bool]] {
72+
get {
73+
// Reads can occur concurrently.
74+
return subTypeMapQueue.sync {
75+
_subTypeMap
76+
}
77+
}
78+
set {
79+
// Writes occur sequentially.
80+
return subTypeMapQueue.async(flags: .barrier) {
81+
self._subTypeMap = newValue
82+
}
83+
}
84+
}
4685

4786
public init(
4887
description: String? = nil,
@@ -56,7 +95,7 @@ public final class GraphQLSchema: @unchecked Sendable {
5695
extensionASTNodes: [SchemaExtensionDefinition] = [],
5796
assumeValid: Bool = false
5897
) throws {
59-
validationErrors = assumeValid ? [] : nil
98+
_validationErrors = assumeValid ? [] : nil
6099

61100
self.description = description
62101
self.extensions = extensions
@@ -109,7 +148,38 @@ public final class GraphQLSchema: @unchecked Sendable {
109148
var typeMap = TypeMap()
110149

111150
// Keep track of all implementations by interface name.
112-
implementations = try collectImplementations(types: Array(typeMap.values))
151+
func collectImplementations(
152+
types: [GraphQLNamedType]
153+
) throws -> [String: InterfaceImplementations] {
154+
var implementations: [String: InterfaceImplementations] = [:]
155+
156+
for type in types {
157+
if let type = type as? GraphQLInterfaceType {
158+
if implementations[type.name] == nil {
159+
implementations[type.name] = InterfaceImplementations()
160+
}
161+
162+
// Store implementations by interface.
163+
for iface in try type.getInterfaces() {
164+
implementations[iface.name] = InterfaceImplementations(
165+
interfaces: (implementations[iface.name]?.interfaces ?? []) + [type]
166+
)
167+
}
168+
}
169+
170+
if let type = type as? GraphQLObjectType {
171+
// Store implementations by objects.
172+
for iface in try type.getInterfaces() {
173+
implementations[iface.name] = InterfaceImplementations(
174+
objects: (implementations[iface.name]?.objects ?? []) + [type]
175+
)
176+
}
177+
}
178+
}
179+
180+
return implementations
181+
}
182+
var implementations = try collectImplementations(types: Array(typeMap.values))
113183

114184
for namedType in allReferencedTypes.values {
115185
let typeName = namedType.name
@@ -124,37 +194,38 @@ public final class GraphQLSchema: @unchecked Sendable {
124194
if let namedType = namedType as? GraphQLInterfaceType {
125195
// Store implementations by interface.
126196
for iface in try namedType.getInterfaces() {
127-
let implementations = self.implementations[iface.name] ?? .init(
197+
let implementation = implementations[iface.name] ?? .init(
128198
objects: [],
129199
interfaces: []
130200
)
131201

132-
var interfaces = implementations.interfaces
202+
var interfaces = implementation.interfaces
133203
interfaces.append(namedType)
134-
self.implementations[iface.name] = .init(
135-
objects: implementations.objects,
204+
implementations[iface.name] = .init(
205+
objects: implementation.objects,
136206
interfaces: interfaces
137207
)
138208
}
139209
} else if let namedType = namedType as? GraphQLObjectType {
140210
// Store implementations by objects.
141211
for iface in try namedType.getInterfaces() {
142-
let implementations = self.implementations[iface.name] ?? .init(
212+
let implementation = implementations[iface.name] ?? .init(
143213
objects: [],
144214
interfaces: []
145215
)
146216

147-
var objects = implementations.objects
217+
var objects = implementation.objects
148218
objects.append(namedType)
149-
self.implementations[iface.name] = .init(
219+
implementations[iface.name] = .init(
150220
objects: objects,
151-
interfaces: implementations.interfaces
221+
interfaces: implementation.interfaces
152222
)
153223
}
154224
}
155225
}
156226

157227
self.typeMap = typeMap
228+
self.implementations = implementations
158229
}
159230

160231
convenience init(config: GraphQLSchemaNormalizedConfig) throws {
@@ -268,7 +339,7 @@ public final class GraphQLSchema: @unchecked Sendable {
268339

269340
public typealias TypeMap = OrderedDictionary<String, GraphQLNamedType>
270341

271-
public struct InterfaceImplementations {
342+
public struct InterfaceImplementations: Sendable {
272343
public let objects: [GraphQLObjectType]
273344
public let interfaces: [GraphQLInterfaceType]
274345

@@ -281,38 +352,6 @@ public struct InterfaceImplementations {
281352
}
282353
}
283354

284-
func collectImplementations(
285-
types: [GraphQLNamedType]
286-
) throws -> [String: InterfaceImplementations] {
287-
var implementations: [String: InterfaceImplementations] = [:]
288-
289-
for type in types {
290-
if let type = type as? GraphQLInterfaceType {
291-
if implementations[type.name] == nil {
292-
implementations[type.name] = InterfaceImplementations()
293-
}
294-
295-
// Store implementations by interface.
296-
for iface in try type.getInterfaces() {
297-
implementations[iface.name] = InterfaceImplementations(
298-
interfaces: (implementations[iface.name]?.interfaces ?? []) + [type]
299-
)
300-
}
301-
}
302-
303-
if let type = type as? GraphQLObjectType {
304-
// Store implementations by objects.
305-
for iface in try type.getInterfaces() {
306-
implementations[iface.name] = InterfaceImplementations(
307-
objects: (implementations[iface.name]?.objects ?? []) + [type]
308-
)
309-
}
310-
}
311-
}
312-
313-
return implementations
314-
}
315-
316355
func typeMapReducer(typeMap: TypeMap, type: GraphQLType) throws -> TypeMap {
317356
var typeMap = typeMap
318357

0 commit comments

Comments
 (0)