Skip to content

Commit 4312579

Browse files
authored
Delay client async writer starting (#1531)
Motivation: It's possible for async streaming clients to fail and drop messages in some situations. The situation leading to this happens because streaming calls set up state and then write out headers. While setting up state the HTTP/2 stream channel is configured, when it becomes active gRPC calls outs to enable the async writer to start emitting writes. This can happen before the headers are written so if a write is already pending then it can race the headers being written. If the message is written first then the write promise is failed and the message is dropped. Modifications: Delay letting the async writer emit writes until the headers have been written. Result: Correct ordering is enforced.
1 parent 524f06b commit 4312579

File tree

2 files changed

+43
-1
lines changed

2 files changed

+43
-1
lines changed

Sources/GRPC/Interceptor/ClientTransport.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,6 @@ extension ClientTransport {
337337
self._pipeline?.logger = self.logger
338338
self.logger.debug("activated stream channel")
339339
self.channel = channel
340-
self.onStart()
341340
self.unbuffer()
342341

343342
case .close:
@@ -943,6 +942,10 @@ extension ClientTransport {
943942
case let .metadata(headers):
944943
let head = self.makeRequestHead(with: headers)
945944
channel.write(self.wrapOutboundOut(.head(head)), promise: promise)
945+
// Messages are buffered by this class and in the async writer for async calls. Initially the
946+
// async writer is not allowed to emit messages; the call to 'onStart()' signals that messages
947+
// may be emitted. We call it here to avoid races between writing headers and messages.
948+
self.onStart()
946949

947950
case let .message(request, metadata):
948951
do {

Tests/GRPCTests/ClientCallTests.swift

+39
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,43 @@ class ClientCallTests: GRPCTestCase {
206206
// Cancellation should now fail, we've already cancelled.
207207
assertThat(try get.cancel().wait(), .throws(.instanceOf(GRPCError.AlreadyComplete.self)))
208208
}
209+
210+
func testWriteMessageOnStart() throws {
211+
// This test isn't deterministic so run a bunch of iterations.
212+
for _ in 0 ..< 100 {
213+
let call = self.update()
214+
let promise = call.eventLoop.makePromise(of: Void.self)
215+
let finished = call.eventLoop.makePromise(of: Void.self)
216+
217+
call.invokeStreamingRequests {
218+
// Send in onStart.
219+
call.send(
220+
.message(.with { $0.text = "foo" }, .init(compress: false, flush: false)),
221+
promise: promise
222+
)
223+
} onError: { _ in // ignore errors
224+
} onResponsePart: {
225+
switch $0 {
226+
case .metadata, .message:
227+
()
228+
case .end:
229+
finished.succeed(())
230+
}
231+
}
232+
233+
// End the stream.
234+
promise.futureResult.whenComplete { _ in
235+
call.send(.end, promise: nil)
236+
}
237+
238+
do {
239+
try promise.futureResult.wait()
240+
try finished.futureResult.wait()
241+
} catch {
242+
// Stop on the first error.
243+
XCTFail("Unexpected error: \(error)")
244+
return
245+
}
246+
}
247+
}
209248
}

0 commit comments

Comments
 (0)