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

Add "debug initializer" hook for channels #801

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

clintonpi
Copy link

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.

Motivation:

As requested in swift-server#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.
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start, I left some feedback inline.

Comment on lines 870 to 875
http1_1ConnectionDebugInitializer:
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
http2ConnectionDebugInitializer:
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
http2StreamChannelDebugInitializer:
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding parameters to a function is a SemVer breaking change, even if they have defaults.

The reason is that you could store the function in a variable:

struct Foo {
  var bar: Int
  init(bar: Int) { ... }
}

// This is totally value
let makeFoo = Foo.init(bar:)
let foo = makeFoo(bar: 42) // equivalent to Foo(bar: 42)

If you subsequently add a parameter to the init then this is an API break because init(bar:) won't exist, even if there's a default for the param you add.

The upshot of this is that you should add a new init rather than updating the existing ones.

Comment on lines 230 to 233
private func executeRequest0(
_ request: HTTPExecutableRequest,
streamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)?
) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this API should change. Instead I think the initialiser should be passed in to the start method. The initialiser isn't something that changes; it remains the same for all requests so it's really a property of the connection.

Tests/AsyncHTTPClientTests/HTTPClientTests.swift Outdated Show resolved Hide resolved
}
}

class DebugInitializerUtil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a very helpful name, something like CountingDebugInitializer would be more appropriate as it hints to what it actually does.

var executionCount: Int

@Sendable
func operation(channel: Channel) -> EventLoopFuture<Void> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in light of the comment above re: naming, initialize would be a more obvious function name.

@clintonpi clintonpi requested a review from glbrntt January 21, 2025 17:15
}

final class CountingDebugInitializerUtil: Sendable {
let executionCount: NIOLockedValueBox<Int>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A useful thing to do is to make this private and have a computed property which returns the value from the box; this makes it easier for any test which wants the value to get it without having to go via withLockedValue.

let executionCount: NIOLockedValueBox<Int>

/// The acual debug initializer.
@Sendable
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this annotation as the type is Sendable

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A warning is given for the implicit conversion.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's because you do http1_1ConnectionDebugInitializer: connectionDebugInitializerUtil.initialize(channel:) rather than:

http1_1ConnectionDebugInitializer: { channel in 
   connectionDebugInitializerUtil.initialize(channel: channel) 
}

)
defer { XCTAssertNoThrow(client.shutdown()) }

let bin = HTTPBin(.http1_1(ssl: true, compress: false))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably the initialiser also works when we're not using SSL? I'm not super familiar with this codebase so would be good to check that.

@clintonpi clintonpi requested a review from glbrntt January 23, 2025 09:43
@Lukasa Lukasa added the 🆕 semver/minor Adds new public API. label Jan 23, 2025
Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really nice, I've left a couple of suggestions for the tests.

Tests/AsyncHTTPClientTests/HTTPClientTests.swift Outdated Show resolved Hide resolved
func initialize(channel: Channel) -> EventLoopFuture<Void> {
self._executionCount.withLockedValue { $0 += 1 }

return channel.eventLoop.makeSucceededVoidFuture()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be useful to add some tests that have the debug initializer actually delay the setup. That is, make sure that we actually do wait for this to complete. Easiest thing to do might be to set some low timeouts, and then confirm the timeouts fire, and that completing the promise doesn't then cause any issues.

clintonpi and others added 2 commits January 24, 2025 09:03
@clintonpi clintonpi requested a review from Lukasa January 24, 2025 14:20
Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one, thanks @clintonpi!

@glbrntt
Copy link
Collaborator

glbrntt commented Jan 24, 2025

Thanks @clintonpi -- this'll be a really handy feature!

@glbrntt glbrntt linked an issue Jan 24, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add functionality to add "Debug handlers" to the channel pipelines
3 participants