Skip to content

Commit 4d09635

Browse files
authored
Merge branch 'main' into jw-http-singleton
2 parents 308946f + 2914386 commit 4d09635

22 files changed

+642
-94
lines changed

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let package = Package(
2121
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]),
2222
],
2323
dependencies: [
24-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.58.0"),
24+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"),
2525
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.22.0"),
2626
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.19.0"),
2727
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.13.0"),
@@ -38,6 +38,7 @@ let package = Package(
3838
dependencies: [
3939
.target(name: "CAsyncHTTPClient"),
4040
.product(name: "NIO", package: "swift-nio"),
41+
.product(name: "NIOTLS", package: "swift-nio"),
4142
.product(name: "NIOCore", package: "swift-nio"),
4243
.product(name: "NIOPosix", package: "swift-nio"),
4344
.product(name: "NIOHTTP1", package: "swift-nio"),
@@ -56,6 +57,7 @@ let package = Package(
5657
name: "AsyncHTTPClientTests",
5758
dependencies: [
5859
.target(name: "AsyncHTTPClient"),
60+
.product(name: "NIOTLS", package: "swift-nio"),
5961
.product(name: "NIOCore", package: "swift-nio"),
6062
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
6163
.product(name: "NIOEmbedded", package: "swift-nio"),

Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ extension Transaction {
356356
// response body stream.
357357
let body = TransactionBody.makeSequence(
358358
backPressureStrategy: .init(lowWatermark: 1, highWatermark: 1),
359+
finishOnDeinit: true,
359360
delegate: AnyAsyncSequenceProducerDelegate(delegate)
360361
)
361362

Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ClientChannelHandler.swift

Lines changed: 159 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,32 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
4242
if let idleReadTimeout = newRequest.requestOptions.idleReadTimeout {
4343
self.idleReadTimeoutStateMachine = .init(timeAmount: idleReadTimeout)
4444
}
45+
46+
if let idleWriteTimeout = newRequest.requestOptions.idleWriteTimeout {
47+
self.idleWriteTimeoutStateMachine = .init(
48+
timeAmount: idleWriteTimeout,
49+
isWritabilityEnabled: self.channelContext?.channel.isWritable ?? false
50+
)
51+
}
4552
} else {
4653
self.logger = self.backgroundLogger
4754
self.idleReadTimeoutStateMachine = nil
55+
self.idleWriteTimeoutStateMachine = nil
4856
}
4957
}
5058
}
5159

5260
private var idleReadTimeoutStateMachine: IdleReadStateMachine?
5361
private var idleReadTimeoutTimer: Scheduled<Void>?
5462

63+
private var idleWriteTimeoutStateMachine: IdleWriteStateMachine?
64+
private var idleWriteTimeoutTimer: Scheduled<Void>?
65+
5566
/// Cancelling a task in NIO does *not* guarantee that the task will not execute under certain race conditions.
5667
/// We therefore give each timer an ID and increase the ID every time we reset or cancel it.
5768
/// We check in the task if the timer ID has changed in the meantime and do not execute any action if has changed.
5869
private var currentIdleReadTimeoutTimerID: Int = 0
70+
private var currentIdleWriteTimeoutTimerID: Int = 0
5971

6072
private let backgroundLogger: Logger
6173
private var logger: Logger
@@ -106,6 +118,10 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
106118
"ahc-channel-writable": "\(context.channel.isWritable)",
107119
])
108120

121+
if let timeoutAction = self.idleWriteTimeoutStateMachine?.channelWritabilityChanged(context: context) {
122+
self.runTimeoutAction(timeoutAction, context: context)
123+
}
124+
109125
let action = self.state.writabilityChanged(writable: context.channel.isWritable)
110126
self.run(action, context: context)
111127
context.fireChannelWritabilityChanged()
@@ -150,6 +166,11 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
150166
self.request = req
151167

152168
self.logger.debug("Request was scheduled on connection")
169+
170+
if let timeoutAction = self.idleWriteTimeoutStateMachine?.write() {
171+
self.runTimeoutAction(timeoutAction, context: context)
172+
}
173+
153174
req.willExecuteRequest(self)
154175

155176
let action = self.state.runNewRequest(
@@ -196,8 +217,12 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
196217
request.resumeRequestBodyStream()
197218
}
198219
if startIdleTimer {
199-
if let timeoutAction = self.idleReadTimeoutStateMachine?.requestEndSent() {
200-
self.runTimeoutAction(timeoutAction, context: context)
220+
if let readTimeoutAction = self.idleReadTimeoutStateMachine?.requestEndSent() {
221+
self.runTimeoutAction(readTimeoutAction, context: context)
222+
}
223+
224+
if let writeTimeoutAction = self.idleWriteTimeoutStateMachine?.requestEndSent() {
225+
self.runTimeoutAction(writeTimeoutAction, context: context)
201226
}
202227
}
203228
case .sendBodyPart(let part, let writePromise):
@@ -206,8 +231,12 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
206231
case .sendRequestEnd(let writePromise):
207232
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: writePromise)
208233

209-
if let timeoutAction = self.idleReadTimeoutStateMachine?.requestEndSent() {
210-
self.runTimeoutAction(timeoutAction, context: context)
234+
if let readTimeoutAction = self.idleReadTimeoutStateMachine?.requestEndSent() {
235+
self.runTimeoutAction(readTimeoutAction, context: context)
236+
}
237+
238+
if let writeTimeoutAction = self.idleWriteTimeoutStateMachine?.requestEndSent() {
239+
self.runTimeoutAction(writeTimeoutAction, context: context)
211240
}
212241

213242
case .pauseRequestBodyStream:
@@ -380,6 +409,40 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
380409
}
381410
}
382411

412+
private func runTimeoutAction(_ action: IdleWriteStateMachine.Action, context: ChannelHandlerContext) {
413+
switch action {
414+
case .startIdleWriteTimeoutTimer(let timeAmount):
415+
assert(self.idleWriteTimeoutTimer == nil, "Expected there is no timeout timer so far.")
416+
417+
let timerID = self.currentIdleWriteTimeoutTimerID
418+
self.idleWriteTimeoutTimer = self.eventLoop.scheduleTask(in: timeAmount) {
419+
guard self.currentIdleWriteTimeoutTimerID == timerID else { return }
420+
let action = self.state.idleWriteTimeoutTriggered()
421+
self.run(action, context: context)
422+
}
423+
case .resetIdleWriteTimeoutTimer(let timeAmount):
424+
if let oldTimer = self.idleWriteTimeoutTimer {
425+
oldTimer.cancel()
426+
}
427+
428+
self.currentIdleWriteTimeoutTimerID &+= 1
429+
let timerID = self.currentIdleWriteTimeoutTimerID
430+
self.idleWriteTimeoutTimer = self.eventLoop.scheduleTask(in: timeAmount) {
431+
guard self.currentIdleWriteTimeoutTimerID == timerID else { return }
432+
let action = self.state.idleWriteTimeoutTriggered()
433+
self.run(action, context: context)
434+
}
435+
case .clearIdleWriteTimeoutTimer:
436+
if let oldTimer = self.idleWriteTimeoutTimer {
437+
self.idleWriteTimeoutTimer = nil
438+
self.currentIdleWriteTimeoutTimerID &+= 1
439+
oldTimer.cancel()
440+
}
441+
case .none:
442+
break
443+
}
444+
}
445+
383446
// MARK: Private HTTPRequestExecutor
384447

385448
private func writeRequestBodyPart0(_ data: IOData, request: HTTPExecutableRequest, promise: EventLoopPromise<Void>?) {
@@ -393,6 +456,10 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
393456
return
394457
}
395458

459+
if let timeoutAction = self.idleWriteTimeoutStateMachine?.write() {
460+
self.runTimeoutAction(timeoutAction, context: context)
461+
}
462+
396463
let action = self.state.requestStreamPartReceived(data, promise: promise)
397464
self.run(action, context: context)
398465
}
@@ -428,6 +495,10 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
428495

429496
self.logger.trace("Request was cancelled")
430497

498+
if let timeoutAction = self.idleWriteTimeoutStateMachine?.cancelRequest() {
499+
self.runTimeoutAction(timeoutAction, context: context)
500+
}
501+
431502
let action = self.state.requestCancelled(closeConnection: true)
432503
self.run(action, context: context)
433504
}
@@ -540,3 +611,87 @@ struct IdleReadStateMachine {
540611
}
541612
}
542613
}
614+
615+
struct IdleWriteStateMachine {
616+
enum Action {
617+
case startIdleWriteTimeoutTimer(TimeAmount)
618+
case resetIdleWriteTimeoutTimer(TimeAmount)
619+
case clearIdleWriteTimeoutTimer
620+
case none
621+
}
622+
623+
enum State {
624+
case waitingForRequestEnd
625+
case waitingForWritabilityEnabled
626+
case requestEndSent
627+
}
628+
629+
private var state: State
630+
private let timeAmount: TimeAmount
631+
632+
init(timeAmount: TimeAmount, isWritabilityEnabled: Bool) {
633+
self.timeAmount = timeAmount
634+
if isWritabilityEnabled {
635+
self.state = .waitingForRequestEnd
636+
} else {
637+
self.state = .waitingForWritabilityEnabled
638+
}
639+
}
640+
641+
mutating func cancelRequest() -> Action {
642+
switch self.state {
643+
case .waitingForRequestEnd, .waitingForWritabilityEnabled:
644+
self.state = .requestEndSent
645+
return .clearIdleWriteTimeoutTimer
646+
case .requestEndSent:
647+
return .none
648+
}
649+
}
650+
651+
mutating func write() -> Action {
652+
switch self.state {
653+
case .waitingForRequestEnd:
654+
return .resetIdleWriteTimeoutTimer(self.timeAmount)
655+
case .waitingForWritabilityEnabled:
656+
return .none
657+
case .requestEndSent:
658+
preconditionFailure("If the request end has been sent, we can't write more data.")
659+
}
660+
}
661+
662+
mutating func requestEndSent() -> Action {
663+
switch self.state {
664+
case .waitingForRequestEnd:
665+
self.state = .requestEndSent
666+
return .clearIdleWriteTimeoutTimer
667+
case .waitingForWritabilityEnabled:
668+
preconditionFailure("If the channel is not writable, we can't have sent the request end.")
669+
case .requestEndSent:
670+
return .none
671+
}
672+
}
673+
674+
mutating func channelWritabilityChanged(context: ChannelHandlerContext) -> Action {
675+
if context.channel.isWritable {
676+
switch self.state {
677+
case .waitingForRequestEnd:
678+
preconditionFailure("If waiting for more data, the channel was already writable.")
679+
case .waitingForWritabilityEnabled:
680+
self.state = .waitingForRequestEnd
681+
return .startIdleWriteTimeoutTimer(self.timeAmount)
682+
case .requestEndSent:
683+
return .none
684+
}
685+
} else {
686+
switch self.state {
687+
case .waitingForRequestEnd:
688+
self.state = .waitingForWritabilityEnabled
689+
return .clearIdleWriteTimeoutTimer
690+
case .waitingForWritabilityEnabled:
691+
preconditionFailure("If the channel was writable before, then we should have been waiting for more data.")
692+
case .requestEndSent:
693+
return .none
694+
}
695+
}
696+
}
697+
}

Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ConnectionStateMachine.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,18 @@ struct HTTP1ConnectionStateMachine {
355355
}
356356
}
357357

358+
mutating func idleWriteTimeoutTriggered() -> Action {
359+
guard case .inRequest(var requestStateMachine, let close) = self.state else {
360+
preconditionFailure("Invalid state: \(self.state)")
361+
}
362+
363+
return self.avoidingStateMachineCoW { state -> Action in
364+
let action = requestStateMachine.idleWriteTimeoutTriggered()
365+
state = .inRequest(requestStateMachine, close: close)
366+
return state.modify(with: action)
367+
}
368+
}
369+
358370
mutating func headSent() -> Action {
359371
guard case .inRequest(var requestStateMachine, let close) = self.state else {
360372
return .wait

0 commit comments

Comments
 (0)