apple / swift-openapi-urlsession

URLSession transport for Swift OpenAPI Generator.
https://swiftpackageindex.com/apple/swift-openapi-urlsession/documentation
Apache License 2.0
160 stars 29 forks source link

Add missing transition AsyncBackpressuredStream state machine #27

Closed simonjbeaumont closed 10 months ago

simonjbeaumont commented 10 months ago

Motivation

Some of our bidirectional streaming tests were failing intermittently. When they failed, the symptom was that more bytes were received by the end user (in the test) than the server sent.

For example, in the test testStreamingDownload_1kChunk_10kChunks_100BDownloadWatermark, we expect:

So we see that there was one more element emitted from the AsyncBackpressuredStream than the delegate callback wrote, which meant the test saw an additional 40960 bytes than it expected to and consequently reconstructed an additional 40 chunks of size 1024 over what the server sent.

We can see that the AsyncBackpressuredStream duplicates an element along the way, of 40960 bytes,

❯ diff -u --label=received-in-delegate-callback <(cat repro.txt | grep "didReceive data" | awk '{ print $6 }' | tr -d \)) --label=received-through-async-sequence <(cat repro.txt | grep "Client received some response body bytes" | awk '{ print $8 }' | tr -d \))
--- received-in-delegate-callback
+++ received-through-async-sequence
@@ -305,6 +305,7 @@
 2048
 1024
 40960
+40960
 24576
 2048
 34841

After some investigation, it turned out there was a missing transition in the state machine that underlies the AsyncBackpressuredStream. When calling suspendNext when there are buffered elements, but we are above the watermark, we popped the first item from the buffer and returned it without updating the state (with the new buffer, without the popped element).

Consequently, this meant that the next call to next() would return the same element again.

Modifications

The following modifications have been made in separate commits to aid review:

Result

Duplicate elements are no longer emitted from the response body.

Test Plan

Additional notes

The implementation we are using for AsyncBackpressuredStream was taken from an early draft of SE-0406. We should probably move to using something closer matching that of the current PR to the Swift tree, or that used by swift-grpc, which has also adopted this code and cleaned it up to remove the dependencies on the standard library internals. Additionally, that implementation does not have this missing state transition and also adds an intermediate state to the state machine to avoid unintended copy-on-write.