@@ -53,63 +53,65 @@ extension HTTPClient {
5353 }
5454
5555 @inlinable
56- @preconcurrency
5756 func writeChunks< Bytes: Collection > (
5857 of bytes: Bytes ,
5958 maxChunkSize: Int
60- ) -> EventLoopFuture < Void > where Bytes. Element == UInt8 , Bytes: Sendable , Bytes . SubSequence : Sendable {
59+ ) -> EventLoopFuture < Void > where Bytes. Element == UInt8 , Bytes: Sendable {
6160 // `StreamWriter` has design issues, for example
6261 // - https://github.com/swift-server/async-http-client/issues/194
6362 // - https://github.com/swift-server/async-http-client/issues/264
6463 // - We're not told the EventLoop the task runs on and the user is free to return whatever EL they
6564 // want.
6665 // One important consideration then is that we must lock around the iterator because we could be hopping
6766 // between threads.
68- typealias Iterator = BodyStreamIterator < Bytes >
67+ typealias Iterator = EnumeratedSequence < ChunksOfCountCollection < Bytes > > . Iterator
6968 typealias Chunk = ( offset: Int , element: ChunksOfCountCollection < Bytes > . Element )
7069
71- func makeIteratorAndFirstChunk(
72- bytes: Bytes
73- ) -> (
74- iterator: NIOLockedValueBox < Iterator > ,
75- chunk: Chunk
76- ) ? {
77- var iterator = bytes. chunks ( ofCount: maxChunkSize) . enumerated ( ) . makeIterator ( )
78- guard let chunk = iterator. next ( ) else {
79- return nil
70+ // HACK (again, we're not told the right EventLoop): Let's write 0 bytes to make the user tell us...
71+ return self . write ( . byteBuffer( ByteBuffer ( ) ) ) . flatMapWithEventLoop { ( _, loop) in
72+ func makeIteratorAndFirstChunk(
73+ bytes: Bytes
74+ ) -> ( iterator: Iterator , chunk: Chunk ) ? {
75+ var iterator = bytes. chunks ( ofCount: maxChunkSize) . enumerated ( ) . makeIterator ( )
76+ guard let chunk = iterator. next ( ) else {
77+ return nil
78+ }
79+
80+ return ( iterator, chunk)
8081 }
8182
82- return ( NIOLockedValueBox ( BodyStreamIterator ( iterator) ) , chunk)
83- }
84-
85- guard let ( iterator, chunk) = makeIteratorAndFirstChunk ( bytes: bytes) else {
86- return self . write ( IOData . byteBuffer ( . init( ) ) )
87- }
83+ guard let iteratorAndChunk = makeIteratorAndFirstChunk ( bytes: bytes) else {
84+ return loop. makeSucceededVoidFuture ( )
85+ }
8886
89- @Sendable // can't use closure here as we recursively call ourselves which closures can't do
90- func writeNextChunk( _ chunk: Chunk , allDone: EventLoopPromise < Void > ) {
91- if let ( index, element) = iterator. withLockedValue ( { $0. next ( ) } ) {
92- self . write ( . byteBuffer( ByteBuffer ( bytes: chunk. element) ) ) . map {
93- if ( index + 1 ) % 4 == 0 {
94- // Let's not stack-overflow if the futures insta-complete which they at least in HTTP/2
95- // mode.
96- // Also, we must frequently return to the EventLoop because we may get the pause signal
97- // from another thread. If we fail to do that promptly, we may balloon our body chunks
98- // into memory.
99- allDone. futureResult. eventLoop. execute {
87+ var iterator = iteratorAndChunk. 0
88+ let chunk = iteratorAndChunk. 1
89+
90+ // can't use closure here as we recursively call ourselves which closures can't do
91+ func writeNextChunk( _ chunk: Chunk , allDone: EventLoopPromise < Void > ) {
92+ let loop = allDone. futureResult. eventLoop
93+ loop. assertInEventLoop ( )
94+
95+ if let ( index, element) = iterator. next ( ) {
96+ self . write ( . byteBuffer( ByteBuffer ( bytes: chunk. element) ) ) . hop ( to: loop) . assumeIsolated ( ) . map {
97+ if ( index + 1 ) % 4 == 0 {
98+ // Let's not stack-overflow if the futures insta-complete which they at least in HTTP/2
99+ // mode.
100+ // Also, we must frequently return to the EventLoop because we may get the pause signal
101+ // from another thread. If we fail to do that promptly, we may balloon our body chunks
102+ // into memory.
103+ allDone. futureResult. eventLoop. assumeIsolated ( ) . execute {
104+ writeNextChunk ( ( offset: index, element: element) , allDone: allDone)
105+ }
106+ } else {
100107 writeNextChunk ( ( offset: index, element: element) , allDone: allDone)
101108 }
102- } else {
103- writeNextChunk ( ( offset: index, element: element) , allDone: allDone)
104- }
105- } . cascadeFailure ( to: allDone)
106- } else {
107- self . write ( . byteBuffer( ByteBuffer ( bytes: chunk. element) ) ) . cascade ( to: allDone)
109+ } . nonisolated ( ) . cascadeFailure ( to: allDone)
110+ } else {
111+ self . write ( . byteBuffer( ByteBuffer ( bytes: chunk. element) ) ) . cascade ( to: allDone)
112+ }
108113 }
109- }
110114
111- // HACK (again, we're not told the right EventLoop): Let's write 0 bytes to make the user tell us...
112- return self . write ( . byteBuffer( ByteBuffer ( ) ) ) . flatMapWithEventLoop { ( _, loop) in
113115 let allDone = loop. makePromise ( of: Void . self)
114116 writeNextChunk ( chunk, allDone: allDone)
115117 return allDone. futureResult
@@ -189,7 +191,7 @@ extension HTTPClient {
189191 @preconcurrency
190192 @inlinable
191193 public static func bytes< Bytes> ( _ bytes: Bytes ) -> Body
192- where Bytes: RandomAccessCollection , Bytes: Sendable , Bytes. SubSequence : Sendable , Bytes . Element == UInt8 {
194+ where Bytes: RandomAccessCollection , Bytes: Sendable , Bytes. Element == UInt8 {
193195 Body ( contentLength: Int64 ( bytes. count) ) { writer in
194196 if bytes. count <= bagOfBytesToByteBufferConversionChunkSize {
195197 return writer. write ( . byteBuffer( ByteBuffer ( bytes: bytes) ) )
@@ -1081,26 +1083,3 @@ extension RequestBodyLength {
10811083 self = . known( length)
10821084 }
10831085}
1084-
1085- @usableFromInline
1086- struct BodyStreamIterator <
1087- Bytes: Collection
1088- > : IteratorProtocol , @unchecked Sendable where Bytes. Element == UInt8 , Bytes: Sendable {
1089- // @unchecked: swift-algorithms hasn't adopted Sendable yet. By inspection, the iterator
1090- // is safe to annotate as sendable.
1091- @usableFromInline
1092- typealias Element = ( offset: Int , element: Bytes . SubSequence )
1093-
1094- @usableFromInline
1095- var _backing : EnumeratedSequence < ChunksOfCountCollection < Bytes > > . Iterator
1096-
1097- @inlinable
1098- init ( _ backing: EnumeratedSequence < ChunksOfCountCollection < Bytes > > . Iterator ) {
1099- self . _backing = backing
1100- }
1101-
1102- @inlinable
1103- mutating func next( ) -> Element ? {
1104- self . _backing. next ( )
1105- }
1106- }
0 commit comments