From d803f8864253778c31eeb60870962aa0fec1c14a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 17 Apr 2025 13:30:29 +0100 Subject: [PATCH 1/3] Add helpers for working with loop bound values Motivation: Isolating state to a given event loop is quite a common pattern. These types often provide sendable view APIs which do the event-loop dance for you. Writing these wrappers is somewhat repetitive (each method needs to do the dance) and requires a precondition. We can make this simpler by providing helper methods on the loop bound types which do the event-loop dance. Modifications: Add three methods to NIOLoopBound and NIOLoopBoundBox: 1. `execute` to execute a task with the loop bound value on the event loop 2. `submit` to execute a task with the loop bound value on the event loop which returns a sendable value 3. `flatSubmit` to execute a task with the loop bound value on the event loop which returns a future holding a sendable value Result: Easier to build sendable views of non-sendable types which are isolated to a given event loop. --- Sources/NIOCore/NIOLoopBound.swift | 101 ++++++++++++++++++++ Tests/NIOPosixTests/NIOLoopBoundTests.swift | 51 ++++++++++ 2 files changed, 152 insertions(+) diff --git a/Sources/NIOCore/NIOLoopBound.swift b/Sources/NIOCore/NIOLoopBound.swift index d3eb66c1a04..0df63a8b63b 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -58,6 +58,56 @@ public struct NIOLoopBound: @unchecked Sendable { yield &self._value } } + + /// Executes the closure on the event loop the value is bound to. + @inlinable + public func execute(_ task: @escaping @Sendable (Value) -> Void) { + if self.eventLoop.inEventLoop { + task(self._value) + } else { + self.eventLoop.execute { + task(self._value) + } + } + } + + /// Executes the closure on the event loop the value is bound to and returns the result in + /// a future. + /// + /// - Parameter task: The task to execute on the event loop with the loop bound value. + /// - Returns: A future containing the result of the task. + @inlinable + public func submit( + _ task: @escaping @Sendable (Value) throws -> Result + ) -> EventLoopFuture { + if self.eventLoop.inEventLoop { + self.eventLoop.makeCompletedFuture { + try task(self._value) + } + } else { + self.eventLoop.submit { + try task(self._value) + } + } + } + + /// Executes the closure on the event loop the value is bound to and returns the result in + /// a future. + /// + /// - Parameter task: The task to execute on the event loop with the loop bound value. + /// - Returns: A future containing the result of the task. + @inlinable + public func flatSubmit( + _ task: @escaping @Sendable (Value) -> EventLoopFuture + ) -> EventLoopFuture { + if self.eventLoop.inEventLoop { + task(self._value) + } else { + self.eventLoop.flatSubmit { + task(self._value) + } + } + } } /// ``NIOLoopBoundBox`` is an always-`Sendable`, reference-typed container allowing you access to ``value`` if and @@ -175,4 +225,55 @@ public final class NIOLoopBoundBox: @unchecked Sendable { yield &self._value } } + + /// Executes the closure on the event loop the value is bound to. + @inlinable + public func execute(_ task: @escaping @Sendable (Value) -> Void) { + if self.eventLoop.inEventLoop { + task(self._value) + } else { + self.eventLoop.execute { + task(self._value) + } + } + } + + /// Executes the closure on the event loop the value is bound to and returns the result in + /// a future. + /// + /// - Parameter task: The task to execute on the event loop with the loop bound value. + /// - Returns: A future containing the result of the task. + @inlinable + public func submit( + _ task: @escaping @Sendable (Value) throws -> Result + ) -> EventLoopFuture { + if self.eventLoop.inEventLoop { + self.eventLoop.makeCompletedFuture { + try task(self._value) + } + } else { + self.eventLoop.submit { + try task(self._value) + } + } + } + + /// Executes the closure on the event loop the value is bound to and returns the result in + /// a future. + /// + /// - Parameter task: The task to execute on the event loop with the loop bound value. + /// - Returns: A future containing the result of the task. + @inlinable + public func flatSubmit( + _ task: @escaping @Sendable (Value) -> EventLoopFuture + ) -> EventLoopFuture { + if self.eventLoop.inEventLoop { + task(self._value) + } else { + self.eventLoop.flatSubmit { + task(self._value) + } + } + } + } diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index e50ccf79c4b..46342b04a8a 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -116,6 +116,46 @@ final class NIOLoopBoundTests: XCTestCase { XCTAssertTrue(loopBoundBox.value.mutateInPlace()) } + func testExecute() { + let loopBound = NIOLoopBound(Ref(31), eventLoop: self.loop) + loopBound.execute { ref in + XCTAssertEqual(ref.value, 31) + ref.value = 415 + } + XCTAssertEqual(loopBound.value.value, 415) + + let loopBoundBox = NIOLoopBoundBox(Ref(31), eventLoop: self.loop) + loopBoundBox.execute { ref in + XCTAssertEqual(ref.value, 31) + ref.value = 415 + } + XCTAssertEqual(loopBoundBox.value.value, 415) + } + + func testSubmit() throws { + let loopBound = NIOLoopBound(Ref(42), eventLoop: self.loop) + let value1 = try loopBound.submit { $0.value }.wait() + XCTAssertEqual(value1, 42) + + let loopBoundBox = NIOLoopBoundBox(Ref(42), eventLoop: self.loop) + let value2 = try loopBoundBox.submit { $0.value }.wait() + XCTAssertEqual(value2, 42) + } + + func testFlatSubmit() throws { + let loopBound = NIOLoopBound(Ref(42), eventLoop: self.loop) + let value1 = try loopBound.flatSubmit { ref in + self.loop.makeSucceededFuture(ref.value) + }.wait() + XCTAssertEqual(value1, 42) + + let loopBoundBox = NIOLoopBoundBox(Ref(42), eventLoop: self.loop) + let value2 = try loopBoundBox.flatSubmit { ref in + self.loop.makeSucceededFuture(ref.value) + }.wait() + XCTAssertEqual(value2, 42) + } + // MARK: - Helpers func sendableBlackhole(_ sendableThing: S) {} @@ -134,3 +174,14 @@ final class NotSendable {} @available(*, unavailable) extension NotSendable: Sendable {} + +final class Ref { + var value: Value + + init(_ value: Value) { + self.value = value + } +} + +@available(*, unavailable) +extension Ref: Sendable {} From b800f048093749d07246b698f88984dde1c0147c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 17 Apr 2025 14:13:01 +0100 Subject: [PATCH 2/3] self aint sendable --- Tests/NIOPosixTests/NIOLoopBoundTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index 46342b04a8a..c4dafddade4 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -144,14 +144,14 @@ final class NIOLoopBoundTests: XCTestCase { func testFlatSubmit() throws { let loopBound = NIOLoopBound(Ref(42), eventLoop: self.loop) - let value1 = try loopBound.flatSubmit { ref in - self.loop.makeSucceededFuture(ref.value) + let value1 = try loopBound.flatSubmit { [loop] ref in + loop.makeSucceededFuture(ref.value) }.wait() XCTAssertEqual(value1, 42) let loopBoundBox = NIOLoopBoundBox(Ref(42), eventLoop: self.loop) - let value2 = try loopBoundBox.flatSubmit { ref in - self.loop.makeSucceededFuture(ref.value) + let value2 = try loopBoundBox.flatSubmit { [loop] ref in + loop.makeSucceededFuture(ref.value) }.wait() XCTAssertEqual(value2, 42) } From 89fe20efac6f02af920177d1a519d6ba83b55f07 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 17 Apr 2025 14:44:52 +0100 Subject: [PATCH 3/3] darn iuo --- Tests/NIOPosixTests/NIOLoopBoundTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index c4dafddade4..730c587e27f 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -145,13 +145,13 @@ final class NIOLoopBoundTests: XCTestCase { func testFlatSubmit() throws { let loopBound = NIOLoopBound(Ref(42), eventLoop: self.loop) let value1 = try loopBound.flatSubmit { [loop] ref in - loop.makeSucceededFuture(ref.value) + loop!.makeSucceededFuture(ref.value) }.wait() XCTAssertEqual(value1, 42) let loopBoundBox = NIOLoopBoundBox(Ref(42), eventLoop: self.loop) let value2 = try loopBoundBox.flatSubmit { [loop] ref in - loop.makeSucceededFuture(ref.value) + loop!.makeSucceededFuture(ref.value) }.wait() XCTAssertEqual(value2, 42) }