Files
swift-nio/Sources/NIOCore/ChannelInvoker.swift
Johannes WeissandGitHub 0ee1657e8a Make NIOFileDescriptor/FileRegion/IOData Sendable & soft-deprecated (#2598)
Motivation:

`IOData` is a legacy but alas also core type that needs to be
`Sendable`. Before this PR however it can't be `Sendable` because it
holds a `FileRegion` which holds a `NIOFileDescriptor`. So let's make
all of these `Sendable` but let's also start the deprecation journey for
the following types:

- `IOData`, now soft-deprecated (no warnings) because on its reliance on
`FileRegion`
- `FileRegion`, now soft-deprecated (no warnings) because on its
reliance on `NIOFileHandle`
- `NIOFileHandle`, now soft-deprecated (warnings on the
`NIOFileHandle(descriptor:)` constructor but with a
`NIOFileHandle(_deprecatedTakingOwnershipOfDescriptor:)` alternative
- `NonBlockingFileIO`, now soft-deprecated (warnings on the `openFile`
functions (but with `_deprecated` alternatives) because of their
reliance on `NIOFileHandle)

Modification:

- Make `NIOFileDescriptor`, `FileRegion` and `IOData` `Sendable` by
tracking the fd number and the usage state in an atomic
- Enforce singular access by making the `withFileDescriptor { fd ... }`
function atomically exchange the fd number for a "I'm busy" sentinel
value
- Start deprecating `IOData`, `NIOFileHandle`, `NonBlockingFileIO`,
`FileRegion`

Result:
- `NIOFileDescriptor`, `FileRegion` and `IOData` can be `Sendable`
2024-11-25 16:05:30 +00:00

303 lines
14 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
/// Allows users to invoke an "outbound" operation related to a `Channel` that will flow through the `ChannelPipeline` until
/// it will finally be executed by the the `ChannelCore` implementation.
public protocol ChannelOutboundInvoker {
/// Register on an `EventLoop` and so have all its IO handled.
///
/// - Parameters:
/// - promise: the `EventLoopPromise` that will be notified once the operation completes,
/// or `nil` if not interested in the outcome of the operation.
func register(promise: EventLoopPromise<Void>?)
/// Bind to a `SocketAddress`.
/// - Parameters:
/// - to: the `SocketAddress` to which we should bind the `Channel`.
/// - promise: the `EventLoopPromise` that will be notified once the operation completes,
/// or `nil` if not interested in the outcome of the operation.
func bind(to: SocketAddress, promise: EventLoopPromise<Void>?)
/// Connect to a `SocketAddress`.
/// - Parameters:
/// - to: the `SocketAddress` to which we should connect the `Channel`.
/// - promise: the `EventLoopPromise` that will be notified once the operation completes,
/// or `nil` if not interested in the outcome of the operation.
func connect(to: SocketAddress, promise: EventLoopPromise<Void>?)
/// Write data to the remote peer.
///
/// Be aware that to be sure that data is really written to the remote peer you need to call `flush` or use `writeAndFlush`.
/// Calling `write` multiple times and then `flush` may allow the `Channel` to `write` multiple data objects to the remote peer with one syscall.
///
/// - Parameters:
/// - data: the data to write
/// - promise: the `EventLoopPromise` that will be notified once the operation completes,
/// or `nil` if not interested in the outcome of the operation.
@available(
*,
deprecated,
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
)
func write(_ data: NIOAny, promise: EventLoopPromise<Void>?)
/// Flush data that was previously written via `write` to the remote peer.
func flush()
/// Shortcut for calling `write` and `flush`.
///
/// - Parameters:
/// - data: the data to write
/// - promise: the `EventLoopPromise` that will be notified once the `write` operation completes,
/// or `nil` if not interested in the outcome of the operation.
@available(
*,
deprecated,
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
)
func writeAndFlush(_ data: NIOAny, promise: EventLoopPromise<Void>?)
/// Signal that we want to read from the `Channel` once there is data ready.
///
/// If `ChannelOptions.autoRead` is set for a `Channel` (which is the default) this method is automatically invoked by the transport implementation,
/// otherwise it's the user's responsibility to call this method manually once new data should be read and processed.
///
func read()
/// Close the `Channel` and so the connection if one exists.
///
/// - Parameters:
/// - mode: the `CloseMode` that is used
/// - promise: the `EventLoopPromise` that will be notified once the operation completes,
/// or `nil` if not interested in the outcome of the operation.
func close(mode: CloseMode, promise: EventLoopPromise<Void>?)
/// Trigger a custom user outbound event which will flow through the `ChannelPipeline`.
///
/// - Parameters:
/// - event: The event itself.
/// - promise: the `EventLoopPromise` that will be notified once the operation completes,
/// or `nil` if not interested in the outcome of the operation.
@preconcurrency
func triggerUserOutboundEvent(_ event: Any & Sendable, promise: EventLoopPromise<Void>?)
/// The `EventLoop` which is used by this `ChannelOutboundInvoker` for execution.
var eventLoop: EventLoop { get }
}
/// Extra `ChannelOutboundInvoker` methods. Each method that returns a `EventLoopFuture` will just do the following:
/// - create a new `EventLoopPromise<Void>`
/// - call the corresponding method that takes a `EventLoopPromise<Void>`
/// - return `EventLoopPromise.futureResult`
extension ChannelOutboundInvoker {
/// Register on an `EventLoop` and so have all its IO handled.
/// - Parameters:
/// - file: The file this function was called in, for debugging purposes.
/// - line: The line this function was called on, for debugging purposes.
/// - Returns: the future which will be notified once the operation completes.
public func register(file: StaticString = #fileID, line: UInt = #line) -> EventLoopFuture<Void> {
let promise = makePromise(file: file, line: line)
register(promise: promise)
return promise.futureResult
}
/// Bind to a `SocketAddress`.
/// - Parameters:
/// - address: the `SocketAddress` to which we should bind the `Channel`.
/// - file: The file this function was called in, for debugging purposes.
/// - line: The line this function was called on, for debugging purposes.
/// - Returns: the future which will be notified once the operation completes.
public func bind(
to address: SocketAddress,
file: StaticString = #fileID,
line: UInt = #line
) -> EventLoopFuture<Void> {
let promise = makePromise(file: file, line: line)
bind(to: address, promise: promise)
return promise.futureResult
}
/// Connect to a `SocketAddress`.
/// - Parameters:
/// - address: the `SocketAddress` to which we should connect the `Channel`.
/// - file: The file this function was called in, for debugging purposes.
/// - line: The line this function was called on, for debugging purposes.
/// - Returns: the future which will be notified once the operation completes.
public func connect(
to address: SocketAddress,
file: StaticString = #fileID,
line: UInt = #line
) -> EventLoopFuture<Void> {
let promise = makePromise(file: file, line: line)
connect(to: address, promise: promise)
return promise.futureResult
}
/// Write data to the remote peer.
///
/// Be aware that to be sure that data is really written to the remote peer you need to call `flush` or use `writeAndFlush`.
/// Calling `write` multiple times and then `flush` may allow the `Channel` to `write` multiple data objects to the remote peer with one syscall.
///
/// - Parameters:
/// - data: the data to write
/// - file: The file this function was called in, for debugging purposes.
/// - line: The line this function was called on, for debugging purposes.
/// - Returns: the future which will be notified once the operation completes.
@available(
*,
deprecated,
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
)
public func write(_ data: NIOAny, file: StaticString = #fileID, line: UInt = #line) -> EventLoopFuture<Void> {
let promise = makePromise(file: file, line: line)
write(data, promise: promise)
return promise.futureResult
}
/// Shortcut for calling `write` and `flush`.
///
/// - Parameters:
/// - data: the data to write
/// - file: The file this function was called in, for debugging purposes.
/// - line: The line this function was called on, for debugging purposes.
/// - Returns: the future which will be notified once the `write` operation completes.
@available(
*,
deprecated,
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
)
public func writeAndFlush(_ data: NIOAny, file: StaticString = #fileID, line: UInt = #line) -> EventLoopFuture<Void>
{
let promise = makePromise(file: file, line: line)
writeAndFlush(data, promise: promise)
return promise.futureResult
}
/// Close the `Channel` and so the connection if one exists.
///
/// - Parameters:
/// - mode: the `CloseMode` that is used
/// - file: The file this function was called in, for debugging purposes.
/// - line: The line this function was called on, for debugging purposes.
/// - Returns: the future which will be notified once the operation completes.
public func close(mode: CloseMode = .all, file: StaticString = #fileID, line: UInt = #line) -> EventLoopFuture<Void>
{
let promise = makePromise(file: file, line: line)
self.close(mode: mode, promise: promise)
return promise.futureResult
}
/// Trigger a custom user outbound event which will flow through the `ChannelPipeline`.
///
/// - Parameters:
/// - event: the event itself.
/// - file: The file this function was called in, for debugging purposes.
/// - line: The line this function was called on, for debugging purposes.
/// - Returns: the future which will be notified once the operation completes.
@preconcurrency
public func triggerUserOutboundEvent(
_ event: Any & Sendable,
file: StaticString = #fileID,
line: UInt = #line
) -> EventLoopFuture<Void> {
let promise = makePromise(file: file, line: line)
triggerUserOutboundEvent(event, promise: promise)
return promise.futureResult
}
private func makePromise(file: StaticString = #fileID, line: UInt = #line) -> EventLoopPromise<Void> {
eventLoop.makePromise(file: file, line: line)
}
}
/// Fire inbound events related to a `Channel` through the `ChannelPipeline` until its end is reached or it's consumed by a `ChannelHandler`.
public protocol ChannelInboundInvoker {
/// Called once a `Channel` was registered to its `EventLoop` and so IO will be processed.
func fireChannelRegistered()
/// Called once a `Channel` was unregistered from its `EventLoop` which means no IO will be handled for a `Channel` anymore.
func fireChannelUnregistered()
/// Called once a `Channel` becomes active.
///
/// What active means depends on the `Channel` implementation and semantics.
/// For example for TCP it means the `Channel` is connected to the remote peer.
func fireChannelActive()
/// Called once a `Channel` becomes inactive.
///
/// What inactive means depends on the `Channel` implementation and semantics.
/// For example for TCP it means the `Channel` was disconnected from the remote peer and closed.
func fireChannelInactive()
/// Called once there is some data read for a `Channel` that needs processing.
///
/// - Parameters:
/// - data: the data that was read and is ready to be processed.
@available(
*,
deprecated,
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
)
func fireChannelRead(_ data: NIOAny)
/// Called once there is no more data to read immediately on a `Channel`. Any new data received will be handled later.
func fireChannelReadComplete()
/// Called when a `Channel`'s writable state changes.
///
/// The writability state of a Channel depends on watermarks that can be set via `Channel.setOption` and how much data
/// is still waiting to be transferred to the remote peer.
/// You should take care to enforce some kind of backpressure if the channel becomes unwritable which means `Channel.isWritable`
/// will return `false` to ensure you do not consume too much memory due to queued writes. What exactly you should do here depends on the
/// protocol and other semantics. But for example you may want to stop writing to the `Channel` until `Channel.writable` becomes
/// `true` again or stop reading at all.
func fireChannelWritabilityChanged()
/// Called when an inbound operation `Error` was caught.
///
/// Be aware that for inbound operations this method is called while for outbound operations defined in `ChannelOutboundInvoker`
/// the `EventLoopFuture` or `EventLoopPromise` will be notified.
///
/// - Parameters:
/// - error: the error we encountered.
func fireErrorCaught(_ error: Error)
/// Trigger a custom user inbound event which will flow through the `ChannelPipeline`.
///
/// - Parameters:
/// - event: the event itself.
@preconcurrency
func fireUserInboundEventTriggered(_ event: Any & Sendable)
}
/// A protocol that signals that outbound and inbound events are triggered by this invoker.
public protocol ChannelInvoker: ChannelOutboundInvoker, ChannelInboundInvoker {}
/// Specify what kind of close operation is requested.
public enum CloseMode: Sendable {
/// Close the output (writing) side of the `Channel` without closing the actual file descriptor.
/// This is an optional mode which means it may not be supported by all `Channel` implementations.
case output
/// Close the input (reading) side of the `Channel` without closing the actual file descriptor.
/// This is an optional mode which means it may not be supported by all `Channel` implementations.
case input
/// Close the whole `Channel (file descriptor).
case all
}