mirror of
https://github.com/swift-server/async-http-client.git
synced 2026-05-03 07:32:29 +00:00
c621142327
Migrate CI to use GitHub Actions. ### Motivation: To migrate to GitHub actions and centralised infrastructure. ### Modifications: Changes of note: * Adopt swift-format using rules from SwiftNIO. * Remove scripts and docker files which are no longer needed. * Disabled warnings-as-errors on Swift 6.0 CI pipelines for now. ### Result: Feature parity with old CI.
318 lines
11 KiB
Swift
318 lines
11 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(delegate: Delegate, logger: Logger, maximumConnectionUses: Int? = nil) {
|
|
self.state = StateMachine(maximumUses: maximumConnectionUses)
|
|
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(maximumUses: Int?)
|
|
case connected(remainingUses: Int?)
|
|
case active(openStreams: Int, maxStreams: Int, remainingUses: Int?)
|
|
case closing(openStreams: Int, maxStreams: Int)
|
|
case closed
|
|
}
|
|
|
|
var state: State
|
|
|
|
init(maximumUses: Int?) {
|
|
self.state = .initialized(maximumUses: maximumUses)
|
|
}
|
|
|
|
mutating func channelActive() {
|
|
switch self.state {
|
|
case .initialized(let maximumUses):
|
|
self.state = .connected(remainingUses: maximumUses)
|
|
|
|
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:
|
|
preconditionFailure("Invalid state: \(self.state)")
|
|
|
|
case .connected(let remainingUses):
|
|
// 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, remainingUses: remainingUses)
|
|
return .notifyConnectionNewMaxStreamsSettings(maxStreams)
|
|
|
|
case .active(let openStreams, let maxStreams, let remainingUses):
|
|
if let newMaxStreams = settings.last(where: { $0.parameter == .maxConcurrentStreams })?.value,
|
|
newMaxStreams != maxStreams
|
|
{
|
|
self.state = .active(
|
|
openStreams: openStreams,
|
|
maxStreams: newMaxStreams,
|
|
remainingUses: remainingUses
|
|
)
|
|
return .notifyConnectionNewMaxStreamsSettings(newMaxStreams)
|
|
}
|
|
return .nothing
|
|
|
|
case .closing:
|
|
return .nothing
|
|
|
|
case .closed:
|
|
// We may receive a Settings frame after we have called connection close, because of
|
|
// packages being delivered from the incoming buffer.
|
|
return .nothing
|
|
}
|
|
}
|
|
|
|
mutating func goAwayReceived() -> Action {
|
|
switch self.state {
|
|
case .initialized:
|
|
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)
|
|
|
|
case .closed:
|
|
// We may receive a GoAway frame after we have called connection close, because of
|
|
// packages being delivered from the incoming buffer.
|
|
return .nothing
|
|
}
|
|
}
|
|
|
|
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 .initialized, .connected:
|
|
preconditionFailure("Invalid state: \(self.state)")
|
|
|
|
case .active(var openStreams, let maxStreams, let remainingUses):
|
|
openStreams += 1
|
|
let remainingUses = remainingUses.map { $0 - 1 }
|
|
self.state = .active(openStreams: openStreams, maxStreams: maxStreams, remainingUses: remainingUses)
|
|
|
|
if remainingUses == 0 {
|
|
// Treat running out of connection uses as if we received a GOAWAY frame. This
|
|
// will notify the delegate (i.e. connection pool) that the connection can no
|
|
// longer be used.
|
|
return self.goAwayReceived()
|
|
} else {
|
|
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 .closed:
|
|
// We may receive a events after we have called connection close, because of
|
|
// internal races. We should just ignore these cases.
|
|
return .nothing
|
|
}
|
|
}
|
|
|
|
mutating func streamClosed() -> Action {
|
|
switch self.state {
|
|
case .initialized, .connected:
|
|
preconditionFailure("Invalid state: \(self.state)")
|
|
|
|
case .active(var openStreams, let maxStreams, let remainingUses):
|
|
openStreams -= 1
|
|
assert(openStreams >= 0)
|
|
self.state = .active(openStreams: openStreams, maxStreams: maxStreams, remainingUses: remainingUses)
|
|
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 .closed:
|
|
// We may receive a events after we have called connection close, because of
|
|
// internal races. We should just ignore these cases.
|
|
return .nothing
|
|
}
|
|
}
|
|
}
|
|
}
|