Skip to content

Commit 3d6126d

Browse files
authored
Mark readers and writers as sending (#15)
1 parent 69849f7 commit 3d6126d

File tree

7 files changed

+67
-29
lines changed

7 files changed

+67
-29
lines changed

Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ where
108108
}
109109

110110
consuming func consumeAndConclude<Return>(
111-
body: (consuming Underlying) async throws -> Return
111+
body: (consuming sending Underlying) async throws -> Return
112112
) async throws -> (Return, FinalElement) {
113113
let (result, trailers) = try await self.base.consumeAndConclude { [logger] reader in
114114
let wrappedReader = RequestBodyAsyncReader(
@@ -166,7 +166,7 @@ where
166166
}
167167

168168
consuming func produceAndConclude<Return>(
169-
body: (consuming ResponseBodyAsyncWriter) async throws -> (Return, HTTPFields?)
169+
body: (consuming sending ResponseBodyAsyncWriter) async throws -> (Return, HTTPFields?)
170170
) async throws -> Return {
171171
let logger = self.logger
172172
return try await self.base.produceAndConclude { writer in

Sources/HTTPServer/AsyncPrimitives/ConcludingAsyncReader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public protocol ConcludingAsyncReader<Underlying, FinalElement>: ~Copyable {
3434
/// }
3535
/// ```
3636
consuming func consumeAndConclude<Return>(
37-
body: (consuming Underlying) async throws -> Return
37+
body: (consuming sending Underlying) async throws -> Return
3838
) async throws -> (Return, FinalElement)
3939
}
4040

@@ -62,7 +62,7 @@ extension ConcludingAsyncReader where Self: ~Copyable {
6262
/// }
6363
/// ```
6464
public consuming func consumeAndConclude(
65-
body: (consuming Underlying) async throws -> Void
65+
body: (consuming sending Underlying) async throws -> Void
6666
) async throws -> FinalElement {
6767
let (_, finalElement) = try await self.consumeAndConclude { reader in
6868
try await body(reader)

Sources/HTTPServer/AsyncPrimitives/ConcludingAsyncWriter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public protocol ConcludingAsyncWriter<Underlying, FinalElement>: ~Copyable {
2929
/// }
3030
/// ```
3131
consuming func produceAndConclude<Return>(
32-
body: (consuming Underlying) async throws -> (Return, FinalElement)
32+
body: (consuming sending Underlying) async throws -> (Return, FinalElement)
3333
) async throws -> Return
3434
}
3535

@@ -56,7 +56,7 @@ extension ConcludingAsyncWriter where Self: ~Copyable {
5656
/// }
5757
/// ```
5858
public consuming func produceAndConclude(
59-
body: (consuming Underlying) async throws -> FinalElement
59+
body: (consuming sending Underlying) async throws -> FinalElement
6060
) async throws {
6161
try await self.produceAndConclude { writer in
6262
((), try await body(writer))

Sources/HTTPServer/HTTPRequestConcludingAsyncReader.swift

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
public import HTTPTypes
22
import NIOCore
33
import NIOHTTPTypes
4+
import Synchronization
45

56
/// A specialized reader for HTTP request bodies and trailers that manages the reading process
67
/// and captures the final trailer fields.
@@ -23,7 +24,7 @@ public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable
2324
public typealias ReadFailure = any Error
2425

2526
/// The HTTP trailer fields captured at the end of the request.
26-
fileprivate var state: ReaderState?
27+
fileprivate var state: ReaderState
2728

2829
/// The iterator that provides HTTP request parts from the underlying channel.
2930
private var iterator: NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator
@@ -32,9 +33,11 @@ public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable
3233
///
3334
/// - Parameter iterator: The NIO async channel inbound stream iterator to use for reading request parts.
3435
fileprivate init(
35-
iterator: consuming sending NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator
36+
iterator: consuming sending NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator,
37+
readerState: ReaderState
3638
) {
3739
self.iterator = iterator
40+
self.state = readerState
3841
}
3942

4043
/// Reads a chunk of request body data.
@@ -53,18 +56,28 @@ public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable
5356
// TODO: Add ByteBuffer span interfaces
5457
return try await body(Array(buffer: element).span)
5558
case .end(let trailers):
56-
self.state?.trailers = trailers
57-
self.state?.finishedReading = true
59+
self.state.wrapped.withLock { state in
60+
state.trailers = trailers
61+
state.finishedReading = true
62+
}
5863
return try await body(nil)
5964
case .none:
6065
return try await body(nil)
6166
}
6267
}
6368
}
6469

65-
final class ReaderState {
66-
var trailers: HTTPFields? = nil
67-
var finishedReading: Bool = false
70+
final class ReaderState: Sendable {
71+
struct Wrapped {
72+
var trailers: HTTPFields? = nil
73+
var finishedReading: Bool = false
74+
}
75+
76+
let wrapped: Mutex<Wrapped>
77+
78+
init() {
79+
self.wrapped = .init(.init())
80+
}
6881
}
6982

7083
/// The underlying reader type for the HTTP request body.
@@ -76,10 +89,9 @@ public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable
7689
/// The type of errors that can occur during reading operations.
7790
public typealias Failure = any Error
7891

79-
/// The internal reader that provides HTTP request parts from the underlying channel.
80-
private var partsReader: RequestBodyAsyncReader
92+
private var iterator: NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator?
8193

82-
fileprivate let readerState: ReaderState
94+
internal var state: ReaderState
8395

8496
/// Initializes a new HTTP request body and trailers reader with the given NIO async channel iterator.
8597
///
@@ -88,8 +100,8 @@ public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable
88100
iterator: consuming sending NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator,
89101
readerState: ReaderState
90102
) {
91-
self.partsReader = RequestBodyAsyncReader(iterator: iterator)
92-
self.readerState = readerState
103+
self.iterator = iterator
104+
self.state = readerState
93105
}
94106

95107
/// Processes the request body reading operation and captures the final trailer fields.
@@ -118,11 +130,16 @@ public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable
118130
/// }
119131
/// ```
120132
public consuming func consumeAndConclude<Return>(
121-
body: (consuming RequestBodyAsyncReader) async throws -> Return
133+
body: (consuming sending RequestBodyAsyncReader) async throws -> Return
122134
) async throws -> (Return, HTTPFields?) {
123-
self.partsReader.state = self.readerState
124-
let result = try await body(self.partsReader)
125-
return (result, self.readerState.trailers)
135+
if let iterator = self.iterator.sendingTake() {
136+
let partsReader = RequestBodyAsyncReader(iterator: iterator, readerState: self.state)
137+
let result = try await body(partsReader)
138+
let trailers = self.state.wrapped.withLock { $0.trailers }
139+
return (result, trailers)
140+
} else {
141+
fatalError("consumeAndConclude called more than once")
142+
}
126143
}
127144
}
128145

@@ -131,3 +148,12 @@ extension HTTPRequestConcludingAsyncReader: Sendable {}
131148

132149
@available(*, unavailable)
133150
extension HTTPRequestConcludingAsyncReader.RequestBodyAsyncReader: Sendable {}
151+
152+
153+
extension Optional {
154+
mutating func sendingTake() -> sending Self {
155+
let result = consume self
156+
self = nil
157+
return result
158+
}
159+
}

Sources/HTTPServer/HTTPResponseConcludingAsyncWriter.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
public import HTTPTypes
22
import NIOCore
33
import NIOHTTPTypes
4+
import Synchronization
45

56
/// A specialized writer for HTTP response bodies and trailers that manages the writing process
67
/// and the final trailer fields.
@@ -48,8 +49,16 @@ public struct HTTPResponseConcludingAsyncWriter: ConcludingAsyncWriter, ~Copyabl
4849
}
4950
}
5051

51-
final class WriterState {
52-
var finishedWriting: Bool = false
52+
final class WriterState: Sendable {
53+
struct Wrapped {
54+
var finishedWriting: Bool = false
55+
}
56+
57+
let wrapped: Mutex<Wrapped>
58+
59+
init() {
60+
self.wrapped = .init(.init())
61+
}
5362
}
5463

5564
/// The underlying writer type for the HTTP response body.
@@ -102,12 +111,14 @@ public struct HTTPResponseConcludingAsyncWriter: ConcludingAsyncWriter, ~Copyabl
102111
/// }
103112
/// ```
104113
public consuming func produceAndConclude<Return>(
105-
body: (consuming ResponseBodyAsyncWriter) async throws -> (Return, FinalElement)
114+
body: (consuming sending ResponseBodyAsyncWriter) async throws -> (Return, FinalElement)
106115
) async throws -> Return {
107116
let responseBodyAsyncWriter = ResponseBodyAsyncWriter(writer: self.writer)
108117
let (result, finalElement) = try await body(responseBodyAsyncWriter)
109118
try await self.writer.write(.end(finalElement))
110-
self.writerState.finishedWriting = true
119+
self.writerState.wrapped.withLock { state in
120+
state.finishedWriting = true
121+
}
111122
return result
112123
}
113124
}

Sources/HTTPServer/HTTPResponseSender.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public import HTTPTypes
99
public struct HTTPResponseSender<ResponseWriter: ConcludingAsyncWriter & ~Copyable>: ~Copyable {
1010
private let _sendResponse: (HTTPResponse) async throws -> ResponseWriter
1111

12-
package init(
12+
public init(
1313
_ sendResponse: @escaping (HTTPResponse) async throws -> ResponseWriter
1414
) {
1515
self._sendResponse = sendResponse

Sources/HTTPServer/HTTPServer.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import NIOPosix
1010
import NIOSSL
1111
import X509
1212
import SwiftASN1
13+
import Synchronization
1314

1415
/// A generic HTTP server that can handle incoming HTTP requests.
1516
///
@@ -363,12 +364,12 @@ public final class Server<RequestHandler: HTTPServerRequestHandler> {
363364
}
364365
)
365366
} catch {
366-
if !readerState.finishedReading {
367+
if !readerState.wrapped.withLock({ $0.finishedReading }) {
367368
// TODO: do something - we didn't finish reading but we threw
368369
// if h2 reset stream; if h1 try draining request?
369370
fatalError("Didn't finish reading but threw.")
370371
}
371-
if !writerState.finishedWriting {
372+
if !writerState.wrapped.withLock({ $0.finishedWriting }) {
372373
// TODO: this means we didn't write a response end and we threw
373374
// we need to do something, possibly just close the connection or
374375
// reset the stream with the appropriate error.

0 commit comments

Comments
 (0)