Skip to content

Commit 296c825

Browse files
committed
Move applicable method interceptors to client state
1 parent 5ec3bbb commit 296c825

File tree

1 file changed

+70
-26
lines changed

1 file changed

+70
-26
lines changed

Sources/GRPCCore/GRPCClient.swift

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -114,37 +114,51 @@ public final class GRPCClient: Sendable {
114114

115115
private let interceptorPipeline: [ClientInterceptorPipelineOperation]
116116

117-
/// A collection of interceptors providing cross-cutting functionality to each accepted RPC, keyed by the method to which they apply.
118-
///
119-
/// The list of interceptors for each method is computed from `interceptorsPipeline` when calling a method for the first time.
120-
/// This caching is done to avoid having to compute the applicable interceptors for each request made.
121-
///
122-
/// The order in which interceptors are added reflects the order in which they are called. The
123-
/// first interceptor added will be the first interceptor to intercept each request. The last
124-
/// interceptor added will be the final interceptor to intercept each request before calling
125-
/// the appropriate handler.
126-
private let interceptorsPerMethod: Mutex<[MethodDescriptor: [any ClientInterceptor]]>
127-
128117
/// The current state of the client.
129118
private let state: Mutex<State>
130119

131120
/// The state of the client.
132121
private enum State: Sendable {
122+
133123
/// The client hasn't been started yet. Can transition to `running` or `stopped`.
134-
case notStarted
124+
case notStarted(
125+
/// A collection of interceptors providing cross-cutting functionality to each accepted RPC, keyed by the method to which they apply.
126+
///
127+
/// The list of interceptors for each method is computed from `interceptorsPipeline` when calling a method for the first time.
128+
/// This caching is done to avoid having to compute the applicable interceptors for each request made.
129+
///
130+
/// The order in which interceptors are added reflects the order in which they are called. The
131+
/// first interceptor added will be the first interceptor to intercept each request. The last
132+
/// interceptor added will be the final interceptor to intercept each request before calling
133+
/// the appropriate handler.
134+
interceptorsPerMethod: [MethodDescriptor: [any ClientInterceptor]]
135+
)
135136
/// The client is running and can send RPCs. Can transition to `stopping`.
136-
case running
137+
case running(
138+
/// A collection of interceptors providing cross-cutting functionality to each accepted RPC, keyed by the method to which they apply.
139+
///
140+
/// The list of interceptors for each method is computed from `interceptorsPipeline` when calling a method for the first time.
141+
/// This caching is done to avoid having to compute the applicable interceptors for each request made.
142+
///
143+
/// The order in which interceptors are added reflects the order in which they are called. The
144+
/// first interceptor added will be the first interceptor to intercept each request. The last
145+
/// interceptor added will be the final interceptor to intercept each request before calling
146+
/// the appropriate handler.
147+
interceptorsPerMethod: [MethodDescriptor: [any ClientInterceptor]]
148+
)
137149
/// The client is stopping and no new RPCs will be sent. Existing RPCs may run to
138150
/// completion. May transition to `stopped`.
139151
case stopping
140152
/// The client has stopped, no RPCs are in flight and no more will be accepted. This state
141153
/// is terminal.
142154
case stopped
155+
/// Temporary state to avoid CoWs.
156+
case _modifying
143157

144158
mutating func run() throws {
145159
switch self {
146-
case .notStarted:
147-
self = .running
160+
case .notStarted(let interceptorsPerMethod):
161+
self = .running(interceptorsPerMethod: interceptorsPerMethod)
148162

149163
case .running:
150164
throw RuntimeError(
@@ -157,6 +171,9 @@ public final class GRPCClient: Sendable {
157171
code: .clientIsStopped,
158172
message: "The client has stopped and can only be started once."
159173
)
174+
175+
case ._modifying:
176+
fatalError("Internal inconsistency")
160177
}
161178
}
162179

@@ -174,6 +191,8 @@ public final class GRPCClient: Sendable {
174191
return true
175192
case .stopping, .stopped:
176193
return false
194+
case ._modifying:
195+
fatalError("Internal inconsistency")
177196
}
178197
}
179198

@@ -188,6 +207,8 @@ public final class GRPCClient: Sendable {
188207
code: .clientIsStopped,
189208
message: "Client has been stopped. Can't make any more RPCs."
190209
)
210+
case ._modifying:
211+
fatalError("Internal inconsistency")
191212
}
192213
}
193214
}
@@ -226,8 +247,7 @@ public final class GRPCClient: Sendable {
226247
) {
227248
self.transport = transport
228249
self.interceptorPipeline = interceptorPipeline
229-
self.interceptorsPerMethod = Mutex([:])
230-
self.state = Mutex(.notStarted)
250+
self.state = Mutex(.notStarted(interceptorsPerMethod: [:]))
231251
}
232252

233253
/// Start the client.
@@ -386,15 +406,39 @@ public final class GRPCClient: Sendable {
386406
var options = options
387407
options.formUnion(with: methodConfig)
388408

389-
let applicableInterceptors = self.interceptorsPerMethod.withLock {
390-
if let interceptors = $0[descriptor] {
391-
return interceptors
392-
} else {
393-
let interceptors = self.interceptorPipeline
394-
.filter { $0.applies(to: descriptor) }
395-
.map { $0.interceptor }
396-
$0[descriptor] = interceptors
397-
return interceptors
409+
let applicableInterceptors = self.state.withLock {
410+
switch $0 {
411+
case .notStarted(var interceptorsPerMethod):
412+
if let interceptors = interceptorsPerMethod[descriptor] {
413+
return interceptors
414+
} else {
415+
$0 = ._modifying
416+
let interceptors = self.interceptorPipeline
417+
.filter { $0.applies(to: descriptor) }
418+
.map { $0.interceptor }
419+
interceptorsPerMethod[descriptor] = interceptors
420+
$0 = .notStarted(interceptorsPerMethod: interceptorsPerMethod)
421+
return interceptors
422+
}
423+
424+
case .running(var interceptorsPerMethod):
425+
if let interceptors = interceptorsPerMethod[descriptor] {
426+
return interceptors
427+
} else {
428+
$0 = ._modifying
429+
let interceptors = self.interceptorPipeline
430+
.filter { $0.applies(to: descriptor) }
431+
.map { $0.interceptor }
432+
interceptorsPerMethod[descriptor] = interceptors
433+
$0 = .running(interceptorsPerMethod: interceptorsPerMethod)
434+
return interceptors
435+
}
436+
437+
case .stopping, .stopped:
438+
fatalError("The checkExecutable call should have failed.")
439+
440+
case ._modifying:
441+
fatalError("Internal inconsistency")
398442
}
399443
}
400444

0 commit comments

Comments
 (0)