From 75c9d25ec99ac616d398795cbaeaae91fdda580c Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Thu, 7 Nov 2024 16:25:59 +0000 Subject: [PATCH 1/9] Add isolated views to EventLoop, Promise, and Future Motivation: Users writing NIO code in a strict concurrency world often need to interact with futures, promises, and event loops. The main interface to these has strict sendability requirements, as it is possible the user is doing so from outside the EventLoop that provides the isolation domain for these types. However, in many cases the user knows that they are on the isolation domain in question. In that case, they need more capabilities. While they can achieve their goals with NIOLoopBound today, it'd be nice if they had a better option. Modifications: - Make EventLoop.Isolated public. - Make EventLoopFuture.Isolated public. - Make EventLoopPromise.Isolated public. - Make all their relevant methods public. - Move the runtime isolation check from the point of use to the point of construction. - Make the types non-Sendable to ensure that isolation check is sufficient. - Add unsafeUnchecked options to create these types when performance matters and correctness is clear. - Add tests for their behaviour. - Update the documentation. Result: Writing safe code with promises, futures, and event loops is easier. --- .../AsyncChannel/AsyncChannelHandler.swift | 2 +- Sources/NIOCore/ChannelHandlers.swift | 24 +- Sources/NIOCore/ChannelPipeline.swift | 12 +- Sources/NIOCore/Codec.swift | 2 +- .../Docs.docc/loops-futures-concurrency.md | 58 ++- .../EventLoopFuture+AssumeIsolated.swift | 205 ++++++---- Sources/NIOEmbedded/Embedded.swift | 10 + .../EventLoopFutureIsolatedTests.swift | 354 ++++++++++++++++++ 8 files changed, 569 insertions(+), 98 deletions(-) create mode 100644 Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift diff --git a/Sources/NIOCore/AsyncChannel/AsyncChannelHandler.swift b/Sources/NIOCore/AsyncChannel/AsyncChannelHandler.swift index 63d243b74e..149c0ff5ea 100644 --- a/Sources/NIOCore/AsyncChannel/AsyncChannelHandler.swift +++ b/Sources/NIOCore/AsyncChannel/AsyncChannelHandler.swift @@ -188,7 +188,7 @@ extension NIOAsyncChannelHandler: ChannelInboundHandler { // We are making sure to be on our event loop so we can safely use self in whenComplete channelReadTransformation(unwrapped) .hop(to: context.eventLoop) - .assumeIsolated() + .assumeIsolatedUnsafeUnchecked() .whenComplete { result in switch result { case .success: diff --git a/Sources/NIOCore/ChannelHandlers.swift b/Sources/NIOCore/ChannelHandlers.swift index 8e0da681ee..358332945b 100644 --- a/Sources/NIOCore/ChannelHandlers.swift +++ b/Sources/NIOCore/ChannelHandlers.swift @@ -114,7 +114,7 @@ public final class AcceptBackoffHandler: ChannelDuplexHandler, RemovableChannelH } private func scheduleRead(at: NIODeadline, context: ChannelHandlerContext) { - self.scheduledRead = context.eventLoop.assumeIsolated().scheduleTask(deadline: at) { + self.scheduledRead = context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask(deadline: at) { self.doRead(context) } } @@ -252,7 +252,9 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl } let writePromise = promise ?? context.eventLoop.makePromise() - writePromise.futureResult.assumeIsolated().whenComplete { (_: Result) in + writePromise.futureResult.hop( + to: context.eventLoop + ).assumeIsolatedUnsafeUnchecked().whenComplete { (_: Result) in self.lastWriteCompleteTime = .now() } context.write(data, promise: writePromise) @@ -272,7 +274,7 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl } if self.reading { - self.scheduledReaderTask = context.eventLoop.assumeIsolated().scheduleTask( + self.scheduledReaderTask = context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask( in: timeout, self.makeReadTimeoutTask(context, timeout) ) @@ -282,7 +284,7 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl let diff = .now() - self.lastReadTime if diff >= timeout { // Reader is idle - set a new timeout and trigger an event through the pipeline - self.scheduledReaderTask = context.eventLoop.assumeIsolated().scheduleTask( + self.scheduledReaderTask = context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask( in: timeout, self.makeReadTimeoutTask(context, timeout) ) @@ -290,7 +292,7 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl context.fireUserInboundEventTriggered(IdleStateEvent.read) } else { // Read occurred before the timeout - set a new timeout with shorter delay. - self.scheduledReaderTask = context.eventLoop.assumeIsolated().scheduleTask( + self.scheduledReaderTask = context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask( deadline: self.lastReadTime + timeout, self.makeReadTimeoutTask(context, timeout) ) @@ -309,7 +311,7 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl if diff >= timeout { // Writer is idle - set a new timeout and notify the callback. - self.scheduledWriterTask = context.eventLoop.assumeIsolated().scheduleTask( + self.scheduledWriterTask = context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask( in: timeout, self.makeWriteTimeoutTask(context, timeout) ) @@ -317,7 +319,7 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl context.fireUserInboundEventTriggered(IdleStateEvent.write) } else { // Write occurred before the timeout - set a new timeout with shorter delay. - self.scheduledWriterTask = context.eventLoop.assumeIsolated().scheduleTask( + self.scheduledWriterTask = context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask( deadline: self.lastWriteCompleteTime + timeout, self.makeWriteTimeoutTask(context, timeout) ) @@ -332,7 +334,7 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl } if self.reading { - self.scheduledReaderTask = context.eventLoop.assumeIsolated().scheduleTask( + self.scheduledReaderTask = context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask( in: timeout, self.makeAllTimeoutTask(context, timeout) ) @@ -345,7 +347,7 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl let diff = .now() - latestLast if diff >= timeout { // Reader is idle - set a new timeout and trigger an event through the pipeline - self.scheduledReaderTask = context.eventLoop.assumeIsolated().scheduleTask( + self.scheduledReaderTask = context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask( in: timeout, self.makeAllTimeoutTask(context, timeout) ) @@ -353,7 +355,7 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl context.fireUserInboundEventTriggered(IdleStateEvent.all) } else { // Read occurred before the timeout - set a new timeout with shorter delay. - self.scheduledReaderTask = context.eventLoop.assumeIsolated().scheduleTask( + self.scheduledReaderTask = context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask( deadline: latestLast + timeout, self.makeAllTimeoutTask(context, timeout) ) @@ -367,7 +369,7 @@ public final class IdleStateHandler: ChannelDuplexHandler, RemovableChannelHandl _ body: @escaping (ChannelHandlerContext, TimeAmount) -> (() -> Void) ) -> Scheduled? { if let timeout = amount { - return context.eventLoop.assumeIsolated().scheduleTask(in: timeout, body(context, timeout)) + return context.eventLoop.assumeIsolatedUnsafeUnchecked().scheduleTask(in: timeout, body(context, timeout)) } return nil } diff --git a/Sources/NIOCore/ChannelPipeline.swift b/Sources/NIOCore/ChannelPipeline.swift index 3db4a88abd..e588904ae9 100644 --- a/Sources/NIOCore/ChannelPipeline.swift +++ b/Sources/NIOCore/ChannelPipeline.swift @@ -472,10 +472,10 @@ public final class ChannelPipeline: ChannelInvoker { let promise = self.eventLoop.makePromise(of: ChannelHandlerContext.self) if self.eventLoop.inEventLoop { - promise.assumeIsolated().completeWith(self.contextSync(handler: handler)) + promise.assumeIsolatedUnsafeUnchecked().completeWith(self.contextSync(handler: handler)) } else { self.eventLoop.execute { - promise.assumeIsolated().completeWith(self.contextSync(handler: handler)) + promise.assumeIsolatedUnsafeUnchecked().completeWith(self.contextSync(handler: handler)) } } @@ -501,10 +501,10 @@ public final class ChannelPipeline: ChannelInvoker { let promise = self.eventLoop.makePromise(of: ChannelHandlerContext.self) if self.eventLoop.inEventLoop { - promise.assumeIsolated().completeWith(self.contextSync(name: name)) + promise.assumeIsolatedUnsafeUnchecked().completeWith(self.contextSync(name: name)) } else { self.eventLoop.execute { - promise.assumeIsolated().completeWith(self.contextSync(name: name)) + promise.assumeIsolatedUnsafeUnchecked().completeWith(self.contextSync(name: name)) } } @@ -534,10 +534,10 @@ public final class ChannelPipeline: ChannelInvoker { let promise = self.eventLoop.makePromise(of: ChannelHandlerContext.self) if self.eventLoop.inEventLoop { - promise.assumeIsolated().completeWith(self._contextSync(handlerType: handlerType)) + promise.assumeIsolatedUnsafeUnchecked().completeWith(self._contextSync(handlerType: handlerType)) } else { self.eventLoop.execute { - promise.assumeIsolated().completeWith(self._contextSync(handlerType: handlerType)) + promise.assumeIsolatedUnsafeUnchecked().completeWith(self._contextSync(handlerType: handlerType)) } } diff --git a/Sources/NIOCore/Codec.swift b/Sources/NIOCore/Codec.swift index acb4fc1394..f38f2eb9e3 100644 --- a/Sources/NIOCore/Codec.swift +++ b/Sources/NIOCore/Codec.swift @@ -749,7 +749,7 @@ extension ByteToMessageHandler: RemovableChannelHandler { public func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) { precondition(self.removalState == .notBeingRemoved) self.removalState = .removalStarted - context.eventLoop.assumeIsolated().execute { + context.eventLoop.assumeIsolatedUnsafeUnchecked().execute { self.processLeftovers(context: context) assert(!self.state.isLeftoversNeedProcessing, "illegal state: \(self.state)") switch self.removalState { diff --git a/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md b/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md index b5cbffa4f1..792afae37e 100644 --- a/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md +++ b/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md @@ -138,10 +138,11 @@ guaranteed to fire on the same isolation domain as the ``ChannelHandlerContext`` of data race is present. However, Swift Concurrency cannot guarantee this at compile time, as the specific isolation domain is determined only at runtime. -In these contexts, today users can make their callbacks safe using ``NIOLoopBound`` and -``NIOLoopBoundBox``. These values can only be constructed on the event loop, and only allow -access to their values on the same event loop. These constraints are enforced at runtime, -so at compile time these are unconditionally `Sendable`. +In these contexts, users that cannot require NIO 2.77.0 can make their callbacks +safe using ``NIOLoopBound`` and ``NIOLoopBoundBox``. These values can only be +constructed on the event loop, and only allow access to their values on the same +event loop. These constraints are enforced at runtime, so at compile time these are +unconditionally `Sendable`. > Warning: ``NIOLoopBound`` and ``NIOLoopBoundBox`` replace compile-time isolation checks with runtime ones. This makes it possible to introduce crashes in your code. Please @@ -150,18 +151,35 @@ so at compile time these are unconditionally `Sendable`. ``EventLoop``, use ``EventLoopFuture/hop(to:)`` to move it to your isolation domain before using these types. -> Note: In a future NIO release we intend to improve the ergonomics of this common problem - by offering a related type that can only be created from an ``EventLoopFuture`` on a - given ``EventLoop``. This minimises the number of runtime checks, and will make it - easier and more pleasant to write this kind of code. +From NIO 2.77.0, new types were introduced to make this common problem easier. These types are +``EventLoopFuture/Isolated`` and ``EventLoopPromise/Isolated``. These isolated view types +can only be constructed from an existing Future or Promise, and they can only be constructed +on the ``EventLoop`` to which those futures or promises are bound. Because they are not +`Sendable`, we can be confident that these values never escape the isolation domain in which +they are created, which must be the same isolation domain where the callbacks execute. + +As a result, these types can drop the `@Sendable` requirements on all the future closures, and +many of the `Sendable` requirements on the return types from future closures. They can also +drop the `Sendable` requirements from the promise completion functions. + +These isolated views can be obtained by calling ``EventLoopFuture/assumeIsolated()`` or +``EventLoopPromise/assumeIsolated()``. + +> Warning: ``EventLoopFuture/assumeIsolated()`` and ``EventLoopPromise/assumeIsolated()`` + supplement compile-time isolation checks with runtime ones. This makes it possible + to introduce crashes in your code. Please ensure that you are 100% confident that the + isolation domains align. If you are not sure that the ``EventLoopFuture`` or + ``EventLoopPromise`` you wish to attach a callback to is bound to your + ``EventLoop``, use ``EventLoopFuture/hop(to:)`` to move it to your isolation domain + before using these types. ## Interacting with Event Loops on the Event Loop As with Futures, there are occasionally times where it is necessary to schedule ``EventLoop`` operations on the ``EventLoop`` where your code is currently executing. -Much like with ``EventLoopFuture``, you can use ``NIOLoopBound`` and ``NIOLoopBoundBox`` -to make these callbacks safe. +Much like with ``EventLoopFuture``, if you need to support NIO versions before 2.77.0 +you can use ``NIOLoopBound`` and ``NIOLoopBoundBox`` to make these callbacks safe. > Warning: ``NIOLoopBound`` and ``NIOLoopBoundBox`` replace compile-time isolation checks with runtime ones. This makes it possible to introduce crashes in your code. Please @@ -170,7 +188,19 @@ to make these callbacks safe. ``EventLoop``, use ``EventLoopFuture/hop(to:)`` to move it to your isolation domain before using these types. -> Note: In a future NIO release we intend to improve the ergonomics of this common problem - by offering a related type that can only be created from an ``EventLoopFuture`` on a - given ``EventLoop``. This minimises the number of runtime checks, and will make it - easier and more pleasant to write this kind of code. +From NIO 2.77.0, a new type was introduced to make this common problem easier. This type is +``EventLoop/Isolated``. This isolated view type can only be constructed from an existing +``EventLoop``, and it can only be constructed on the ``EventLoop`` that is being wrapped. +Because this type is not `Sendable`, we can be confident that this value never escapes the +isolation domain in which it was created, which must be the same isolation domain where the +callbacks execute. + +As a result, this type can drop the `@Sendable` requirements on all the operation closures, and +many of the `Sendable` requirements on the return types from these closures. + +This isolated view can be obtained by calling ``EventLoop/assumeIsolated()``. + +> Warning: ``EventLoop/assumeIsolated()`` supplements compile-time isolation checks with + runtime ones. This makes it possible to introduce crashes in your code. Please ensure + that you are 100% confident that the isolation domains align. If you are not sure that + the your code is running on the relevant ``EventLoop``, prefer the non-isolated type. diff --git a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift index d2dded2cfd..9931bbbabf 100644 --- a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift +++ b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift @@ -14,8 +14,14 @@ /// A struct wrapping an ``EventLoop`` that ensures all calls to any method on this struct /// are coming from the event loop. -@usableFromInline -struct IsolatedEventLoop { +/// +/// This type is explicitly not `Sendable`. It may only be constructed on an event loop, +/// using ``EventLoop/assumeIsolated``, and may not subsequently be passed to other isolation +/// domains. +/// +/// Using this type relaxes the need to have the closures for ``EventLoop/execute(_:)``, +/// ``EventLoop/submit(_:)``, and ``EventLoop/scheduleTask(in:_:)`` to be `@Sendable`. +public struct IsolatedEventLoop { @usableFromInline let _wrapped: EventLoop @@ -26,8 +32,7 @@ struct IsolatedEventLoop { /// Submit a given task to be executed by the `EventLoop` @inlinable - func execute(_ task: @escaping () -> Void) { - self._wrapped.assertInEventLoop() + public func execute(_ task: @escaping () -> Void) { let unsafeTransfer = UnsafeTransfer(task) self._wrapped.execute { unsafeTransfer.wrappedValue() @@ -40,8 +45,7 @@ struct IsolatedEventLoop { /// - task: The closure that will be submitted to the `EventLoop` for execution. /// - Returns: `EventLoopFuture` that is notified once the task was executed. @inlinable - func submit(_ task: @escaping () throws -> T) -> EventLoopFuture { - self._wrapped.assertInEventLoop() + public func submit(_ task: @escaping () throws -> T) -> EventLoopFuture { let unsafeTransfer = UnsafeTransfer(task) return self._wrapped.submit { try unsafeTransfer.wrappedValue() @@ -58,11 +62,10 @@ struct IsolatedEventLoop { /// - Note: You can only cancel a task before it has started executing. @discardableResult @inlinable - func scheduleTask( + public func scheduleTask( deadline: NIODeadline, _ task: @escaping () throws -> T ) -> Scheduled { - self._wrapped.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(task) return self._wrapped.scheduleTask(deadline: deadline) { try unsafeTransfer.wrappedValue() @@ -80,11 +83,10 @@ struct IsolatedEventLoop { /// - Note: The `in` value is clamped to a maximum when running on a Darwin-kernel. @discardableResult @inlinable - func scheduleTask( + public func scheduleTask( in delay: TimeAmount, _ task: @escaping () throws -> T ) -> Scheduled { - self._wrapped.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(task) return self._wrapped.scheduleTask(in: delay) { try unsafeTransfer.wrappedValue() @@ -104,13 +106,12 @@ struct IsolatedEventLoop { /// - Note: You can only cancel a task before it has started executing. @discardableResult @inlinable - func flatScheduleTask( + public func flatScheduleTask( deadline: NIODeadline, file: StaticString = #file, line: UInt = #line, _ task: @escaping () throws -> EventLoopFuture ) -> Scheduled { - self._wrapped.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(task) return self._wrapped.flatScheduleTask(deadline: deadline, file: file, line: line) { try unsafeTransfer.wrappedValue() @@ -119,26 +120,53 @@ struct IsolatedEventLoop { /// Returns the wrapped event loop. @inlinable - func nonisolated() -> any EventLoop { + public func nonisolated() -> any EventLoop { self._wrapped } } + extension EventLoop { /// Assumes the calling context is isolated to the event loop. - @usableFromInline - func assumeIsolated() -> IsolatedEventLoop { - IsolatedEventLoop(self) + @inlinable + public func assumeIsolated() -> IsolatedEventLoop { + self.preconditionInEventLoop() + return IsolatedEventLoop(self) + } + + /// Assumes the calling context is isolated to the event loop. + /// + /// This version of ``EventLoop/assumeIsolated()`` omits the runtime + /// isolation check in release builds. It retains it in debug mode to + /// ensure correctness. + @inlinable + public func assumeIsolatedUnsafeUnchecked() -> IsolatedEventLoop { + self.assertInEventLoop() + return IsolatedEventLoop(self) } } +@available(*, unavailable) +extension IsolatedEventLoop: Sendable {} + extension EventLoopFuture { /// A struct wrapping an ``EventLoopFuture`` that ensures all calls to any method on this struct /// are coming from the event loop of the future. - @usableFromInline - struct Isolated { + /// + /// This type is explicitly not `Sendable`. It may only be constructed on an event loop, + /// using ``EventLoopFuture/assumeIsolated``, and may not subsequently be passed to other isolation + /// domains. + /// + /// Using this type relaxes the need to have the closures for the various ``EventLoopFuture`` + /// callback-attaching functions be `Sendable`. + public struct Isolated { @usableFromInline let _wrapped: EventLoopFuture + @inlinable + init(_wrapped: EventLoopFuture) { + self._wrapped = _wrapped + } + /// When the current `EventLoopFuture` is fulfilled, run the provided callback, /// which will provide a new `EventLoopFuture`. /// @@ -160,6 +188,9 @@ extension EventLoopFuture { /// } /// ``` /// + /// Note that the returned ``EventLoopFuture`` still needs a `Sendable` wrapped value, + /// as it may have been created on a different event loop. + /// /// Note: In a sense, the `EventLoopFuture` is returned before it's created. /// /// - Parameters: @@ -167,14 +198,13 @@ extension EventLoopFuture { /// a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value. @inlinable - func flatMap( + public func flatMap( _ callback: @escaping (Value) -> EventLoopFuture ) -> EventLoopFuture.Isolated { - self._wrapped.eventLoop.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.flatMap { unsafeTransfer.wrappedValue($0) - }.assumeIsolated() + }.assumeIsolatedUnsafeUnchecked() } /// When the current `EventLoopFuture` is fulfilled, run the provided callback, which @@ -192,14 +222,13 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value. @inlinable - func flatMapThrowing( + public func flatMapThrowing( _ callback: @escaping (Value) throws -> NewValue ) -> EventLoopFuture.Isolated { - self._wrapped.eventLoop.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.flatMapThrowing { try unsafeTransfer.wrappedValue($0) - }.assumeIsolated() + }.assumeIsolatedUnsafeUnchecked() } /// When the current `EventLoopFuture` is in an error state, run the provided callback, which @@ -217,14 +246,13 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value or a rethrown error. @inlinable - func flatMapErrorThrowing( + public func flatMapErrorThrowing( _ callback: @escaping (Error) throws -> Value ) -> EventLoopFuture.Isolated { - self._wrapped.eventLoop.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.flatMapErrorThrowing { try unsafeTransfer.wrappedValue($0) - }.assumeIsolated() + }.assumeIsolatedUnsafeUnchecked() } /// When the current `EventLoopFuture` is fulfilled, run the provided callback, which @@ -254,14 +282,13 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value. @inlinable - func map( + public func map( _ callback: @escaping (Value) -> (NewValue) ) -> EventLoopFuture.Isolated { - self._wrapped.eventLoop.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.map { unsafeTransfer.wrappedValue($0) - }.assumeIsolated() + }.assumeIsolatedUnsafeUnchecked() } /// When the current `EventLoopFuture` is in an error state, run the provided callback, which @@ -279,14 +306,13 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the recovered value. @inlinable - func flatMapError( + public func flatMapError( _ callback: @escaping (Error) -> EventLoopFuture ) -> EventLoopFuture.Isolated where Value: Sendable { - self._wrapped.eventLoop.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.flatMapError { unsafeTransfer.wrappedValue($0) - }.assumeIsolated() + }.assumeIsolatedUnsafeUnchecked() } /// When the current `EventLoopFuture` is fulfilled, run the provided callback, which @@ -303,14 +329,13 @@ extension EventLoopFuture { /// a new value or error lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value. @inlinable - func flatMapResult( + public func flatMapResult( _ body: @escaping (Value) -> Result ) -> EventLoopFuture.Isolated { - self._wrapped.eventLoop.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(body) return self._wrapped.flatMapResult { unsafeTransfer.wrappedValue($0) - }.assumeIsolated() + }.assumeIsolatedUnsafeUnchecked() } /// When the current `EventLoopFuture` is in an error state, run the provided callback, which @@ -326,14 +351,13 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the recovered value. @inlinable - func recover( + public func recover( _ callback: @escaping (Error) -> Value ) -> EventLoopFuture.Isolated { - self._wrapped.eventLoop.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.recover { unsafeTransfer.wrappedValue($0) - }.assumeIsolated() + }.assumeIsolatedUnsafeUnchecked() } /// Adds an observer callback to this `EventLoopFuture` that is called when the @@ -347,8 +371,7 @@ extension EventLoopFuture { /// - Parameters: /// - callback: The callback that is called with the successful result of the `EventLoopFuture`. @inlinable - func whenSuccess(_ callback: @escaping (Value) -> Void) { - self._wrapped.eventLoop.assertInEventLoop() + public func whenSuccess(_ callback: @escaping (Value) -> Void) { let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.whenSuccess { unsafeTransfer.wrappedValue($0) @@ -366,8 +389,7 @@ extension EventLoopFuture { /// - Parameters: /// - callback: The callback that is called with the failed result of the `EventLoopFuture`. @inlinable - func whenFailure(_ callback: @escaping (Error) -> Void) { - self._wrapped.eventLoop.assertInEventLoop() + public func whenFailure(_ callback: @escaping (Error) -> Void) { let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.whenFailure { unsafeTransfer.wrappedValue($0) @@ -380,10 +402,9 @@ extension EventLoopFuture { /// - Parameters: /// - callback: The callback that is called when the `EventLoopFuture` is fulfilled. @inlinable - func whenComplete( + public func whenComplete( _ callback: @escaping (Result) -> Void ) { - self._wrapped.eventLoop.assertInEventLoop() let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.whenComplete { unsafeTransfer.wrappedValue($0) @@ -397,14 +418,13 @@ extension EventLoopFuture { /// - callback: the callback that is called when the `EventLoopFuture` is fulfilled. /// - Returns: the current `EventLoopFuture` @inlinable - func always( + public func always( _ callback: @escaping (Result) -> Void - ) -> EventLoopFuture { - self._wrapped.eventLoop.assertInEventLoop() + ) -> EventLoopFuture.Isolated { let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.always { unsafeTransfer.wrappedValue($0) - } + }.assumeIsolatedUnsafeUnchecked() } /// Unwrap an `EventLoopFuture` where its type parameter is an `Optional`. @@ -419,7 +439,7 @@ extension EventLoopFuture { /// - orReplace: the value of the returned `EventLoopFuture` when then resolved future's value is `Optional.some()`. /// - Returns: an new `EventLoopFuture` with new type parameter `NewValue` and the value passed in the `orReplace` parameter. @inlinable - func unwrap( + public func unwrap( orReplace replacement: NewValue ) -> EventLoopFuture.Isolated where Value == NewValue? { self.map { (value) -> NewValue in @@ -445,7 +465,7 @@ extension EventLoopFuture { /// - Returns: an new `EventLoopFuture` with new type parameter `NewValue` and with the value returned by the closure /// passed in the `orElse` parameter. @inlinable - func unwrap( + public func unwrap( orElse callback: @escaping () -> NewValue ) -> EventLoopFuture.Isolated where Value == NewValue? { self.map { (value) -> NewValue in @@ -458,34 +478,70 @@ extension EventLoopFuture { /// Returns the wrapped event loop future. @inlinable - func nonisolated() -> EventLoopFuture { + public func nonisolated() -> EventLoopFuture { self._wrapped } } - /// Assumes the calling context is isolated to the future's event loop. - @usableFromInline - func assumeIsolated() -> Isolated { + /// Returns a variant of this ``EventLoopFuture`` with less strict + /// `Sendable` requirements. Can only be called from on the + /// ``EventLoop`` to which this ``EventLoopFuture`` is bound, will crash + /// if that invariant fails to be met. + @inlinable + public func assumeIsolated() -> Isolated { + self.eventLoop.preconditionInEventLoop() + return Isolated(_wrapped: self) + } + + /// Returns a variant of this ``EventLoopFuture`` with less strict + /// `Sendable` requirements. Can only be called from on the + /// ``EventLoop`` to which this ``EventLoopFuture`` is bound, will crash + /// if that invariant fails to be met. + /// + /// This is an unsafe version of ``EventLoopFuture/assumeIsolated()`` which + /// omits the runtime check in release builds. This improves performance, but + /// should only be used sparingly. + @inlinable + public func assumeIsolatedUnsafeUnchecked() -> Isolated { self.eventLoop.assertInEventLoop() return Isolated(_wrapped: self) } } +@available(*, unavailable) +extension EventLoopFuture.Isolated: Sendable {} + extension EventLoopPromise { /// A struct wrapping an ``EventLoopPromise`` that ensures all calls to any method on this struct /// are coming from the event loop of the promise. - @usableFromInline - struct Isolated { + /// + /// This type is explicitly not `Sendable`. It may only be constructed on an event loop, + /// using ``EventLoopPromise/assumeIsolated``, and may not subsequently be passed to other isolation + /// domains. + /// + /// Using this type relaxes the need to have the promise completion functions accept ``Sendable`` + /// values, as this type can only be handled on the ``EventLoop``. + /// + /// This type does not offer the full suite of completion functions that ``EventLoopPromise`` + /// does, as many of those functions do not require `Sendable` values already. It only offers + /// versions for the functions that do require `Sendable` types. If you have an + /// ``EventLoopPromise/Isolated`` but need a regular ``EventLoopPromise``, use + /// ``EventLoopPromise/Isolated/nonisolated()`` to unwrap the value. + public struct Isolated { @usableFromInline let _wrapped: EventLoopPromise + @inlinable + init(_wrapped: EventLoopPromise) { + self._wrapped = _wrapped + } + /// Deliver a successful result to the associated `EventLoopFuture` object. /// /// - Parameters: /// - value: The successful result of the operation. @inlinable - func succeed(_ value: Value) { - self._wrapped.futureResult.eventLoop.assertInEventLoop() + public func succeed(_ value: Value) { self._wrapped._setValue(value: .success(value))._run() } @@ -504,22 +560,41 @@ extension EventLoopPromise { /// - Parameters: /// - result: The result which will be used to succeed or fail this promise. @inlinable - func completeWith(_ result: Result) { - self._wrapped.futureResult.eventLoop.assertInEventLoop() + public func completeWith(_ result: Result) { self._wrapped._setValue(value: result)._run() } /// Returns the wrapped event loop promise. @inlinable - func nonisolated() -> EventLoopPromise { + public func nonisolated() -> EventLoopPromise { self._wrapped } } - /// Assumes the calling context is isolated to the promise's event loop. - @usableFromInline - func assumeIsolated() -> Isolated { + /// Returns a variant of this ``EventLoopPromise`` with less strict + /// `Sendable` requirements. Can only be called from on the + /// ``EventLoop`` to which this ``EventLoopPromise`` is bound, will crash + /// if that invariant fails to be met. + @inlinable + public func assumeIsolated() -> Isolated { + self.futureResult.eventLoop.preconditionInEventLoop() + return Isolated(_wrapped: self) + } + + /// Returns a variant of this ``EventLoopPromise`` with less strict + /// `Sendable` requirements. Can only be called from on the + /// ``EventLoop`` to which this ``EventLoopPromise`` is bound, will crash + /// if that invariant fails to be met. + /// + /// This is an unsafe version of ``EventLoopPromise/assumeIsolated()`` which + /// omits the runtime check in release builds. This improves performance, but + /// should only be used sparingly. + @inlinable + public func assumeIsolatedUnsafeUnchecked() -> Isolated { self.futureResult.eventLoop.assertInEventLoop() return Isolated(_wrapped: self) } } + +@available(*, unavailable) +extension EventLoopPromise.Isolated: Sendable {} diff --git a/Sources/NIOEmbedded/Embedded.swift b/Sources/NIOEmbedded/Embedded.swift index c509a8ffa3..cf05751b1f 100644 --- a/Sources/NIOEmbedded/Embedded.swift +++ b/Sources/NIOEmbedded/Embedded.swift @@ -303,6 +303,16 @@ public final class EmbeddedEventLoop: EventLoop, CustomStringConvertible { } #endif + public func preconditionInEventLoop(file: StaticString, line: UInt) { + self.checkCorrectThread() + // Currently, inEventLoop is always true so this always passes. + } + + public func preconditionNotInEventLoop(file: StaticString, line: UInt) { + // As inEventLoop always returns true, this must always preconditon. + preconditionFailure("Always in EmbeddedEventLoop", file: file, line: line) + } + public func _preconditionSafeToWait(file: StaticString, line: UInt) { self.checkCorrectThread() // EmbeddedEventLoop always allows a wait, as waiting will essentially always block diff --git a/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift b/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift new file mode 100644 index 0000000000..840314befd --- /dev/null +++ b/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift @@ -0,0 +1,354 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOPosix +import XCTest + +final class SuperNotSendable { + var x: Int = 5 +} + +@available(*, unavailable) +extension SuperNotSendable: Sendable {} + +final class EventLoopFutureIsolatedTest: XCTestCase { + func testCompletingPromiseWithNonSendableValue() throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let loop = group.next() + + try loop.flatSubmit { + let promise = loop.makePromise(of: SuperNotSendable.self) + let value = SuperNotSendable() + promise.assumeIsolated().succeed(value) + return promise.futureResult.assumeIsolated().map { val in + XCTAssertTrue(val === value) + }.nonisolated() + }.wait() + } + + func testCompletingPromiseWithNonSendableResult() throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let loop = group.next() + + try loop.flatSubmit { + let promise = loop.makePromise(of: SuperNotSendable.self) + let value = SuperNotSendable() + promise.assumeIsolated().completeWith(.success(value)) + return promise.futureResult.assumeIsolated().map { val in + XCTAssertTrue(val === value) + }.nonisolated() + }.wait() + } + + func testCompletingPromiseWithNonSendableValueUnchecked() throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let loop = group.next() + + try loop.flatSubmit { + let promise = loop.makePromise(of: SuperNotSendable.self) + let value = SuperNotSendable() + promise.assumeIsolatedUnsafeUnchecked().succeed(value) + return promise.futureResult.assumeIsolatedUnsafeUnchecked().map { val in + XCTAssertTrue(val === value) + }.nonisolated() + }.wait() + } + + func testCompletingPromiseWithNonSendableResultUnchecked() throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let loop = group.next() + + try loop.flatSubmit { + let promise = loop.makePromise(of: SuperNotSendable.self) + let value = SuperNotSendable() + promise.assumeIsolatedUnsafeUnchecked().completeWith(.success(value)) + return promise.futureResult.assumeIsolatedUnsafeUnchecked().map { val in + XCTAssertTrue(val === value) + }.nonisolated() + }.wait() + } + + func testBackAndForthUnwrapping() throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let loop = group.next() + + try loop.submit { + let promise = loop.makePromise(of: SuperNotSendable.self) + let future = promise.futureResult + + XCTAssertEqual(promise.assumeIsolated().nonisolated(), promise) + XCTAssertEqual(future.assumeIsolated().nonisolated(), future) + promise.assumeIsolated().succeed(SuperNotSendable()) + }.wait() + } + + func testBackAndForthUnwrappingUnchecked() throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let loop = group.next() + + try loop.submit { + let promise = loop.makePromise(of: SuperNotSendable.self) + let future = promise.futureResult + + XCTAssertEqual(promise.assumeIsolatedUnsafeUnchecked().nonisolated(), promise) + XCTAssertEqual(future.assumeIsolatedUnsafeUnchecked().nonisolated(), future) + promise.assumeIsolated().succeed(SuperNotSendable()) + }.wait() + } + + func testFutureChaining() throws { + enum TestError: Error { + case error + } + + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let loop = group.next() + try loop.flatSubmit { + let promise = loop.makePromise(of: SuperNotSendable.self) + let future = promise.futureResult.assumeIsolated() + let originalValue = SuperNotSendable() + + // Note that for this test it is _very important_ that all of these + // close over `originalValue`. This proves the non-Sendability of + // the closure. + + // This block is the main happy path. + let newFuture = future.flatMap { result in + XCTAssertTrue(originalValue === result) + let promise = loop.makePromise(of: Int.self) + promise.succeed(4) + return promise.futureResult + }.map { (result: Int) in + XCTAssertEqual(result, 4) + return originalValue + }.flatMapThrowing { (result: SuperNotSendable) in + XCTAssertTrue(originalValue === result) + return SuperNotSendable() + }.flatMapResult { (result: SuperNotSendable) -> Result in + XCTAssertFalse(originalValue === result) + return .failure(TestError.error) + }.recover { err in + XCTAssertTrue(err is TestError) + return originalValue + }.always { val in + XCTAssertNotNil(try? val.get()) + } + + newFuture.whenComplete { result in + guard case .success(let r) = result else { + XCTFail("Unexpected error") + return + } + XCTAssertTrue(r === originalValue) + } + newFuture.whenSuccess { result in + XCTAssertTrue(result === originalValue) + } + + // This block covers the flatMapError and whenFailure tests + let throwingFuture = newFuture.flatMapThrowing { (_: SuperNotSendable) throws -> SuperNotSendable in + XCTAssertEqual(originalValue.x, 5) + throw TestError.error + } + throwingFuture.whenFailure { error in + // Supurious but forces the closure. + XCTAssertEqual(originalValue.x, 5) + guard let error = error as? TestError, error == .error else { + XCTFail("Invalid passed error: \(error)") + return + } + } + throwingFuture.flatMapErrorThrowing { error in + guard let error = error as? TestError, error == .error else { + XCTFail("Invalid passed error: \(error)") + throw error + } + return originalValue + }.whenComplete { result in + guard case .success(let r) = result else { + XCTFail("Unexpected error") + return + } + XCTAssertTrue(r === originalValue) + } + throwingFuture.map { _ in 5 }.flatMapError { (error: any Error) -> EventLoopFuture in + guard let error = error as? TestError, error == .error else { + XCTFail("Invalid passed error: \(error)") + return loop.makeSucceededFuture(originalValue.x) + } + return loop.makeSucceededFuture(originalValue.x - 1) + }.whenComplete { (result: Result) in + guard case .success(let r) = result else { + XCTFail("Unexpected error") + return + } + XCTAssertEqual(r, originalValue.x - 1) + } + + // This block handles unwrap. + newFuture.map { x -> SuperNotSendable? in + XCTAssertEqual(originalValue.x, 5) + return nil + }.unwrap(orReplace: originalValue).unwrap( + orReplace: SuperNotSendable() + ).map { x -> SuperNotSendable? in + XCTAssertTrue(x === originalValue) + return nil + }.unwrap(orElse: { + originalValue + }).unwrap(orElse: { + SuperNotSendable() + }).whenSuccess { x in + XCTAssertTrue(x === originalValue) + } + + promise.assumeIsolated().succeed(originalValue) + return newFuture.map { _ in }.nonisolated() + }.wait() + } + + func testEventLoopIsolated() throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let loop = group.next() + + let result: Int = try loop.flatSubmit { + let value = SuperNotSendable() + + // Again, all of these need to close over value. In addition, + // many need to return it as well. + let isolated = loop.assumeIsolated() + XCTAssertTrue(isolated.nonisolated() === loop) + isolated.execute { + XCTAssertEqual(value.x, 5) + } + let firstFuture = isolated.submit { + let val = SuperNotSendable() + val.x = value.x + 1 + return val + }.map { $0.x } + + let secondFuture = isolated.scheduleTask(deadline: .now() + .milliseconds(50)) { + let val = SuperNotSendable() + val.x = value.x + 1 + return val + }.futureResult.map { $0.x } + + let thirdFuture = isolated.scheduleTask(in: .milliseconds(50)) { + let val = SuperNotSendable() + val.x = value.x + 1 + return val + }.futureResult.map { $0.x } + + let fourthFuture = isolated.flatScheduleTask(deadline: .now() + .milliseconds(50)) { + let promise = loop.makePromise(of: Int.self) + promise.succeed(value.x + 1) + return promise.futureResult + }.futureResult.map { $0 } + + return EventLoopFuture.reduce( + into: 0, + [firstFuture, secondFuture, thirdFuture, fourthFuture], + on: loop + ) { $0 += $1 } + }.wait() + + XCTAssertEqual(result, 6 * 4) + } + + func testEventLoopIsolatedUnchecked() throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let loop = group.next() + + let result: Int = try loop.flatSubmit { + let value = SuperNotSendable() + + // Again, all of these need to close over value. In addition, + // many need to return it as well. + let isolated = loop.assumeIsolatedUnsafeUnchecked() + XCTAssertTrue(isolated.nonisolated() === loop) + isolated.execute { + XCTAssertEqual(value.x, 5) + } + let firstFuture = isolated.submit { + let val = SuperNotSendable() + val.x = value.x + 1 + return val + }.map { $0.x } + + let secondFuture = isolated.scheduleTask(deadline: .now() + .milliseconds(50)) { + let val = SuperNotSendable() + val.x = value.x + 1 + return val + }.futureResult.map { $0.x } + + let thirdFuture = isolated.scheduleTask(in: .milliseconds(50)) { + let val = SuperNotSendable() + val.x = value.x + 1 + return val + }.futureResult.map { $0.x } + + let fourthFuture = isolated.flatScheduleTask(deadline: .now() + .milliseconds(50)) { + let promise = loop.makePromise(of: Int.self) + promise.succeed(value.x + 1) + return promise.futureResult + }.futureResult.map { $0 } + + return EventLoopFuture.reduce( + into: 0, + [firstFuture, secondFuture, thirdFuture, fourthFuture], + on: loop + ) { $0 += $1 } + }.wait() + + XCTAssertEqual(result, 6 * 4) + } +} + From 66090d5292190df6bda9ebe2af5f79db4121fb91 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 12 Nov 2024 15:01:31 +0000 Subject: [PATCH 2/9] Namespace --- Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift index 9931bbbabf..1542014376 100644 --- a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift +++ b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift @@ -21,7 +21,7 @@ /// /// Using this type relaxes the need to have the closures for ``EventLoop/execute(_:)``, /// ``EventLoop/submit(_:)``, and ``EventLoop/scheduleTask(in:_:)`` to be `@Sendable`. -public struct IsolatedEventLoop { +public struct NIOIsolatedEventLoop { @usableFromInline let _wrapped: EventLoop @@ -128,9 +128,9 @@ public struct IsolatedEventLoop { extension EventLoop { /// Assumes the calling context is isolated to the event loop. @inlinable - public func assumeIsolated() -> IsolatedEventLoop { + public func assumeIsolated() -> NIOIsolatedEventLoop { self.preconditionInEventLoop() - return IsolatedEventLoop(self) + return NIOIsolatedEventLoop(self) } /// Assumes the calling context is isolated to the event loop. @@ -139,14 +139,14 @@ extension EventLoop { /// isolation check in release builds. It retains it in debug mode to /// ensure correctness. @inlinable - public func assumeIsolatedUnsafeUnchecked() -> IsolatedEventLoop { + public func assumeIsolatedUnsafeUnchecked() -> NIOIsolatedEventLoop { self.assertInEventLoop() - return IsolatedEventLoop(self) + return NIOIsolatedEventLoop(self) } } @available(*, unavailable) -extension IsolatedEventLoop: Sendable {} +extension NIOIsolatedEventLoop: Sendable {} extension EventLoopFuture { /// A struct wrapping an ``EventLoopFuture`` that ensures all calls to any method on this struct From 2d2c65e257b97722569c4104f1cad94c4a600528 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 12 Nov 2024 15:02:54 +0000 Subject: [PATCH 3/9] Singletons --- .../EventLoopFutureIsolatedTests.swift | 55 ++++--------------- 1 file changed, 10 insertions(+), 45 deletions(-) diff --git a/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift b/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift index 840314befd..d0c4d7c73e 100644 --- a/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift +++ b/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift @@ -25,11 +25,7 @@ extension SuperNotSendable: Sendable {} final class EventLoopFutureIsolatedTest: XCTestCase { func testCompletingPromiseWithNonSendableValue() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - + let group = MultiThreadedEventLoopGroup.singleton let loop = group.next() try loop.flatSubmit { @@ -43,11 +39,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { } func testCompletingPromiseWithNonSendableResult() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - + let group = MultiThreadedEventLoopGroup.singleton let loop = group.next() try loop.flatSubmit { @@ -61,11 +53,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { } func testCompletingPromiseWithNonSendableValueUnchecked() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - + let group = MultiThreadedEventLoopGroup.singleton let loop = group.next() try loop.flatSubmit { @@ -79,11 +67,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { } func testCompletingPromiseWithNonSendableResultUnchecked() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - + let group = MultiThreadedEventLoopGroup.singleton let loop = group.next() try loop.flatSubmit { @@ -97,11 +81,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { } func testBackAndForthUnwrapping() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - + let group = MultiThreadedEventLoopGroup.singleton let loop = group.next() try loop.submit { @@ -115,11 +95,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { } func testBackAndForthUnwrappingUnchecked() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - + let group = MultiThreadedEventLoopGroup.singleton let loop = group.next() try loop.submit { @@ -137,12 +113,9 @@ final class EventLoopFutureIsolatedTest: XCTestCase { case error } - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - + let group = MultiThreadedEventLoopGroup.singleton let loop = group.next() + try loop.flatSubmit { let promise = loop.makePromise(of: SuperNotSendable.self) let future = promise.futureResult.assumeIsolated() @@ -248,11 +221,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { } func testEventLoopIsolated() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - + let group = MultiThreadedEventLoopGroup.singleton let loop = group.next() let result: Int = try loop.flatSubmit { @@ -300,11 +269,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { } func testEventLoopIsolatedUnchecked() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - + let group = MultiThreadedEventLoopGroup.singleton let loop = group.next() let result: Int = try loop.flatSubmit { From fa0672d8d0e6278b2154989deff4043eb73728e3 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 12 Nov 2024 15:08:27 +0000 Subject: [PATCH 4/9] XCTAssertIdentical --- .../EventLoopFutureIsolatedTests.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift b/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift index d0c4d7c73e..6bc41e72b9 100644 --- a/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift +++ b/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift @@ -33,7 +33,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { let value = SuperNotSendable() promise.assumeIsolated().succeed(value) return promise.futureResult.assumeIsolated().map { val in - XCTAssertTrue(val === value) + XCTAssertIdentical(val, value) }.nonisolated() }.wait() } @@ -47,7 +47,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { let value = SuperNotSendable() promise.assumeIsolated().completeWith(.success(value)) return promise.futureResult.assumeIsolated().map { val in - XCTAssertTrue(val === value) + XCTAssertIdentical(val, value) }.nonisolated() }.wait() } @@ -61,7 +61,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { let value = SuperNotSendable() promise.assumeIsolatedUnsafeUnchecked().succeed(value) return promise.futureResult.assumeIsolatedUnsafeUnchecked().map { val in - XCTAssertTrue(val === value) + XCTAssertIdentical(val, value) }.nonisolated() }.wait() } @@ -75,7 +75,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { let value = SuperNotSendable() promise.assumeIsolatedUnsafeUnchecked().completeWith(.success(value)) return promise.futureResult.assumeIsolatedUnsafeUnchecked().map { val in - XCTAssertTrue(val === value) + XCTAssertIdentical(val, value) }.nonisolated() }.wait() } @@ -127,7 +127,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { // This block is the main happy path. let newFuture = future.flatMap { result in - XCTAssertTrue(originalValue === result) + XCTAssertIdentical(originalValue, result) let promise = loop.makePromise(of: Int.self) promise.succeed(4) return promise.futureResult @@ -135,10 +135,10 @@ final class EventLoopFutureIsolatedTest: XCTestCase { XCTAssertEqual(result, 4) return originalValue }.flatMapThrowing { (result: SuperNotSendable) in - XCTAssertTrue(originalValue === result) + XCTAssertIdentical(originalValue, result) return SuperNotSendable() }.flatMapResult { (result: SuperNotSendable) -> Result in - XCTAssertFalse(originalValue === result) + XCTAssertNotIdentical(originalValue, result) return .failure(TestError.error) }.recover { err in XCTAssertTrue(err is TestError) @@ -152,10 +152,10 @@ final class EventLoopFutureIsolatedTest: XCTestCase { XCTFail("Unexpected error") return } - XCTAssertTrue(r === originalValue) + XCTAssertIdentical(r, originalValue) } newFuture.whenSuccess { result in - XCTAssertTrue(result === originalValue) + XCTAssertIdentical(result, originalValue) } // This block covers the flatMapError and whenFailure tests @@ -182,7 +182,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { XCTFail("Unexpected error") return } - XCTAssertTrue(r === originalValue) + XCTAssertIdentical(r, originalValue) } throwingFuture.map { _ in 5 }.flatMapError { (error: any Error) -> EventLoopFuture in guard let error = error as? TestError, error == .error else { @@ -205,14 +205,14 @@ final class EventLoopFutureIsolatedTest: XCTestCase { }.unwrap(orReplace: originalValue).unwrap( orReplace: SuperNotSendable() ).map { x -> SuperNotSendable? in - XCTAssertTrue(x === originalValue) + XCTAssertIdentical(x, originalValue) return nil }.unwrap(orElse: { originalValue }).unwrap(orElse: { SuperNotSendable() }).whenSuccess { x in - XCTAssertTrue(x === originalValue) + XCTAssertIdentical(x, originalValue) } promise.assumeIsolated().succeed(originalValue) @@ -230,7 +230,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { // Again, all of these need to close over value. In addition, // many need to return it as well. let isolated = loop.assumeIsolated() - XCTAssertTrue(isolated.nonisolated() === loop) + XCTAssertIdentical(isolated.nonisolated(), loop) isolated.execute { XCTAssertEqual(value.x, 5) } @@ -278,7 +278,7 @@ final class EventLoopFutureIsolatedTest: XCTestCase { // Again, all of these need to close over value. In addition, // many need to return it as well. let isolated = loop.assumeIsolatedUnsafeUnchecked() - XCTAssertTrue(isolated.nonisolated() === loop) + XCTAssertIdentical(isolated.nonisolated(), loop) isolated.execute { XCTAssertEqual(value.x, 5) } From e0fd516eb15ff0591898268310fd05e61a705fee Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 12 Nov 2024 15:48:13 +0000 Subject: [PATCH 5/9] Format fix --- Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift b/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift index 6bc41e72b9..2388c28236 100644 --- a/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift +++ b/Tests/NIOPosixTests/EventLoopFutureIsolatedTests.swift @@ -316,4 +316,3 @@ final class EventLoopFutureIsolatedTest: XCTestCase { XCTAssertEqual(result, 6 * 4) } } - From 28a02b9e8937d85365ca02265c099e5cad331c5a Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 12 Nov 2024 15:55:01 +0000 Subject: [PATCH 6/9] Clean up documentation --- .../Docs.docc/loops-futures-concurrency.md | 2 +- .../NIOCore/EventLoopFuture+AssumeIsolated.swift | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md b/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md index 792afae37e..c2bf4304d6 100644 --- a/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md +++ b/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md @@ -189,7 +189,7 @@ you can use ``NIOLoopBound`` and ``NIOLoopBoundBox`` to make these callbacks saf before using these types. From NIO 2.77.0, a new type was introduced to make this common problem easier. This type is -``EventLoop/Isolated``. This isolated view type can only be constructed from an existing +``NIOIsolatedEventLoop``. This isolated view type can only be constructed from an existing ``EventLoop``, and it can only be constructed on the ``EventLoop`` that is being wrapped. Because this type is not `Sendable`, we can be confident that this value never escapes the isolation domain in which it was created, which must be the same isolation domain where the diff --git a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift index 1542014376..a34638b6d8 100644 --- a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift +++ b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift @@ -16,7 +16,7 @@ /// are coming from the event loop. /// /// This type is explicitly not `Sendable`. It may only be constructed on an event loop, -/// using ``EventLoop/assumeIsolated``, and may not subsequently be passed to other isolation +/// using ``EventLoop/assumeIsolated()``, and may not subsequently be passed to other isolation /// domains. /// /// Using this type relaxes the need to have the closures for ``EventLoop/execute(_:)``, @@ -75,6 +75,7 @@ public struct NIOIsolatedEventLoop { /// Schedule a `task` that is executed by this `EventLoop` after the given amount of time. /// /// - Parameters: + /// - delay: The time to wait before running the task. /// - task: The synchronous task to run. As with everything that runs on the `EventLoop`, it must not block. /// - Returns: A `Scheduled` object which may be used to cancel the task if it has not yet run, or to wait /// on the completion of the task. @@ -99,6 +100,9 @@ public struct NIOIsolatedEventLoop { /// this event loop might differ. /// /// - Parameters: + /// - deadline: The time at which we should run the asynchronous task. + /// - file: The file in which the task is scheduled. + /// - line: The line of the `file` in which the task is scheduled. /// - task: The asynchronous task to run. As with everything that runs on the `EventLoop`, it must not block. /// - Returns: A `Scheduled` object which may be used to cancel the task if it has not yet run, or to wait /// on the full execution of the task, including its returned `EventLoopFuture`. @@ -153,7 +157,7 @@ extension EventLoopFuture { /// are coming from the event loop of the future. /// /// This type is explicitly not `Sendable`. It may only be constructed on an event loop, - /// using ``EventLoopFuture/assumeIsolated``, and may not subsequently be passed to other isolation + /// using ``EventLoopFuture/assumeIsolated()``, and may not subsequently be passed to other isolation /// domains. /// /// Using this type relaxes the need to have the closures for the various ``EventLoopFuture`` @@ -436,7 +440,7 @@ extension EventLoopFuture { /// ``` /// /// - Parameters: - /// - orReplace: the value of the returned `EventLoopFuture` when then resolved future's value is `Optional.some()`. + /// - replacement: the value of the returned `EventLoopFuture` when then resolved future's value is `Optional.some()`. /// - Returns: an new `EventLoopFuture` with new type parameter `NewValue` and the value passed in the `orReplace` parameter. @inlinable public func unwrap( @@ -460,7 +464,7 @@ extension EventLoopFuture { /// ``` /// /// - Parameters: - /// - orElse: a closure that returns the value of the returned `EventLoopFuture` when then resolved future's value + /// - callback: a closure that returns the value of the returned `EventLoopFuture` when then resolved future's value /// is `Optional.some()`. /// - Returns: an new `EventLoopFuture` with new type parameter `NewValue` and with the value returned by the closure /// passed in the `orElse` parameter. @@ -516,10 +520,10 @@ extension EventLoopPromise { /// are coming from the event loop of the promise. /// /// This type is explicitly not `Sendable`. It may only be constructed on an event loop, - /// using ``EventLoopPromise/assumeIsolated``, and may not subsequently be passed to other isolation + /// using ``EventLoopPromise/assumeIsolated()``, and may not subsequently be passed to other isolation /// domains. /// - /// Using this type relaxes the need to have the promise completion functions accept ``Sendable`` + /// Using this type relaxes the need to have the promise completion functions accept `Sendable` /// values, as this type can only be handled on the ``EventLoop``. /// /// This type does not offer the full suite of completion functions that ``EventLoopPromise`` From ccff9c8b0095061109765baf38090504e129ab84 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 12 Nov 2024 15:58:34 +0000 Subject: [PATCH 7/9] Missed the one documentation one --- Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift index a34638b6d8..723ee32739 100644 --- a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift +++ b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift @@ -55,6 +55,7 @@ public struct NIOIsolatedEventLoop { /// Schedule a `task` that is executed by this `EventLoop` at the given time. /// /// - Parameters: + /// - deadline: The time at which the task should run. /// - task: The synchronous task to run. As with everything that runs on the `EventLoop`, it must not block. /// - Returns: A `Scheduled` object which may be used to cancel the task if it has not yet run, or to wait /// on the completion of the task. From 4714b902e0e0b6b9952135ef2e36b8619b7fc3d4 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Sun, 17 Nov 2024 17:12:28 +0000 Subject: [PATCH 8/9] Unavailable from async --- .../Docs.docc/loops-futures-concurrency.md | 16 +++++++++++++ .../EventLoopFuture+AssumeIsolated.swift | 24 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md b/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md index c2bf4304d6..a9af119ec0 100644 --- a/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md +++ b/Sources/NIOCore/Docs.docc/loops-futures-concurrency.md @@ -173,6 +173,14 @@ These isolated views can be obtained by calling ``EventLoopFuture/assumeIsolated ``EventLoop``, use ``EventLoopFuture/hop(to:)`` to move it to your isolation domain before using these types. +> Warning: ``EventLoopFuture/assumeIsolated()`` and ``EventLoopPromise/assumeIsolated()`` + **must not** be called from a Swift concurrency context, either an async method or + from within an actor. This is because it uses runtime checking of the event loop + to confirm that the value is not being sent to a different concurrency domain. + + When using an ``EventLoop`` as a custom actor executor, this API can be used to retrieve + a value that region based isolation will then allow to be sent to another domain. + ## Interacting with Event Loops on the Event Loop As with Futures, there are occasionally times where it is necessary to schedule @@ -204,3 +212,11 @@ This isolated view can be obtained by calling ``EventLoop/assumeIsolated()``. runtime ones. This makes it possible to introduce crashes in your code. Please ensure that you are 100% confident that the isolation domains align. If you are not sure that the your code is running on the relevant ``EventLoop``, prefer the non-isolated type. + +> Warning: ``EventLoop/assumeIsolated()`` **must not** be called from a Swift concurrency + context, either an async method or from within an actor. This is because it uses runtime + checking of the event loop to confirm that the value is not being sent to a different + concurrency domain. + + When using an ``EventLoop`` as a custom actor executor, this API can be used to retrieve + a value that region based isolation will then allow to be sent to another domain. diff --git a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift index 723ee32739..6b9ae952d5 100644 --- a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift +++ b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift @@ -31,6 +31,7 @@ public struct NIOIsolatedEventLoop { } /// Submit a given task to be executed by the `EventLoop` + @available(*, noasync) @inlinable public func execute(_ task: @escaping () -> Void) { let unsafeTransfer = UnsafeTransfer(task) @@ -44,6 +45,7 @@ public struct NIOIsolatedEventLoop { /// - Parameters: /// - task: The closure that will be submitted to the `EventLoop` for execution. /// - Returns: `EventLoopFuture` that is notified once the task was executed. + @available(*, noasync) @inlinable public func submit(_ task: @escaping () throws -> T) -> EventLoopFuture { let unsafeTransfer = UnsafeTransfer(task) @@ -62,6 +64,7 @@ public struct NIOIsolatedEventLoop { /// /// - Note: You can only cancel a task before it has started executing. @discardableResult + @available(*, noasync) @inlinable public func scheduleTask( deadline: NIODeadline, @@ -84,6 +87,7 @@ public struct NIOIsolatedEventLoop { /// - Note: You can only cancel a task before it has started executing. /// - Note: The `in` value is clamped to a maximum when running on a Darwin-kernel. @discardableResult + @available(*, noasync) @inlinable public func scheduleTask( in delay: TimeAmount, @@ -110,6 +114,7 @@ public struct NIOIsolatedEventLoop { /// /// - Note: You can only cancel a task before it has started executing. @discardableResult + @available(*, noasync) @inlinable public func flatScheduleTask( deadline: NIODeadline, @@ -133,6 +138,7 @@ public struct NIOIsolatedEventLoop { extension EventLoop { /// Assumes the calling context is isolated to the event loop. @inlinable + @available(*, noasync) public func assumeIsolated() -> NIOIsolatedEventLoop { self.preconditionInEventLoop() return NIOIsolatedEventLoop(self) @@ -203,6 +209,7 @@ extension EventLoopFuture { /// a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value. @inlinable + @available(*, noasync) public func flatMap( _ callback: @escaping (Value) -> EventLoopFuture ) -> EventLoopFuture.Isolated { @@ -227,6 +234,7 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value. @inlinable + @available(*, noasync) public func flatMapThrowing( _ callback: @escaping (Value) throws -> NewValue ) -> EventLoopFuture.Isolated { @@ -251,6 +259,7 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value or a rethrown error. @inlinable + @available(*, noasync) public func flatMapErrorThrowing( _ callback: @escaping (Error) throws -> Value ) -> EventLoopFuture.Isolated { @@ -287,6 +296,7 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value. @inlinable + @available(*, noasync) public func map( _ callback: @escaping (Value) -> (NewValue) ) -> EventLoopFuture.Isolated { @@ -311,6 +321,7 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the recovered value. @inlinable + @available(*, noasync) public func flatMapError( _ callback: @escaping (Error) -> EventLoopFuture ) -> EventLoopFuture.Isolated where Value: Sendable { @@ -334,6 +345,7 @@ extension EventLoopFuture { /// a new value or error lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the eventual value. @inlinable + @available(*, noasync) public func flatMapResult( _ body: @escaping (Value) -> Result ) -> EventLoopFuture.Isolated { @@ -356,6 +368,7 @@ extension EventLoopFuture { /// a new value lifted into a new `EventLoopFuture`. /// - Returns: A future that will receive the recovered value. @inlinable + @available(*, noasync) public func recover( _ callback: @escaping (Error) -> Value ) -> EventLoopFuture.Isolated { @@ -376,6 +389,7 @@ extension EventLoopFuture { /// - Parameters: /// - callback: The callback that is called with the successful result of the `EventLoopFuture`. @inlinable + @available(*, noasync) public func whenSuccess(_ callback: @escaping (Value) -> Void) { let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.whenSuccess { @@ -394,6 +408,7 @@ extension EventLoopFuture { /// - Parameters: /// - callback: The callback that is called with the failed result of the `EventLoopFuture`. @inlinable + @available(*, noasync) public func whenFailure(_ callback: @escaping (Error) -> Void) { let unsafeTransfer = UnsafeTransfer(callback) return self._wrapped.whenFailure { @@ -407,6 +422,7 @@ extension EventLoopFuture { /// - Parameters: /// - callback: The callback that is called when the `EventLoopFuture` is fulfilled. @inlinable + @available(*, noasync) public func whenComplete( _ callback: @escaping (Result) -> Void ) { @@ -423,6 +439,7 @@ extension EventLoopFuture { /// - callback: the callback that is called when the `EventLoopFuture` is fulfilled. /// - Returns: the current `EventLoopFuture` @inlinable + @available(*, noasync) public func always( _ callback: @escaping (Result) -> Void ) -> EventLoopFuture.Isolated { @@ -444,6 +461,7 @@ extension EventLoopFuture { /// - replacement: the value of the returned `EventLoopFuture` when then resolved future's value is `Optional.some()`. /// - Returns: an new `EventLoopFuture` with new type parameter `NewValue` and the value passed in the `orReplace` parameter. @inlinable + @available(*, noasync) public func unwrap( orReplace replacement: NewValue ) -> EventLoopFuture.Isolated where Value == NewValue? { @@ -470,6 +488,7 @@ extension EventLoopFuture { /// - Returns: an new `EventLoopFuture` with new type parameter `NewValue` and with the value returned by the closure /// passed in the `orElse` parameter. @inlinable + @available(*, noasync) public func unwrap( orElse callback: @escaping () -> NewValue ) -> EventLoopFuture.Isolated where Value == NewValue? { @@ -493,6 +512,7 @@ extension EventLoopFuture { /// ``EventLoop`` to which this ``EventLoopFuture`` is bound, will crash /// if that invariant fails to be met. @inlinable + @available(*, noasync) public func assumeIsolated() -> Isolated { self.eventLoop.preconditionInEventLoop() return Isolated(_wrapped: self) @@ -507,6 +527,7 @@ extension EventLoopFuture { /// omits the runtime check in release builds. This improves performance, but /// should only be used sparingly. @inlinable + @available(*, noasync) public func assumeIsolatedUnsafeUnchecked() -> Isolated { self.eventLoop.assertInEventLoop() return Isolated(_wrapped: self) @@ -546,6 +567,7 @@ extension EventLoopPromise { /// - Parameters: /// - value: The successful result of the operation. @inlinable + @available(*, noasync) public func succeed(_ value: Value) { self._wrapped._setValue(value: .success(value))._run() } @@ -565,6 +587,7 @@ extension EventLoopPromise { /// - Parameters: /// - result: The result which will be used to succeed or fail this promise. @inlinable + @available(*, noasync) public func completeWith(_ result: Result) { self._wrapped._setValue(value: result)._run() } @@ -581,6 +604,7 @@ extension EventLoopPromise { /// ``EventLoop`` to which this ``EventLoopPromise`` is bound, will crash /// if that invariant fails to be met. @inlinable + @available(*, noasync) public func assumeIsolated() -> Isolated { self.futureResult.eventLoop.preconditionInEventLoop() return Isolated(_wrapped: self) From d3dc7b95131de6d02d879161888778437dedd1b9 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Mon, 18 Nov 2024 11:29:23 +0000 Subject: [PATCH 9/9] Docs clarification --- Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift index 6b9ae952d5..4748ef320d 100644 --- a/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift +++ b/Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift @@ -147,8 +147,8 @@ extension EventLoop { /// Assumes the calling context is isolated to the event loop. /// /// This version of ``EventLoop/assumeIsolated()`` omits the runtime - /// isolation check in release builds. It retains it in debug mode to - /// ensure correctness. + /// isolation check in release builds and doesn't prevent you using it + /// from using it in async contexts. @inlinable public func assumeIsolatedUnsafeUnchecked() -> NIOIsolatedEventLoop { self.assertInEventLoop() @@ -524,8 +524,8 @@ extension EventLoopFuture { /// if that invariant fails to be met. /// /// This is an unsafe version of ``EventLoopFuture/assumeIsolated()`` which - /// omits the runtime check in release builds. This improves performance, but - /// should only be used sparingly. + /// omits the runtime check in release builds and doesn't prevent you using it + /// from using it in async contexts. @inlinable @available(*, noasync) public func assumeIsolatedUnsafeUnchecked() -> Isolated { @@ -616,8 +616,8 @@ extension EventLoopPromise { /// if that invariant fails to be met. /// /// This is an unsafe version of ``EventLoopPromise/assumeIsolated()`` which - /// omits the runtime check in release builds. This improves performance, but - /// should only be used sparingly. + /// omits the runtime check in release builds and doesn't prevent you using it + /// from using it in async contexts. @inlinable public func assumeIsolatedUnsafeUnchecked() -> Isolated { self.futureResult.eventLoop.assertInEventLoop()