Files
async-http-client/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2IdleHandler.swift
T
Fabian Fett eab2a84b1c Use explicit NIO imports (#407)
* Use explicit NIO imports for `NIOCore`, `NIOPosix` and `NIOEmbedded`
* Updated dependencies
2021-08-19 21:11:49 +02:00

278 lines
9.4 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Logging
import NIOCore
import NIOHTTP2
protocol HTTP2IdleHandlerDelegate {
func http2SettingsReceived(maxStreams: Int)
func http2GoAwayReceived()
func http2StreamClosed(availableStreams: Int)
}
// This is a `ChannelDuplexHandler` since we need to intercept outgoing user events. It is generic
// over its delegate to allow for specialization.
final class HTTP2IdleHandler<Delegate: HTTP2IdleHandlerDelegate>: ChannelDuplexHandler {
typealias InboundIn = HTTP2Frame
typealias InboundOut = HTTP2Frame
typealias OutboundIn = HTTP2Frame
typealias OutboundOut = HTTP2Frame
let logger: Logger
let delegate: Delegate
private var state: StateMachine = .init()
init(delegate: Delegate, logger: Logger) {
self.delegate = delegate
self.logger = logger
}
func handlerAdded(context: ChannelHandlerContext) {
if context.channel.isActive {
self.state.channelActive()
}
}
func channelActive(context: ChannelHandlerContext) {
self.state.channelActive()
context.fireChannelActive()
}
func channelInactive(context: ChannelHandlerContext) {
self.state.channelInactive()
context.fireChannelInactive()
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let frame = self.unwrapInboundIn(data)
switch frame.payload {
case .goAway:
let action = self.state.goAwayReceived()
self.run(action, context: context)
case .settings(.settings(let settings)):
let action = self.state.settingsReceived(settings)
self.run(action, context: context)
default:
// We're not interested in other events.
break
}
context.fireChannelRead(data)
}
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
// We intercept calls between the `NIOHTTP2ChannelHandler` and the `HTTP2StreamMultiplexer`
// to learn, how many open streams we have.
switch event {
case is StreamClosedEvent:
let action = self.state.streamClosed()
self.run(action, context: context)
case is NIOHTTP2StreamCreatedEvent:
let action = self.state.streamCreated()
self.run(action, context: context)
default:
// We're not interested in other events.
break
}
context.fireUserInboundEventTriggered(event)
}
func triggerUserOutboundEvent(context: ChannelHandlerContext, event: Any, promise: EventLoopPromise<Void>?) {
switch event {
case HTTPConnectionEvent.shutdownRequested:
let action = self.state.closeEventReceived()
self.run(action, context: context)
default:
context.triggerUserOutboundEvent(event, promise: promise)
}
}
private func run(_ action: StateMachine.Action, context: ChannelHandlerContext) {
switch action {
case .nothing:
break
case .notifyConnectionNewMaxStreamsSettings(let maxStreams):
self.delegate.http2SettingsReceived(maxStreams: maxStreams)
case .notifyConnectionStreamClosed(let currentlyAvailable):
self.delegate.http2StreamClosed(availableStreams: currentlyAvailable)
case .notifyConnectionGoAwayReceived:
self.delegate.http2GoAwayReceived()
case .close:
context.close(mode: .all, promise: nil)
}
}
}
extension HTTP2IdleHandler {
struct StateMachine {
enum Action {
case notifyConnectionNewMaxStreamsSettings(Int)
case notifyConnectionGoAwayReceived(close: Bool)
case notifyConnectionStreamClosed(currentlyAvailable: Int)
case nothing
case close
}
enum State {
case initialized
case connected
case active(openStreams: Int, maxStreams: Int)
case closing(openStreams: Int, maxStreams: Int)
case closed
}
var state: State = .initialized
mutating func channelActive() {
switch self.state {
case .initialized:
self.state = .connected
case .connected, .active, .closing, .closed:
break
}
}
mutating func channelInactive() {
switch self.state {
case .initialized, .connected, .active, .closing, .closed:
self.state = .closed
}
}
mutating func settingsReceived(_ settings: HTTP2Settings) -> Action {
switch self.state {
case .initialized, .closed:
preconditionFailure("Invalid state: \(self.state)")
case .connected:
// a settings frame might have multiple entries for `maxConcurrentStreams`. We are
// only interested in the last value! If no `maxConcurrentStreams` is set, we assume
// the http/2 default of 100.
let maxStreams = settings.last(where: { $0.parameter == .maxConcurrentStreams })?.value ?? 100
self.state = .active(openStreams: 0, maxStreams: maxStreams)
return .notifyConnectionNewMaxStreamsSettings(maxStreams)
case .active(openStreams: let openStreams, maxStreams: let maxStreams):
if let newMaxStreams = settings.last(where: { $0.parameter == .maxConcurrentStreams })?.value, newMaxStreams != maxStreams {
self.state = .active(openStreams: openStreams, maxStreams: newMaxStreams)
return .notifyConnectionNewMaxStreamsSettings(newMaxStreams)
}
return .nothing
case .closing:
return .nothing
}
}
mutating func goAwayReceived() -> Action {
switch self.state {
case .initialized, .closed:
preconditionFailure("Invalid state: \(self.state)")
case .connected:
self.state = .closing(openStreams: 0, maxStreams: 0)
return .notifyConnectionGoAwayReceived(close: true)
case .active(let openStreams, let maxStreams):
self.state = .closing(openStreams: openStreams, maxStreams: maxStreams)
return .notifyConnectionGoAwayReceived(close: openStreams == 0)
case .closing:
return .notifyConnectionGoAwayReceived(close: false)
}
}
mutating func closeEventReceived() -> Action {
switch self.state {
case .initialized:
preconditionFailure("Invalid state: \(self.state)")
case .connected:
self.state = .closing(openStreams: 0, maxStreams: 0)
return .close
case .active(let openStreams, let maxStreams):
if openStreams == 0 {
self.state = .closed
return .close
}
self.state = .closing(openStreams: openStreams, maxStreams: maxStreams)
return .nothing
case .closed, .closing:
return .nothing
}
}
mutating func streamCreated() -> Action {
switch self.state {
case .active(var openStreams, let maxStreams):
openStreams += 1
self.state = .active(openStreams: openStreams, maxStreams: maxStreams)
return .nothing
case .closing(var openStreams, let maxStreams):
// A stream might be opened, while we are closing because of race conditions. For
// this reason, we should handle this case.
openStreams += 1
self.state = .closing(openStreams: openStreams, maxStreams: maxStreams)
return .nothing
case .initialized, .connected, .closed:
preconditionFailure("Invalid state: \(self.state)")
}
}
mutating func streamClosed() -> Action {
switch self.state {
case .active(var openStreams, let maxStreams):
openStreams -= 1
assert(openStreams >= 0)
self.state = .active(openStreams: openStreams, maxStreams: maxStreams)
return .notifyConnectionStreamClosed(currentlyAvailable: maxStreams - openStreams)
case .closing(var openStreams, let maxStreams):
openStreams -= 1
assert(openStreams >= 0)
if openStreams == 0 {
self.state = .closed
return .close
}
self.state = .closing(openStreams: openStreams, maxStreams: maxStreams)
return .nothing
case .initialized, .connected, .closed:
preconditionFailure("Invalid state: \(self.state)")
}
}
}
}