Skip to content

Commit 6c5c289

Browse files
[async-await] Support for sending response headers via context (#1262)
The adopter may wish to set the response headers (aka "initial metadata") from their user handler. Until now this was not possible, even in the existing non–async-await API, and it was only possible for an adopter to set the trailers. This introduces a new mutable property to the context passed to the user handler that allows them to set the headers that should be sent back to the client before the first response message. * `let GRPCAsyncServerCallContext.headers` has been renamed to `requestHeaders` to disambiguate from newly introduced property. * `var GRPCAsyncServerCallContext.trailers` has been renamed to `responseTrailers` to better align with newly introduced property. * `var GRPCAsyncServerCallContext.responseHeaders` has been introduced.
1 parent fc2d640 commit 6c5c289

7 files changed

+163
-95
lines changed

Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public struct GRPCAsyncBidirectionalStreamingCall<Request, Response> {
4040
// MARK: - Response Parts
4141

4242
/// The initial metadata returned from the server.
43+
///
44+
/// - Important: The initial metadata will only be available when the first response has been
45+
/// received. However, it is not necessary for the response to have been consumed before reading
46+
/// this property.
4347
public var initialMetadata: HPACKHeaders {
4448
// swiftformat:disable:next redundantGet
4549
get async throws {

Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public struct GRPCAsyncClientStreamingCall<Request, Response> {
3636
// MARK: - Response Parts
3737

3838
/// The initial metadata returned from the server.
39+
///
40+
/// - Important: The initial metadata will only be available when the response has been received.
3941
public var initialMetadata: HPACKHeaders {
4042
// swiftformat:disable:next redundantGet
4143
get async throws {

Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ import NIOHPACK
3434
public final class GRPCAsyncServerCallContext {
3535
private let lock = Lock()
3636

37-
/// Request headers for this request.
38-
public let headers: HPACKHeaders
37+
/// Metadata for this request.
38+
public let requestMetadata: HPACKHeaders
3939

4040
/// The logger used for this call.
4141
public var logger: Logger {
@@ -83,26 +83,42 @@ public final class GRPCAsyncServerCallContext {
8383
@usableFromInline
8484
internal let userInfoRef: Ref<UserInfo>
8585

86-
/// Metadata to return at the end of the RPC. If this is required it should be updated before
87-
/// the `responsePromise` or `statusPromise` is fulfilled.
88-
public var trailers: HPACKHeaders {
86+
/// Metadata to return at the start of the RPC.
87+
///
88+
/// - Important: If this is required it should be updated _before_ the first response is sent via
89+
/// the response stream writer. Any updates made after the first response will be ignored.
90+
public var initialResponseMetadata: HPACKHeaders {
91+
get { self.lock.withLock {
92+
return self._initialResponseMetadata
93+
} }
94+
set { self.lock.withLock {
95+
self._initialResponseMetadata = newValue
96+
} }
97+
}
98+
99+
private var _initialResponseMetadata: HPACKHeaders = [:]
100+
101+
/// Metadata to return at the end of the RPC.
102+
///
103+
/// If this is required it should be updated before returning from the handler.
104+
public var trailingResponseMetadata: HPACKHeaders {
89105
get { self.lock.withLock {
90-
return self._trailers
106+
return self._trailingResponseMetadata
91107
} }
92108
set { self.lock.withLock {
93-
self._trailers = newValue
109+
self._trailingResponseMetadata = newValue
94110
} }
95111
}
96112

97-
private var _trailers: HPACKHeaders = [:]
113+
private var _trailingResponseMetadata: HPACKHeaders = [:]
98114

99115
@inlinable
100116
internal init(
101117
headers: HPACKHeaders,
102118
logger: Logger,
103119
userInfoRef: Ref<UserInfo>
104120
) {
105-
self.headers = headers
121+
self.requestMetadata = headers
106122
self.userInfoRef = userInfoRef
107123
self._logger = logger
108124
}

Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift

Lines changed: 76 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -204,38 +204,56 @@ internal final class AsyncServerHandler<
204204
/// No headers have been received.
205205
case idle
206206

207-
/// Headers have been received, and an async `Task` has been created to execute the user
208-
/// handler.
209-
///
210-
/// The inputs to the user handler are held in the associated data of this enum value:
211-
///
212-
/// - The `PassthroughMessageSource` is the source backing the request stream that is being
213-
/// consumed by the user handler.
214-
///
215-
/// - The `GRPCAsyncServerContext` is a reference to the context that was passed to the user
216-
/// handler.
217-
///
218-
/// - The `GRPCAsyncResponseStreamWriter` is the response stream writer that is being written to
219-
/// by the user handler. Because this is pausable, it may contain responses after the user
220-
/// handler has completed that have yet to be written. However we will remain in the `.active`
221-
/// state until the response stream writer has completed.
222-
///
223-
/// - The `EventLoopPromise` bridges the NIO and async-await worlds. It is the mechanism that we
224-
/// use to run a callback when the user handler has completed. The promise is not passed to the
225-
/// user handler directly. Instead it is fulfilled with the result of the async `Task` executing
226-
/// the user handler using `completeWithTask(_:)`.
227-
///
228-
/// - TODO: It shouldn't really be necessary to stash the `GRPCAsyncResponseStreamWriter` or the
229-
/// `EventLoopPromise` in this enum value. Specifically they are never used anywhere when this
230-
/// enum value is accessed. However, if we do not store them here then the tests periodically
231-
/// segfault. This appears to be an bug in Swift and/or NIO since these should both have been
232-
/// captured by `completeWithTask(_:)`.
233-
case active(
234-
PassthroughMessageSource<Request, Error>,
235-
GRPCAsyncServerCallContext,
236-
GRPCAsyncResponseStreamWriter<Response>,
237-
EventLoopPromise<Void>
238-
)
207+
@usableFromInline
208+
internal struct ActiveState {
209+
/// The source backing the request stream that is being consumed by the user handler.
210+
@usableFromInline
211+
let requestStreamSource: PassthroughMessageSource<Request, Error>
212+
213+
/// The call context that was passed to the user handler.
214+
@usableFromInline
215+
let context: GRPCAsyncServerCallContext
216+
217+
/// The response stream writer that is being used by the user handler.
218+
///
219+
/// Because this is pausable, it may contain responses after the user handler has completed
220+
/// that have yet to be written. However we will remain in the `.active` state until the
221+
/// response stream writer has completed.
222+
@usableFromInline
223+
let responseStreamWriter: GRPCAsyncResponseStreamWriter<Response>
224+
225+
/// The response headers have been sent back to the client via the interceptors.
226+
@usableFromInline
227+
var haveSentResponseHeaders: Bool = false
228+
229+
/// The promise we are using to bridge the NIO and async-await worlds.
230+
///
231+
/// It is the mechanism that we use to run a callback when the user handler has completed.
232+
/// The promise is not passed to the user handler directly. Instead it is fulfilled with the
233+
/// result of the async `Task` executing the user handler using `completeWithTask(_:)`.
234+
///
235+
/// - TODO: It shouldn't really be necessary to stash this promise here. Specifically it is
236+
/// never used anywhere when the `.active` enum value is accessed. However, if we do not store
237+
/// it here then the tests periodically segfault. This appears to be a reference counting bug
238+
/// in Swift and/or NIO since it should have been captured by `completeWithTask(_:)`.
239+
let _userHandlerPromise: EventLoopPromise<Void>
240+
241+
@usableFromInline
242+
internal init(
243+
requestStreamSource: PassthroughMessageSource<Request, Error>,
244+
context: GRPCAsyncServerCallContext,
245+
responseStreamWriter: GRPCAsyncResponseStreamWriter<Response>,
246+
userHandlerPromise: EventLoopPromise<Void>
247+
) {
248+
self.requestStreamSource = requestStreamSource
249+
self.context = context
250+
self.responseStreamWriter = responseStreamWriter
251+
self._userHandlerPromise = userHandlerPromise
252+
}
253+
}
254+
255+
/// Headers have been received and an async `Task` has been created to execute the user handler.
256+
case active(ActiveState)
239257

240258
/// The handler has completed.
241259
case completed
@@ -363,15 +381,16 @@ internal final class AsyncServerHandler<
363381
)
364382

365383
// Set the state to active and bundle in all the associated data.
366-
self.state = .active(requestStreamSource, context, responseStreamWriter, userHandlerPromise)
384+
self.state = .active(.init(
385+
requestStreamSource: requestStreamSource,
386+
context: context,
387+
responseStreamWriter: responseStreamWriter,
388+
userHandlerPromise: userHandlerPromise
389+
))
367390

368391
// Register callback for the completion of the user handler.
369392
userHandlerPromise.futureResult.whenComplete(self.userHandlerCompleted(_:))
370393

371-
// Send response headers back via the interceptors.
372-
// TODO: In future we may want to defer this until the first response is available from the user handler which will allow the user to set the response headers via the context.
373-
self.interceptors.send(.metadata([:]), promise: nil)
374-
375394
// Spin up a task to call the async user handler.
376395
self.userHandlerTask = userHandlerPromise.completeWithTask {
377396
return try await withTaskCancellationHandler {
@@ -443,8 +462,8 @@ internal final class AsyncServerHandler<
443462
switch self.state {
444463
case .idle:
445464
self.handleError(GRPCError.ProtocolViolation("Message received before headers"))
446-
case let .active(requestStreamSource, _, _, _):
447-
switch requestStreamSource.yield(request) {
465+
case let .active(activeState):
466+
switch activeState.requestStreamSource.yield(request) {
448467
case .accepted(queueDepth: _):
449468
// TODO: In future we will potentially issue a read request to the channel based on the value of `queueDepth`.
450469
break
@@ -467,8 +486,8 @@ internal final class AsyncServerHandler<
467486
switch self.state {
468487
case .idle:
469488
self.handleError(GRPCError.ProtocolViolation("End of stream received before headers"))
470-
case let .active(requestStreamSource, _, _, _):
471-
switch requestStreamSource.finish() {
489+
case let .active(activeState):
490+
switch activeState.requestStreamSource.finish() {
472491
case .accepted(queueDepth: _):
473492
break
474493
case .dropped:
@@ -495,7 +514,14 @@ internal final class AsyncServerHandler<
495514
// The user handler cannot send responses before it has been invoked.
496515
preconditionFailure()
497516

498-
case .active:
517+
case var .active(activeState):
518+
if !activeState.haveSentResponseHeaders {
519+
activeState.haveSentResponseHeaders = true
520+
self.state = .active(activeState)
521+
// Send response headers back via the interceptors.
522+
self.interceptors.send(.metadata(activeState.context.initialResponseMetadata), promise: nil)
523+
}
524+
// Send the response back via the interceptors.
499525
self.interceptors.send(.message(response, metadata), promise: nil)
500526

501527
case .completed:
@@ -547,10 +573,13 @@ internal final class AsyncServerHandler<
547573
case .idle:
548574
preconditionFailure()
549575

550-
case let .active(_, context, _, _):
576+
case let .active(activeState):
551577
// Now we have drained the response stream writer from the user handler we can send end.
552578
self.state = .completed
553-
self.interceptors.send(.end(status, context.trailers), promise: nil)
579+
self.interceptors.send(
580+
.end(status, activeState.context.trailingResponseMetadata),
581+
promise: nil
582+
)
554583

555584
case .completed:
556585
()
@@ -580,7 +609,7 @@ internal final class AsyncServerHandler<
580609
)
581610
self.interceptors.send(.end(status, trailers), promise: nil)
582611

583-
case let .active(_, context, _, _):
612+
case let .active(activeState):
584613
self.state = .completed
585614

586615
// If we have an async task, then cancel it, which will terminate the request stream from
@@ -593,8 +622,8 @@ internal final class AsyncServerHandler<
593622
if isHandlerError {
594623
(status, trailers) = ServerErrorProcessor.processObserverError(
595624
error,
596-
headers: context.headers,
597-
trailers: context.trailers,
625+
headers: activeState.context.requestMetadata,
626+
trailers: activeState.context.trailingResponseMetadata,
598627
delegate: self.context.errorDelegate
599628
)
600629
} else {

Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ public struct GRPCAsyncServerStreamingCall<Request, Response> {
3939
// MARK: - Response Parts
4040

4141
/// The initial metadata returned from the server.
42+
///
43+
/// - Important: The initial metadata will only be available when the first response has been
44+
/// received. However, it is not necessary for the response to have been consumed before reading
45+
/// this property.
4246
public var initialMetadata: HPACKHeaders {
4347
// swiftformat:disable:next redundantGet
4448
get async throws {

Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public struct GRPCAsyncUnaryCall<Request, Response> {
3939
// MARK: - Response Parts
4040

4141
/// The initial metadata returned from the server.
42+
///
43+
/// - Important: The initial metadata will only be available when the response has been received.
4244
public var initialMetadata: HPACKHeaders {
4345
// swiftformat:disable:next redundantGet
4446
get async throws {

0 commit comments

Comments
 (0)