Skip to content

Commit

Permalink
Add Idle state handling for HTTP2 channels
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler committed Dec 1, 2023
1 parent 8e01e17 commit 560fafd
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 13 deletions.
59 changes: 51 additions & 8 deletions Sources/HummingbirdHTTP2/ChannelInitializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,66 @@ import NIOSSL

/// Setup child channel for HTTP2
public struct HTTP2ChannelInitializer: HBChannelInitializer {
public init() {}
/// Idle state handler configuration for HTTP2 channel
public struct IdleStateHandlerConfiguration: Sendable {
/// timeout when reading a request
let readTimeout: TimeAmount
/// timeout since last writing a response
let writeTimeout: TimeAmount

public init(readTimeout: TimeAmount = .seconds(30), writeTimeout: TimeAmount = .minutes(3)) {
self.readTimeout = readTimeout
self.writeTimeout = writeTimeout
}

public var idleStateHandler: IdleStateHandler {
IdleStateHandler(readTimeout: self.readTimeout, writeTimeout: self.writeTimeout)
}
}

/// Initialise HTTP2ChannelInitializer
@available(*, deprecated, renamed: "init(idleTimeoutConfiguration:)")
public init() {
self.idleTimeoutConfiguration = nil
}

/// Initialise HTTP2ChannelInitializer
/// - Parameter idleTimeoutConfiguration: Configure when server should close the channel based of idle events
public init(idleTimeoutConfiguration: IdleStateHandlerConfiguration?) {
self.idleTimeoutConfiguration = idleTimeoutConfiguration
}

public func initialize(channel: Channel, childHandlers: [RemovableChannelHandler], configuration: HBHTTPServer.Configuration) -> EventLoopFuture<Void> {
channel.configureHTTP2Pipeline(mode: .server) { streamChannel -> EventLoopFuture<Void> in
return streamChannel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).flatMap { _ in
streamChannel.pipeline.addHandlers(childHandlers)
func configureHTTP2Pipeline() -> EventLoopFuture<Void> {
channel.configureHTTP2Pipeline(mode: .server) { streamChannel -> EventLoopFuture<Void> in
return streamChannel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).flatMap { _ in
streamChannel.pipeline.addHandlers(childHandlers)
}
}.flatMap { _ in
channel.pipeline.addHandler(HTTP2UserEventHandler())
}
}
if let idleTimeoutConfiguration = self.idleTimeoutConfiguration {
return channel.pipeline.addHandler(idleTimeoutConfiguration.idleStateHandler).flatMap {
configureHTTP2Pipeline()
}
}.flatMap { _ in
channel.pipeline.addHandler(HTTP2UserEventHandler())
} else {
return configureHTTP2Pipeline()
}
}

let idleTimeoutConfiguration: IdleStateHandlerConfiguration?
}

/// Setup child channel for HTTP2 upgrade
struct HTTP2UpgradeChannelInitializer: HBChannelInitializer {
var http1 = HTTP1ChannelInitializer()
let http2 = HTTP2ChannelInitializer()
var http1: HTTP1ChannelInitializer
let http2: HTTP2ChannelInitializer

init(idleTimeoutConfiguration: HTTP2ChannelInitializer.IdleStateHandlerConfiguration?) {
self.http1 = HTTP1ChannelInitializer()
self.http2 = HTTP2ChannelInitializer(idleTimeoutConfiguration: idleTimeoutConfiguration)
}

func initialize(channel: Channel, childHandlers: [RemovableChannelHandler], configuration: HBHTTPServer.Configuration) -> EventLoopFuture<Void> {
channel.configureHTTP2SecureUpgrade(
Expand Down
56 changes: 52 additions & 4 deletions Sources/HummingbirdHTTP2/HTTP2UserEventHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ final class HTTP2UserEventHandler: ChannelInboundHandler, RemovableChannelHandle
case is ChannelShouldQuiesceEvent:
self.quiesce(context: context)

case let evt as IdleStateHandler.IdleStateEvent where evt == .read:
self.processIdleReadState(context: context)

case let evt as IdleStateHandler.IdleStateEvent where evt == .write:
self.processIdleWriteState(context: context)

default:
break
}
Expand All @@ -53,8 +59,7 @@ final class HTTP2UserEventHandler: ChannelInboundHandler, RemovableChannelHandle
if numberOpenStreams > 1 {
self.state = .quiescing(numberOpenStreams: numberOpenStreams - 1)
} else {
self.state = .closing
context.close(promise: nil)
self.close(context: context)
}
case .closing:
assertionFailure("If we have initiated a close, there should be no streams to close.")
Expand All @@ -67,11 +72,54 @@ final class HTTP2UserEventHandler: ChannelInboundHandler, RemovableChannelHandle
if numberOpenStreams > 0 {
self.state = .quiescing(numberOpenStreams: numberOpenStreams)
} else {
self.state = .closing
context.close(promise: nil)
self.close(context: context)
}
case .quiescing, .closing:
break
}
}

func processIdleReadState(context: ChannelHandlerContext) {
switch self.state {
case .active(let numberOpenStreams):
// if we get a read idle state and there are streams open
if numberOpenStreams > 0 {
self.close(context: context)
}
case .quiescing(let numberOpenStreams):
// if we get a read idle state and there are streams open
if numberOpenStreams > 0 {
self.close(context: context)
}
default:
break
}
}

func processIdleWriteState(context: ChannelHandlerContext) {
switch self.state {
case .active(let numberOpenStreams):
// if we get a write idle state and there are no longer any streams open
if numberOpenStreams == 0 {
self.close(context: context)
}
case .quiescing(let numberOpenStreams):
// if we get a write idle state and there are no longer any streams open
if numberOpenStreams == 0 {
self.close(context: context)
}
default:
break
}
}

func close(context: ChannelHandlerContext) {
switch self.state {
case .active, .quiescing:
self.state = .closing
context.close(promise: nil)
case .closing:
break
}
}
}
24 changes: 23 additions & 1 deletion Sources/HummingbirdHTTP2/HTTPServer+HTTP2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,35 @@ extension HBHTTPServer {
/// you will then be adding two TLS handlers.
///
/// - Parameter tlsConfiguration: TLS configuration
@available(*, deprecated, renamed: "addHTTP2Upgrade(tlsConfiguration:idleTimeoutConfiguration:)")
@discardableResult public func addHTTP2Upgrade(tlsConfiguration: TLSConfiguration) throws -> HBHTTPServer {
var tlsConfiguration = tlsConfiguration
tlsConfiguration.applicationProtocols.append("h2")
tlsConfiguration.applicationProtocols.append("http/1.1")
let sslContext = try NIOSSLContext(configuration: tlsConfiguration)

self.httpChannelInitializer = HTTP2UpgradeChannelInitializer()
self.httpChannelInitializer = HTTP2UpgradeChannelInitializer(idleTimeoutConfiguration: nil)
return self.addTLSChannelHandler(NIOSSLServerHandler(context: sslContext))
}

/// Add HTTP2 secure upgrade handler
///
/// HTTP2 secure upgrade requires a TLS connection so this will add a TLS handler as well. Do not call `addTLS()` inconjunction with this as
/// you will then be adding two TLS handlers.
///
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - idleTimeoutConfiguration: Configure when server should close the channel based of idle events
@discardableResult public func addHTTP2Upgrade(
tlsConfiguration: TLSConfiguration,
idleTimeoutConfiguration: HTTP2ChannelInitializer.IdleStateHandlerConfiguration
) throws -> HBHTTPServer {
var tlsConfiguration = tlsConfiguration
tlsConfiguration.applicationProtocols.append("h2")
tlsConfiguration.applicationProtocols.append("http/1.1")
let sslContext = try NIOSSLContext(configuration: tlsConfiguration)

self.httpChannelInitializer = HTTP2UpgradeChannelInitializer(idleTimeoutConfiguration: idleTimeoutConfiguration)
return self.addTLSChannelHandler(NIOSSLServerHandler(context: sslContext))
}
}

0 comments on commit 560fafd

Please sign in to comment.