Add "debug initializer" hook for channels (#801)

Motivation:

As requested in #596, it can be handy to have a lower-level access to
channels (HTTP/1 connection, HTTP/2 connection, or HTTP/2 stream) to
enable a more fine-grained interaction for, say, observability, testing,
etc.

Modifications:

- Add 3 new properties (`http1_1ConnectionDebugInitializer`,
`http2ConnectionDebugInitializer` and
`http2StreamChannelDebugInitializer`) to `HTTPClient.Configuration` with
access to the respective channels. These properties are of `Optional`
type `@Sendable (Channel) -> EventLoopFuture<Void>` and are called when
creating a connection/stream.

Result:

Provides APIs for a lower-level access to channels.

---------

Co-authored-by: Cory Benfield <lukasa@apple.com>
Co-authored-by: David Nadoba <d_nadoba@apple.com>
Co-authored-by: George Barnett <gbarnett@apple.com>
This commit is contained in:
Clinton Nkwocha
2025-04-25 12:11:02 +00:00
committed by GitHub
co-authored by Cory Benfield David Nadoba George Barnett
parent 373862aa09
commit a3d00a65b9
5 changed files with 264 additions and 9 deletions
@@ -35,6 +35,9 @@ final class HTTP2Connection {
let multiplexer: HTTP2StreamMultiplexer
let logger: Logger
/// A method with access to the stream channel that is called when creating the stream.
let streamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)?
/// the connection pool that created the connection
let delegate: HTTP2ConnectionDelegate
@@ -95,7 +98,8 @@ final class HTTP2Connection {
decompression: HTTPClient.Decompression,
maximumConnectionUses: Int?,
delegate: HTTP2ConnectionDelegate,
logger: Logger
logger: Logger,
streamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)? = nil
) {
self.channel = channel
self.id = connectionID
@@ -114,6 +118,7 @@ final class HTTP2Connection {
)
self.delegate = delegate
self.state = .initialized
self.streamChannelDebugInitializer = streamChannelDebugInitializer
}
deinit {
@@ -128,7 +133,8 @@ final class HTTP2Connection {
delegate: HTTP2ConnectionDelegate,
decompression: HTTPClient.Decompression,
maximumConnectionUses: Int?,
logger: Logger
logger: Logger,
streamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)? = nil
) -> EventLoopFuture<(HTTP2Connection, Int)> {
let connection = HTTP2Connection(
channel: channel,
@@ -136,7 +142,8 @@ final class HTTP2Connection {
decompression: decompression,
maximumConnectionUses: maximumConnectionUses,
delegate: delegate,
logger: logger
logger: logger,
streamChannelDebugInitializer: streamChannelDebugInitializer
)
return connection._start0().map { maxStreams in (connection, maxStreams) }
}
@@ -259,8 +266,14 @@ final class HTTP2Connection {
self.openStreams.remove(box)
}
channel.write(request, promise: nil)
return channel.eventLoop.makeSucceededVoidFuture()
if let streamChannelDebugInitializer = self.streamChannelDebugInitializer {
return streamChannelDebugInitializer(channel).map { _ in
channel.write(request, promise: nil)
}
} else {
channel.write(request, promise: nil)
return channel.eventLoop.makeSucceededVoidFuture()
}
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
@@ -85,7 +85,19 @@ extension HTTPConnectionPool.ConnectionFactory {
decompression: self.clientConfiguration.decompression,
logger: logger
)
requester.http1ConnectionCreated(connection)
if let connectionDebugInitializer = self.clientConfiguration.http1_1ConnectionDebugInitializer {
connectionDebugInitializer(channel).whenComplete { debugInitializerResult in
switch debugInitializerResult {
case .success:
requester.http1ConnectionCreated(connection)
case .failure(let error):
requester.failedToCreateHTTPConnection(connectionID, error: error)
}
}
} else {
requester.http1ConnectionCreated(connection)
}
} catch {
requester.failedToCreateHTTPConnection(connectionID, error: error)
}
@@ -96,11 +108,34 @@ extension HTTPConnectionPool.ConnectionFactory {
delegate: http2ConnectionDelegate,
decompression: self.clientConfiguration.decompression,
maximumConnectionUses: self.clientConfiguration.maximumUsesPerConnection,
logger: logger
logger: logger,
streamChannelDebugInitializer:
self.clientConfiguration.http2StreamChannelDebugInitializer
).whenComplete { result in
switch result {
case .success((let connection, let maximumStreams)):
requester.http2ConnectionCreated(connection, maximumStreams: maximumStreams)
if let connectionDebugInitializer = self.clientConfiguration.http2ConnectionDebugInitializer {
connectionDebugInitializer(channel).whenComplete {
debugInitializerResult in
switch debugInitializerResult {
case .success:
requester.http2ConnectionCreated(
connection,
maximumStreams: maximumStreams
)
case .failure(let error):
requester.failedToCreateHTTPConnection(
connectionID,
error: error
)
}
}
} else {
requester.http2ConnectionCreated(
connection,
maximumStreams: maximumStreams
)
}
case .failure(let error):
requester.failedToCreateHTTPConnection(connectionID, error: error)
}
@@ -324,7 +324,9 @@ final class HTTPConnectionPool:
connection.executeRequest(request.req)
case .executeRequests(let requests, let connection):
for request in requests { connection.executeRequest(request.req) }
for request in requests {
connection.executeRequest(request.req)
}
case .failRequest(let request, let error):
request.req.fail(error)