@@ -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