Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove ServerChildChannel generic associatedtype from ApplicationProtocol #426

Merged
merged 3 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 12 additions & 16 deletions Sources/Hummingbird/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,13 @@ public enum EventLoopGroupProvider {
public protocol ApplicationProtocol: Service where Context: RequestContext {
/// Responder that generates a response from a requests and context
associatedtype Responder: HTTPResponder
/// Child Channel setup. This defaults to support HTTP1
associatedtype ChildChannel: ServerChildChannel & HTTPChannelHandler = HTTP1Channel
/// Context passed with Request to responder
typealias Context = Responder.Context

/// Build the responder
var responder: Responder { get async throws }
/// Server channel setup
var server: HTTPChannelBuilder<ChildChannel> { get }
/// Server channel builder
var server: HTTPChannelBuilder { get }

/// event loop group used by application
var eventLoopGroup: EventLoopGroup { get }
Expand All @@ -71,7 +69,7 @@ public protocol ApplicationProtocol: Service where Context: RequestContext {

extension ApplicationProtocol {
/// Server channel setup
public var server: HTTPChannelBuilder<HTTP1Channel> { .http1() }
public var server: HTTPChannelBuilder { .http1() }
}

extension ApplicationProtocol {
Expand Down Expand Up @@ -111,17 +109,16 @@ extension ApplicationProtocol {
}
return response
}
// get channel Setup
let channelSetup = try self.server.build(respond)
// create server
let server = Server(
childChannelSetup: channelSetup,
// get server channel
let serverChannel = try self.server.build(respond)
// create server `Service``
let server = serverChannel.server(
configuration: self.configuration.httpServer,
onServerRunning: self.onServerRunning,
eventLoopGroup: self.eventLoopGroup,
logger: self.logger
)
let serverService = PrecursorService(service: server) {
let serverService = server.withPrelude {
for process in self.processesRunBeforeServerStart {
try await process()
}
Expand Down Expand Up @@ -159,9 +156,8 @@ extension ApplicationProtocol {
/// try await app.runService()
/// ```
/// Editing the application setup after calling `runService` will produce undefined behaviour.
public struct Application<Responder: HTTPResponder, ChildChannel: ServerChildChannel & HTTPChannelHandler>: ApplicationProtocol where Responder.Context: RequestContext {
public struct Application<Responder: HTTPResponder>: ApplicationProtocol where Responder.Context: RequestContext {
public typealias Context = Responder.Context
public typealias ChildChannel = ChildChannel
public typealias Responder = Responder

// MARK: Member variables
Expand All @@ -177,7 +173,7 @@ public struct Application<Responder: HTTPResponder, ChildChannel: ServerChildCha
/// on server running
private var _onServerRunning: @Sendable (Channel) async -> Void
/// Server channel setup
public let server: HTTPChannelBuilder<ChildChannel>
public let server: HTTPChannelBuilder
/// services attached to the application.
public var services: [any Service]
/// Processes to be run before server is started
Expand All @@ -196,7 +192,7 @@ public struct Application<Responder: HTTPResponder, ChildChannel: ServerChildCha
/// - logger: Logger application uses
public init(
responder: Responder,
server: HTTPChannelBuilder<ChildChannel> = .http1(),
server: HTTPChannelBuilder = .http1(),
configuration: ApplicationConfiguration = ApplicationConfiguration(),
onServerRunning: @escaping @Sendable (Channel) async -> Void = { _ in },
eventLoopGroupProvider: EventLoopGroupProvider = .singleton,
Expand Down Expand Up @@ -230,7 +226,7 @@ public struct Application<Responder: HTTPResponder, ChildChannel: ServerChildCha
/// - logger: Logger application uses
public init<ResponderBuilder: HTTPResponderBuilder>(
router: ResponderBuilder,
server: HTTPChannelBuilder<ChildChannel> = .http1(),
server: HTTPChannelBuilder = .http1(),
configuration: ApplicationConfiguration = ApplicationConfiguration(),
onServerRunning: @escaping @Sendable (Channel) async -> Void = { _ in },
eventLoopGroupProvider: EventLoopGroupProvider = .singleton,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,29 @@

import ServiceLifecycle

/// Wrap another service to run after a precursor closure has completed
struct PrecursorService<S: Service>: Service, CustomStringConvertible {
let precursor: @Sendable () async throws -> Void
/// Wrap another service to run after a prelude closure has completed
struct PreludeService<S: Service>: Service, CustomStringConvertible {
let prelude: @Sendable () async throws -> Void
let service: S

var description: String {
"PrecursorService<\(S.self)>"
"PreludeService<\(S.self)>"
}

init(service: S, process: @escaping @Sendable () async throws -> Void) {
init(service: S, prelude: @escaping @Sendable () async throws -> Void) {
self.service = service
self.precursor = process
self.prelude = prelude
}

func run() async throws {
try await self.precursor()
try await self.prelude()
try await self.service.run()
}
}

extension Service {
/// Build existential ``PreludeService`` from an existential `Service`
func withPrelude(_ prelude: @escaping @Sendable () async throws -> Void) -> Service {
PreludeService(service: self, prelude: prelude)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ import NIOCore
///
/// Used when building an ``Hummingbird/Application``. It delays the building
/// of the ``ServerChildChannel`` until the HTTP responder has been built.
public struct HTTPChannelBuilder<ChildChannel: ServerChildChannel>: Sendable {
public struct HTTPChannelBuilder: Sendable {
/// build child channel from HTTP responder
public let build: @Sendable (@escaping HTTPChannelHandler.Responder) throws -> ChildChannel
public let build: @Sendable (@escaping HTTPChannelHandler.Responder) throws -> any ServerChildChannel

/// Initialize HTTPChannelBuilder
/// - Parameter build: closure building child channel from HTTP responder
public init(_ build: @escaping @Sendable (@escaping HTTPChannelHandler.Responder) throws -> ChildChannel) {
public init(_ build: @escaping @Sendable (@escaping HTTPChannelHandler.Responder) throws -> any ServerChildChannel) {
self.build = build
}
}
Expand All @@ -43,7 +43,7 @@ extension HTTPChannelBuilder {
/// - Returns: HTTPChannelHandler builder
public static func http1(
additionalChannelHandlers: @autoclosure @escaping @Sendable () -> [any RemovableChannelHandler] = []
) -> HTTPChannelBuilder<HTTP1Channel> {
) -> HTTPChannelBuilder {
return .init { responder in
return HTTP1Channel(responder: responder, additionalChannelHandlers: additionalChannelHandlers)
}
Expand Down
7 changes: 5 additions & 2 deletions Sources/HummingbirdCore/Server/Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ public actor Server<ChildChannel: ServerChildChannel>: Service {

/// Initialize Server
/// - Parameters:
/// - group: EventLoopGroup server uses
/// - childChannelSetup: Server child channel
/// - configuration: Configuration for server
/// - onServerRunning: Closure to run once server is up and running
/// - eventLoopGroup: EventLoopGroup the server uses
/// - logger: Logger used by server
public init(
childChannelSetup: ChildChannel,
configuration: ServerConfiguration,
Expand Down Expand Up @@ -209,7 +212,7 @@ public actor Server<ChildChannel: ServerChildChannel>: Service {
logger: self.logger
)
}
self.logger.info("Server started and listening on \(host):\(port)")
self.logger.info("Server started and listening on \(host):\(asyncChannel.channel.localAddress?.port ?? port)")
return asyncChannel

case .unixDomainSocket(let path):
Expand Down
26 changes: 26 additions & 0 deletions Sources/HummingbirdCore/Server/ServerChildChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import Logging
import NIOCore
import ServiceLifecycle

/// HTTPServer child channel setup protocol
public protocol ServerChildChannel: Sendable {
Expand All @@ -32,3 +33,28 @@ public protocol ServerChildChannel: Sendable {
/// - logger: Logger to use while processing messages
func handle(value: Value, logger: Logger) async
}

extension ServerChildChannel {
/// Build existential ``Server`` from existential `ServerChildChannel`
///
/// - Parameters:
/// - configuration: Configuration for server
/// - onServerRunning: Closure to run once server is up and running
/// - eventLoopGroup: EventLoopGroup the server uses
/// - logger: Logger used by server
/// - Returns: Server Service
public func server(
configuration: ServerConfiguration,
onServerRunning: (@Sendable (Channel) async -> Void)? = { _ in },
eventLoopGroup: EventLoopGroup,
logger: Logger
) -> Service {
Server(
childChannelSetup: self,
configuration: configuration,
onServerRunning: onServerRunning,
eventLoopGroup: eventLoopGroup,
logger: logger
)
}
}
2 changes: 1 addition & 1 deletion Sources/HummingbirdHTTP2/HTTP2ChannelBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ extension HTTPChannelBuilder {
public static func http2Upgrade(
tlsConfiguration: TLSConfiguration,
additionalChannelHandlers: @autoclosure @escaping @Sendable () -> [any RemovableChannelHandler] = []
) throws -> HTTPChannelBuilder<HTTP2UpgradeChannel> {
) throws -> HTTPChannelBuilder {
return .init { responder in
return try HTTP2UpgradeChannel(
tlsConfiguration: tlsConfiguration,
Expand Down
7 changes: 7 additions & 0 deletions Sources/HummingbirdTLS/TLSChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,10 @@ extension TLSChannel: HTTPChannelHandler where BaseChannel: HTTPChannelHandler {
baseChannel.responder
}
}

extension ServerChildChannel {
/// Construct existential ``TLSChannel`` from existential `ServerChildChannel`
func withTLS(tlsConfiguration: TLSConfiguration) throws -> any ServerChildChannel {
try TLSChannel(self, tlsConfiguration: tlsConfiguration)
}
}
8 changes: 4 additions & 4 deletions Sources/HummingbirdTLS/TLSChannelBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ extension HTTPChannelBuilder {
/// - base: Base child channel to wrap with TLS
/// - tlsConfiguration: TLS configuration
/// - Returns: HTTPChannelHandler builder
public static func tls<BaseChannel: ServerChildChannel>(
_ base: HTTPChannelBuilder<BaseChannel> = .http1(),
public static func tls(
_ base: HTTPChannelBuilder = .http1(),
tlsConfiguration: TLSConfiguration
) throws -> HTTPChannelBuilder<TLSChannel<BaseChannel>> {
) throws -> HTTPChannelBuilder {
return .init { responder in
return try TLSChannel(base.build(responder), tlsConfiguration: tlsConfiguration)
return try base.build(responder).withTLS(tlsConfiguration: tlsConfiguration)
}
}
}
3 changes: 1 addition & 2 deletions Sources/HummingbirdTesting/TestApplication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@ import ServiceLifecycle
/// This is needed to override the `onServerRunning` function
internal struct TestApplication<BaseApp: ApplicationProtocol>: ApplicationProtocol, Service {
typealias Responder = BaseApp.Responder
typealias ChildChannel = BaseApp.ChildChannel

let base: BaseApp

var responder: Responder {
get async throws { try await self.base.responder }
}

var server: HTTPChannelBuilder<ChildChannel> {
var server: HTTPChannelBuilder {
self.base.server
}

Expand Down
86 changes: 42 additions & 44 deletions Tests/HummingbirdCoreTests/CoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,53 +309,51 @@ class HummingBirdCoreTests: XCTestCase {
}

func testChildChannelGracefulShutdown() async throws {
let promise = Promise<Void>()
let handlerPromise = Promise<Void>()

try await testServer(
responder: { request, _ in
await promise.complete(())
try await Task.sleep(for: .milliseconds(500))
return Response(status: .ok, body: .init(asyncSequence: request.body.delayed()))
},
httpChannelSetup: .http1(),
configuration: .init(address: .hostname(port: 0)),
eventLoopGroup: Self.eventLoopGroup,
logger: Logger(label: "Hummingbird")
) { server, client in
try await withTimeout(.seconds(5)) {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
do {
let response = try await client.get("/")
XCTAssertEqual(response.status, .ok)
} catch {
XCTFail("Error: \(error)")
}
}
await promise.wait()
try await server.shutdownGracefully()
try await group.waitForAll()
}
}
let childChannel = try HTTPChannelBuilder.http1().build { request, _ in
await handlerPromise.complete(())
try await Task.sleep(for: .milliseconds(500))
return Response(status: .ok, body: .init(asyncSequence: request.body.delayed()))
}
}

func testIdleChildChannelGracefulShutdown() async throws {
try await testServer(
responder: { request, _ in
try await Task.sleep(for: .milliseconds(500))
return Response(status: .ok, body: .init(asyncSequence: request.body.delayed()))
},
httpChannelSetup: .http1(),
configuration: .init(address: .hostname(port: 0)),
eventLoopGroup: Self.eventLoopGroup,
logger: Logger(label: "Hummingbird")
) { server, client in
try await withTimeout(.seconds(5)) {
let response = try await client.get("/")
XCTAssertEqual(response.status, .ok)
try await server.shutdownGracefully()
await withThrowingTaskGroup(of: Void.self) { group in
let portPromise = Promise<Int>()
let logger = Logger(label: "Hummingbird")
let server = childChannel.server(
configuration: .init(address: .hostname(port: 0)),
onServerRunning: { await portPromise.complete($0.localAddress!.port!) },
eventLoopGroup: Self.eventLoopGroup,
logger: logger
)
let serviceGroup = ServiceGroup(
configuration: .init(
services: [server],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: logger
)
)
group.addTask {
try await serviceGroup.run()
}
let client = await TestClient(
host: "localhost",
port: portPromise.wait(),
configuration: .init(),
eventLoopGroupProvider: .createNew
)
group.addTask {
do {
client.connect()
let response = try await client.get("/")
XCTAssertEqual(response.status, .ok)
} catch {
XCTFail("Error: \(error)")
}
}
// wait until we are sure handler has been called
await handlerPromise.wait()
// trigger graceful shutdown
await serviceGroup.triggerGracefulShutdown()
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/HummingbirdCoreTests/HTTP2Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class HummingBirdHTTP2Tests: XCTestCase {
configuration: .init(address: .hostname(port: 0), serverName: testServerName),
eventLoopGroup: eventLoopGroup,
logger: Logger(label: "Hummingbird")
) { _, port in
) { port in
var tlsConfiguration = try getClientTLSConfiguration()
// no way to override the SSL server name with AsyncHTTPClient so need to set
// hostname verification off
Expand Down
Loading
Loading