@@ -42,9 +42,17 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
42
42
if let idleReadTimeout = newRequest. requestOptions. idleReadTimeout {
43
43
self . idleReadTimeoutStateMachine = . init( timeAmount: idleReadTimeout)
44
44
}
45
+
46
+ if let idleWriteTimeout = newRequest. requestOptions. idleWriteTimeout {
47
+ self . idleWriteTimeoutStateMachine = . init(
48
+ timeAmount: idleWriteTimeout,
49
+ isWritabilityEnabled: self . channelContext? . channel. isWritable ?? false
50
+ )
51
+ }
45
52
} else {
46
53
self . logger = self . backgroundLogger
47
54
self . idleReadTimeoutStateMachine = nil
55
+ self . idleWriteTimeoutStateMachine = nil
48
56
}
49
57
}
50
58
}
@@ -57,6 +65,14 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
57
65
/// We check in the task if the timer ID has changed in the meantime and do not execute any action if has changed.
58
66
private var currentIdleReadTimeoutTimerID : Int = 0
59
67
68
+ private var idleWriteTimeoutStateMachine : IdleWriteStateMachine ?
69
+ private var idleWriteTimeoutTimer : Scheduled < Void > ?
70
+
71
+ /// Cancelling a task in NIO does *not* guarantee that the task will not execute under certain race conditions.
72
+ /// We therefore give each timer an ID and increase the ID every time we reset or cancel it.
73
+ /// We check in the task if the timer ID has changed in the meantime and do not execute any action if has changed.
74
+ private var currentIdleWriteTimeoutTimerID : Int = 0
75
+
60
76
private let backgroundLogger : Logger
61
77
private var logger : Logger
62
78
private let eventLoop : EventLoop
@@ -106,6 +122,10 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
106
122
" ahc-channel-writable " : " \( context. channel. isWritable) " ,
107
123
] )
108
124
125
+ if let timeoutAction = self . idleWriteTimeoutStateMachine? . channelWritabilityChanged ( context: context) {
126
+ self . runTimeoutAction ( timeoutAction, context: context)
127
+ }
128
+
109
129
let action = self . state. writabilityChanged ( writable: context. channel. isWritable)
110
130
self . run ( action, context: context)
111
131
context. fireChannelWritabilityChanged ( )
@@ -150,6 +170,11 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
150
170
self . request = req
151
171
152
172
self . logger. debug ( " Request was scheduled on connection " )
173
+
174
+ if let timeoutAction = self . idleWriteTimeoutStateMachine? . write ( ) {
175
+ self . runTimeoutAction ( timeoutAction, context: context)
176
+ }
177
+
153
178
req. willExecuteRequest ( self )
154
179
155
180
let action = self . state. runNewRequest (
@@ -196,8 +221,12 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
196
221
request. resumeRequestBodyStream ( )
197
222
}
198
223
if startIdleTimer {
199
- if let timeoutAction = self . idleReadTimeoutStateMachine? . requestEndSent ( ) {
200
- self . runTimeoutAction ( timeoutAction, context: context)
224
+ if let readTimeoutAction = self . idleReadTimeoutStateMachine? . requestEndSent ( ) {
225
+ self . runTimeoutAction ( readTimeoutAction, context: context)
226
+ }
227
+
228
+ if let writeTimeoutAction = self . idleWriteTimeoutStateMachine? . requestEndSent ( ) {
229
+ self . runTimeoutAction ( writeTimeoutAction, context: context)
201
230
}
202
231
}
203
232
case . sendBodyPart( let part, let writePromise) :
@@ -206,8 +235,12 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
206
235
case . sendRequestEnd( let writePromise) :
207
236
context. writeAndFlush ( self . wrapOutboundOut ( . end( nil ) ) , promise: writePromise)
208
237
209
- if let timeoutAction = self . idleReadTimeoutStateMachine? . requestEndSent ( ) {
210
- self . runTimeoutAction ( timeoutAction, context: context)
238
+ if let readTimeoutAction = self . idleReadTimeoutStateMachine? . requestEndSent ( ) {
239
+ self . runTimeoutAction ( readTimeoutAction, context: context)
240
+ }
241
+
242
+ if let writeTimeoutAction = self . idleWriteTimeoutStateMachine? . requestEndSent ( ) {
243
+ self . runTimeoutAction ( writeTimeoutAction, context: context)
211
244
}
212
245
213
246
case . pauseRequestBodyStream:
@@ -380,6 +413,40 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
380
413
}
381
414
}
382
415
416
+ private func runTimeoutAction( _ action: IdleWriteStateMachine . Action , context: ChannelHandlerContext ) {
417
+ switch action {
418
+ case . startIdleWriteTimeoutTimer( let timeAmount) :
419
+ assert ( self . idleWriteTimeoutTimer == nil , " Expected there is no timeout timer so far. " )
420
+
421
+ let timerID = self . currentIdleWriteTimeoutTimerID
422
+ self . idleWriteTimeoutTimer = self . eventLoop. scheduleTask ( in: timeAmount) {
423
+ guard self . currentIdleWriteTimeoutTimerID == timerID else { return }
424
+ let action = self . state. idleWriteTimeoutTriggered ( )
425
+ self . run ( action, context: context)
426
+ }
427
+ case . resetIdleWriteTimeoutTimer( let timeAmount) :
428
+ if let oldTimer = self . idleWriteTimeoutTimer {
429
+ oldTimer. cancel ( )
430
+ }
431
+
432
+ self . currentIdleWriteTimeoutTimerID &+= 1
433
+ let timerID = self . currentIdleWriteTimeoutTimerID
434
+ self . idleWriteTimeoutTimer = self . eventLoop. scheduleTask ( in: timeAmount) {
435
+ guard self . currentIdleWriteTimeoutTimerID == timerID else { return }
436
+ let action = self . state. idleWriteTimeoutTriggered ( )
437
+ self . run ( action, context: context)
438
+ }
439
+ case . clearIdleWriteTimeoutTimer:
440
+ if let oldTimer = self . idleWriteTimeoutTimer {
441
+ self . idleWriteTimeoutTimer = nil
442
+ self . currentIdleWriteTimeoutTimerID &+= 1
443
+ oldTimer. cancel ( )
444
+ }
445
+ case . none:
446
+ break
447
+ }
448
+ }
449
+
383
450
// MARK: Private HTTPRequestExecutor
384
451
385
452
private func writeRequestBodyPart0( _ data: IOData , request: HTTPExecutableRequest , promise: EventLoopPromise < Void > ? ) {
@@ -393,6 +460,10 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
393
460
return
394
461
}
395
462
463
+ if let timeoutAction = self . idleWriteTimeoutStateMachine? . write ( ) {
464
+ self . runTimeoutAction ( timeoutAction, context: context)
465
+ }
466
+
396
467
let action = self . state. requestStreamPartReceived ( data, promise: promise)
397
468
self . run ( action, context: context)
398
469
}
@@ -428,6 +499,10 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
428
499
429
500
self . logger. trace ( " Request was cancelled " )
430
501
502
+ if let timeoutAction = self . idleWriteTimeoutStateMachine? . cancelRequest ( ) {
503
+ self . runTimeoutAction ( timeoutAction, context: context)
504
+ }
505
+
431
506
let action = self . state. requestCancelled ( closeConnection: true )
432
507
self . run ( action, context: context)
433
508
}
@@ -540,3 +615,87 @@ struct IdleReadStateMachine {
540
615
}
541
616
}
542
617
}
618
+
619
+ struct IdleWriteStateMachine {
620
+ enum Action {
621
+ case startIdleWriteTimeoutTimer( TimeAmount )
622
+ case resetIdleWriteTimeoutTimer( TimeAmount )
623
+ case clearIdleWriteTimeoutTimer
624
+ case none
625
+ }
626
+
627
+ enum State {
628
+ case waitingForRequestEnd
629
+ case waitingForWritabilityEnabled
630
+ case requestEndSent
631
+ }
632
+
633
+ private var state : State
634
+ private let timeAmount : TimeAmount
635
+
636
+ init ( timeAmount: TimeAmount , isWritabilityEnabled: Bool ) {
637
+ self . timeAmount = timeAmount
638
+ if isWritabilityEnabled {
639
+ self . state = . waitingForRequestEnd
640
+ } else {
641
+ self . state = . waitingForWritabilityEnabled
642
+ }
643
+ }
644
+
645
+ mutating func cancelRequest( ) -> Action {
646
+ switch self . state {
647
+ case . waitingForRequestEnd, . waitingForWritabilityEnabled:
648
+ self . state = . requestEndSent
649
+ return . clearIdleWriteTimeoutTimer
650
+ case . requestEndSent:
651
+ return . none
652
+ }
653
+ }
654
+
655
+ mutating func write( ) -> Action {
656
+ switch self . state {
657
+ case . waitingForRequestEnd:
658
+ return . resetIdleWriteTimeoutTimer( self . timeAmount)
659
+ case . waitingForWritabilityEnabled:
660
+ return . none
661
+ case . requestEndSent:
662
+ preconditionFailure ( " If the request end has been sent, we can't write more data. " )
663
+ }
664
+ }
665
+
666
+ mutating func requestEndSent( ) -> Action {
667
+ switch self . state {
668
+ case . waitingForRequestEnd:
669
+ self . state = . requestEndSent
670
+ return . clearIdleWriteTimeoutTimer
671
+ case . waitingForWritabilityEnabled:
672
+ preconditionFailure ( " If the channel is not writable, we can't have sent the request end. " )
673
+ case . requestEndSent:
674
+ return . none
675
+ }
676
+ }
677
+
678
+ mutating func channelWritabilityChanged( context: ChannelHandlerContext ) -> Action {
679
+ if context. channel. isWritable {
680
+ switch self . state {
681
+ case . waitingForRequestEnd:
682
+ preconditionFailure ( " If waiting for more data, the channel was already writable. " )
683
+ case . waitingForWritabilityEnabled:
684
+ self . state = . waitingForRequestEnd
685
+ return . startIdleWriteTimeoutTimer( self . timeAmount)
686
+ case . requestEndSent:
687
+ return . none
688
+ }
689
+ } else {
690
+ switch self . state {
691
+ case . waitingForRequestEnd:
692
+ self . state = . waitingForWritabilityEnabled
693
+ return . clearIdleWriteTimeoutTimer
694
+ case . waitingForWritabilityEnabled:
695
+ preconditionFailure ( " If the channel was writable before, then we should have been waiting for more data. " )
696
+ case . requestEndSent:
697
+ return . none
698
+ }
699
+ }
700
+ }
701
+ }
0 commit comments