Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 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
3 changes: 1 addition & 2 deletions Sources/NIOCore/AsyncChannel/AsyncChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,11 @@ public struct NIOAsyncChannel<Inbound: Sendable, Outbound: Sendable>: Sendable {
/// The stream of inbound messages.
///
/// - Important: The `inbound` stream is a unicast `AsyncSequence` and only one iterator can be created.
@available(*, deprecated, message: "Use the executeThenClose scoped method instead.")
public var inbound: NIOAsyncChannelInboundStream<Inbound> {
self._inbound
}

/// The writer for writing outbound messages.
@available(*, deprecated, message: "Use the executeThenClose scoped method instead.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we undeprecating these?

Copy link
Member Author

Choose a reason for hiding this comment

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

because we want to pass in the NIOAsyncChannel and users shall then use inbound and outbound on it. So that potentially we can add further properties to NIOAsyncChannel later.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can we undeprecate with an spi?

Copy link
Member

Choose a reason for hiding this comment

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

No you can't. What you can do is put it behind a trait like _ExperimentalStructuredBootstrap. You just have to duplicate the properties.

Copy link
Member Author

Choose a reason for hiding this comment

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

should I intro new in and out properties and mark them with the same @_spi?

public var outbound: NIOAsyncChannelOutboundWriter<Outbound> {
self._outbound
}
Expand Down
291 changes: 280 additions & 11 deletions Sources/NIOPosix/Bootstrap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,204 @@ public final class ServerBootstrap {
// MARK: Async bind methods

extension ServerBootstrap {

/// Represents a target address or socket for binding a server socket.
///
/// `BindTarget` provides a type-safe way to specify different types of binding targets
/// for server bootstraps. It supports various address types including network addresses,
/// Unix domain sockets, VSOCK addresses, and existing socket handles.
public struct BindTarget: Sendable {

enum Base {
case hostAndPort(host: String, port: Int)
case socketAddress(SocketAddress)
case unixDomainSocketPath(String)
case vsockAddress(VsockAddress)
case socket(NIOBSDSocket.Handle)
}

var base: Base

/// Creates a binding target for a hostname and port.
///
/// This method creates a target that will resolve the hostname and bind to the
/// specified port. The hostname resolution follows standard system behavior
/// and may resolve to both IPv4 and IPv6 addresses depending on system configuration.
///
/// - Parameters:
/// - host: The hostname or IP address to bind to. Can be a domain name like
/// "localhost" or "example.com", or an IP address like "127.0.0.1" or "::1"
/// - port: The port number to bind to (0-65535). Use 0 to let the system
/// choose an available port
public static func hostAndPort(_ host: String, _ port: Int) -> BindTarget {
BindTarget(base: .hostAndPort(host: host, port: port))
}

/// Creates a binding target for a specific socket address.
///
/// Use this method when you have a pre-constructed ``SocketAddress`` that
/// specifies the exact binding location, including IPv4, IPv6, or Unix domain addresses.
///
/// - Parameter address: The socket address to bind to
public static func socketAddress(_ address: SocketAddress) -> BindTarget {
BindTarget(base: .socketAddress(address))
}

/// Creates a binding target for a Unix domain socket.
///
/// Unix domain sockets provide high-performance inter-process communication
/// on the same machine using filesystem paths. The socket file will be created
/// at the specified path when binding occurs.
///
/// - Parameter path: The filesystem path for the Unix domain socket.
/// Must be a valid filesystem path and should not exist
/// unless cleanup is enabled in the binding operation
/// - Warning: The path must not exist.
public static func unixDomainSocketPath(_ path: String) -> BindTarget {
BindTarget(base: .unixDomainSocketPath(path))
}

/// Creates a binding target for a VSOCK address.
///
/// VSOCK (Virtual Socket) provides communication between virtual machines and their hosts,
/// or between different virtual machines on the same host. This is commonly used
/// in virtualized environments for guest-host communication.
///
/// - Parameter vsockAddress: The VSOCK address to bind to, containing both
/// context ID (CID) and port number
/// - Note: VSOCK support depends on the underlying platform and virtualization technology
public static func vsockAddress(_ vsockAddress: VsockAddress) -> BindTarget {
BindTarget(base: .vsockAddress(vsockAddress))
}

/// Creates a binding target for an existing socket handle.
///
/// This method allows you to use a pre-existing socket that has already been
/// created and optionally configured. This is useful for advanced scenarios where you
/// need custom socket setup before binding, or when integrating with external libraries.
///
/// - Parameters:
/// - handle: The existing socket handle to use. Must be a valid, open socket
/// that is compatible with the intended server bootstrap type
/// - Note: The bootstrap will take ownership of the socket handle and will close
/// it when the server shuts down
public static func socket(_ handle: NIOBSDSocket.Handle) -> BindTarget {
BindTarget(base: .socket(handle))
}
}

/// Bind the `ServerSocketChannel` to the ``BindTarget``. This method will returns once all connections that
/// were spawned have been closed.
///
/// # Supporting graceful shutdown
///
/// To support a graceful server shutdown we recommend using the `ServerQuiescingHelper` from the
/// SwiftNIO extras package. The `ServerQuiescingHelper` can be installed using the
/// ``ServerBootstrap/serverChannelInitializer`` callback.
///
/// Below you can find the code to setup a simple TCP echo server that supports graceful server closure.
///
/// ```swift
/// let quiesce = ServerQuiescingHelper(group: group)
/// let signalSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: signalQueue)
/// signalSource.setEventHandler {
/// signalSource.cancel()
/// print("received signal, initiating shutdown which should complete after the last request finished.")
///
/// quiesce.initiateShutdown(promise: fullyShutdownPromise)
/// }
/// try await ServerBootstrap(group: self.eventLoopGroup)
/// .serverChannelInitializer { channel in
/// channel.eventLoop.makeCompletedFuture {
/// try channel.pipeline.syncOperations.addHandler(quiesce.makeServerChannelHandler(channel: channel))
/// }
/// }
/// .serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
/// .bind(
/// target: .hostAndPort(self.host, self.port),
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the label here be to:? e.g. bind(to: .hostAndPort("foo", 0))

Copy link
Member Author

Choose a reason for hiding this comment

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

I've discussed this with @Lukasa, and he wants this to be target, as this type might change in the future. With a special label we can deprecate much better, than with a to: label.

/// childChannelInitializer: { channel in
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you use multiple trailing closure syntax here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this is done in the NIOTCPEchoExample which is part of this pr.

/// channel.eventLoop.makeCompletedFuture {
/// try channel.pipeline.syncOperations.addHandler(ByteToMessageHandler(NewlineDelimiterCoder()))
/// try channel.pipeline.syncOperations.addHandler(MessageToByteHandler(NewlineDelimiterCoder()))
///
/// return try NIOAsyncChannel(
/// wrappingChannelSynchronously: channel,
/// configuration: NIOAsyncChannel.Configuration(
/// inboundType: String.self,
/// outboundType: String.self
/// )
/// )
/// }
/// }
/// ) { channel in
/// print("Handling new connection")
/// await self.handleConnection(channel: channel)
/// print("Done handling connection")
/// }
///
/// ```
///
/// - Parameters:
/// - target: The ``BindTarget`` to use.
/// - serverBackPressureStrategy: The back pressure strategy used by the server socket channel.
/// - childChannelInitializer: A closure to initialize the channel. The return value of this closure is used in the `onConnection`
/// closure.
/// - onceStartup: A closure that will be called once the server has been started. Use this to get access to
/// the port number, if you used port `0` in the ``BindTarget``.
/// - onConnection: A closure to handle the connection. Use the channel's `inbound` property to read from
/// the connection and channel's `outbound` to write to the connection.
///
/// - Note: The bind method respects task cancellation which will force close the server. If you want to gracefully
/// shut-down use the quiescing helper approach as outlined above.
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
public func bind<Inbound: Sendable, Outbound: Sendable>(
Copy link
Contributor

Choose a reason for hiding this comment

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

If I implement a server using this method, how can I access the address that the listening socket was bound to? (e.g. I set port to zero and want to know the actual port I got.)

Currently it seems the only way to do this is via the parent channel of an accepted connection. This seems like a pretty big API deficiency.

Copy link
Member

Choose a reason for hiding this comment

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

I think that's a good point and we probably need another closure that is called once the server channel is bound.

Copy link
Member Author

@fabianfett fabianfett Sep 23, 2025

Choose a reason for hiding this comment

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

@glbrntt I've added a onceStartup closure. Is this sufficient for you @glbrntt?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should be async (see other comment) and I'm not keen on the name. Maybe something along the lines of onTCPListener / onListeningChannel? That'd at least be consistent with onConnection.

Copy link
Member

Choose a reason for hiding this comment

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

I agree. I think actually it should be async and throws since it is similar to a with-style method i.e. it runs in the calling task. I also like with the onXXX naming. To throw in one more alternative onListeningChannelBound

Copy link
Member Author

Choose a reason for hiding this comment

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

went for onListeningChannel and handleConnection (as @FranzBusch suggested somewhere else). Please bikeshed! @glbrntt @FranzBusch @Lukasa

Copy link
Contributor

Choose a reason for hiding this comment

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

I would bias towards some level of consistency.

We already have the established {server,child}ChannelInitializer pattern and we use childChannelInitializer in this signature already.

So perhaps we should have:

  • handleServerChannel and
  • handleChildChannel

This has clear parallels with the existing bootstrap APIs.

Copy link
Member Author

Choose a reason for hiding this comment

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

@glbrntt love it!

target: BindTarget,
serverBackPressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark? = nil,
childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<NIOAsyncChannel<Inbound, Outbound>>,
onceStartup: (Channel) -> () = { _ in },
onConnection: @escaping @Sendable (
_ channel: NIOAsyncChannel<Inbound, Outbound>
) async -> ()
) async throws {
let channel = try await self.makeConnectedChannel(
target: target,
serverBackPressureStrategy: serverBackPressureStrategy,
childChannelInitializer: childChannelInitializer
)

onceStartup(channel.channel)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be part of the task group instead of before it.

Users can modify the server channel pipeline in the bootstrap so they should be able to interact with it while the server is running (maybe they're waiting for a signal before firing something down the pipeline).

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I'm following. When you say part of the task group you mean moving it into the beginning of the withDiscardingTaskGroup or into a child task?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, that was unclear, I meant as a child task.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I agree with this since it significantly changes the composability of the closure. When it moves into a child task then it must become sending at least which means you can't interact with any task isolated state. Let's take a common server example

final class HTTPServer {
    var state = "starting"

    func run() async throws {
        try await ServerBootstrap.bind(
            target: .hostAndPort(self.host, self.port),
            onListeningChannelBound: { channel in self.state = "bound: \(channel.localAddress!)" }
            onConnection: { ... }
        )
    } 
}

If it moves into a child task then onListeningChannelBound: { channel in self.state = "bound: \(channel.localAddress!)" } will need the state to move into a Mutex or use another synchronization mechanism.

Copy link
Contributor

Choose a reason for hiding this comment

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

This means that you can only use the channel before the server starts processing requests though.

What if I want to later send an event into the server channel?

Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't the intent of these APIs to use structured concurrency?

Copy link
Member

Choose a reason for hiding this comment

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

@FranzBusch but you 100% need a lock around that.

The code I pasted above is data race safe. Channel is Sendable so you are allowed to store it in your local property. Also the closure is not sending or @Sendable.

Isn't the intent of these APIs to use structured concurrency?

I hear you and I can buy into saying that this is the reason that it needs to be called within a child task but that child tasks is going to introduce new race conditions such as a connection being handled before the onListeningChannelBound closure is called. So maybe we need both?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, maybe? I don't love giving two separate closures here, I think it makes the API more confusing than it should be.

When would the race matter? Anything that needs to be applied before channels are accepted should be done in the channel initializer.

Copy link
Member Author

Choose a reason for hiding this comment

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

@Lukasa do you have an opinion here?

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if the tension is best addressed in a different way.

The serverChannelInitializer can be used for unstructured access to the server channel, and it can be used to escape the server channel reference as needed, though it does require a lock to do it. There is no way to get access to the server channel before you have bound it other than that spelling, and that spelling isn't structured. We can choose to make it so, but I'm honestly not sure it's the right answer.

However, having an async closure to operate on the server channel does make sense. This task provides a place to do asynchronous operations that may want to monitor the server channel in various ways. So my instinct is that I am leaning towards George's proposal, with the suggestion that we should document the behaviour required for non-structured access to the server channel.

In the event that we want a "before active" step, that's still something we can add later, and should probably have that specific name to indicate its lifecycle behaviour.


try await withTaskCancellationHandler {
try await channel.executeThenClose { inbound, outbound in
// we need to dance the result dance here, since we can't throw from the
// withDiscardingTaskGroup closure.
let result = await withDiscardingTaskGroup { group -> Result<Void, any Error> in
do {
try await channel.executeThenClose { inbound in
for try await connectionChannel in inbound {
group.addTask {
do {
try await connectionChannel.executeThenClose { _, _ in
await onConnection(connectionChannel)
}
Comment on lines 706 to 708
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand the rationale for passing in the NIOAsyncChannel but if we use this approach the API becomes pretty confusing:

  • If I use this structured API I mustn't call executeThenClose and I'm expected to use inbound and outbound which are no longer deprecated
  • If I use another bind method which isn't structured then I must call executeThenClose and shouldn't use inbound and outbound

I think the usability here needs a little more thought. We could just pass in the inbound, outbound and channel.

Copy link
Member

Choose a reason for hiding this comment

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

My thought process here was that initially when we designed the NIOAsyncChannel we wanted folks to use inbound and outbound. Then we realized our bootstraps aren't structured and we added the executeThenClose method. Now with the structured bootstraps we can go back to inbound/outbound. However, we need to tell one coherent story so I would argue to deprecate all the other bind methods + executeThenClose and push users to these new structured APIs.

We do have to consider the ecosystem churn this produces but I personally would push on us deprecating the non-structured bootstrap methods.

Copy link
Member Author

Choose a reason for hiding this comment

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

@glbrntt I think I'm with Franz here. Does this approach work for you as well?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm okay with that if we can first prove out that the new APIs (not just the server one) are suitable replacements. I think we burned some good will with the first deprecate and replace and so we should be really sure that these APIs are going to stick.

To that end it might be worth adding the new APIs as underscored and holding off on removing the deprecation warning on the inbound and outbound.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure that I agree with that. Our view on approaching structured concurrency has significantly evolved in the last two years. The initial set of APIs was introduced in June 2023. So we change those APIs after two years. Who will adopt those APIs if they are underscored so that we can get feedback from the new APIs?

Copy link
Contributor

Choose a reason for hiding this comment

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

The point is we introduced the APIs and then changed them.

Before we move to the third iteration of this API we should be really damn sure that the model we land on works and is coherent for both server, client and multiplexing.

It being underscored gives us a chance to try it out (e.g. in gRPC, Vapor) without fully committing to it.

} catch {
// ignore single connection failures
}
}
}
}
return .success(())
Copy link

Choose a reason for hiding this comment

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

Does this implementation need to know about quiescing and/or graceful shutdown?

What's the expected behavior when a server stops accepting clients I.E. ServerBootstrap closes? Do all child tasks cancel, or do we wait for them to shut down? This kinda plays into graceful shutdown from Service Lifecycle. Can a connection continue to live while the parent channel is closed?

} catch {
return .failure(error)
}
}
try result.get()
}
} onCancel: {
channel.channel.close(promise: nil)
}
}

/// Bind the `ServerSocketChannel` to the `host` and `port` parameters.
///
/// - Parameters:
Expand Down Expand Up @@ -622,6 +820,87 @@ extension ServerBootstrap {
to vsockAddress: VsockAddress,
serverBackPressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark? = nil,
childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<Output>
) async throws -> NIOAsyncChannel<Output, Never> {
try await self._bind(
to: vsockAddress,
serverBackPressureStrategy: serverBackPressureStrategy,
childChannelInitializer: childChannelInitializer
)
}

/// Use the existing bound socket file descriptor.
///
/// - Parameters:
/// - socket: The _Unix file descriptor_ representing the bound stream socket.
/// - cleanupExistingSocketFile: Unused.
/// - serverBackPressureStrategy: The back pressure strategy used by the server socket channel.
/// - childChannelInitializer: A closure to initialize the channel. The return value of this closure is returned from the `connect`
/// method.
/// - Returns: The result of the channel initializer.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public func bind<Output: Sendable>(
_ socket: NIOBSDSocket.Handle,
cleanupExistingSocketFile: Bool = false,
serverBackPressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark? = nil,
childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<Output>
) async throws -> NIOAsyncChannel<Output, Never> {
try await self._bind(
socket,
serverBackPressureStrategy: serverBackPressureStrategy,
childChannelInitializer: childChannelInitializer
)
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
private func makeConnectedChannel<Inbound: Sendable, Outbound: Sendable>(
target: BindTarget,
serverBackPressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark? = nil,
childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<NIOAsyncChannel<Inbound, Outbound>>
) async throws -> NIOAsyncChannel<NIOAsyncChannel<Inbound, Outbound>, Never> {
switch target.base {
case .hostAndPort(let host, let port):
try await self.bind(
to: try SocketAddress.makeAddressResolvingHost(host, port: port),
serverBackPressureStrategy: serverBackPressureStrategy,
childChannelInitializer: childChannelInitializer
)

case .unixDomainSocketPath(let unixDomainSocketPath):
try await self.bind(
to: try SocketAddress(unixDomainSocketPath: unixDomainSocketPath),
serverBackPressureStrategy: serverBackPressureStrategy,
childChannelInitializer: childChannelInitializer
)

case .socketAddress(let address):
try await self.bind(
to: address,
serverBackPressureStrategy: serverBackPressureStrategy,
childChannelInitializer: childChannelInitializer
)

case .vsockAddress(let vsockAddress):
try await self._bind(
to: vsockAddress,
serverBackPressureStrategy: serverBackPressureStrategy,
childChannelInitializer: childChannelInitializer
)

case .socket(let handle):
try await self._bind(
handle,
serverBackPressureStrategy: serverBackPressureStrategy,
childChannelInitializer: childChannelInitializer
)
}
}


@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
private func _bind<Output: Sendable>(
to vsockAddress: VsockAddress,
serverBackPressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark? = nil,
childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<Output>
) async throws -> NIOAsyncChannel<Output, Never> {
func makeChannel(
_ eventLoop: SelectableEventLoop,
Expand Down Expand Up @@ -652,19 +931,9 @@ extension ServerBootstrap {
}.get()
}

/// Use the existing bound socket file descriptor.
///
/// - Parameters:
/// - socket: The _Unix file descriptor_ representing the bound stream socket.
/// - cleanupExistingSocketFile: Unused.
/// - serverBackPressureStrategy: The back pressure strategy used by the server socket channel.
/// - childChannelInitializer: A closure to initialize the channel. The return value of this closure is returned from the `connect`
/// method.
/// - Returns: The result of the channel initializer.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public func bind<Output: Sendable>(
private func _bind<Output: Sendable>(
_ socket: NIOBSDSocket.Handle,
cleanupExistingSocketFile: Bool = false,
serverBackPressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark? = nil,
childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<Output>
) async throws -> NIOAsyncChannel<Output, Never> {
Expand Down
Loading
Loading