From f63c7088f8ae47b6042d082899832c73802882ea Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 30 Sep 2025 17:53:07 +0200 Subject: [PATCH 1/4] Quality of Life: Add `NIOLoopBoundBox.withValue` --- Sources/NIOCore/NIOLoopBound.swift | 19 ++++++++++++++ Tests/NIOPosixTests/NIOLoopBoundTests.swift | 28 +++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Sources/NIOCore/NIOLoopBound.swift b/Sources/NIOCore/NIOLoopBound.swift index ad216fd5c7..699d3e80d0 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -175,4 +175,23 @@ public final class NIOLoopBoundBox: @unchecked Sendable { yield &self._value } } + + /// Safely access and potentially modify the contained value with a closure. + /// + /// This method provides a way to perform operations on the contained value while ensuring + /// thread safety through EventLoop verification. The closure receives an `inout` parameter + /// allowing both read and write access to the value. + /// + /// - Parameter handler: A closure that receives an `inout` reference to the contained value. + /// The closure can read from and write to this value. Any modifications made within the + /// closure will be reflected in the box after the closure completes, even if the closure throws. + /// - Returns: The value returned by the `handler` closure. + /// - Note: This method is particularly useful when you need to perform read and write operations + /// on the value because it reduces the on EventLoop checks. + public func withValue(_ handler: (inout Value) throws(E) -> T) throws(E) -> T { + self.eventLoop.preconditionInEventLoop() + var value = self._value + defer { self._value = value } + return try handler(&value) + } } diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index e50ccf79c4..58a4548ba3 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -116,6 +116,34 @@ final class NIOLoopBoundTests: XCTestCase { XCTAssertTrue(loopBoundBox.value.mutateInPlace()) } + func testWithValue() { + var expectedValue = 0 + let loopBound = NIOLoopBoundBox(expectedValue, eventLoop: loop) + for value in 1...100 { + loopBound.withValue { boundValue in + XCTAssertEqual(boundValue, expectedValue) + boundValue = value + expectedValue = value + } + } + XCTAssertEqual(100, loopBound.value) + } + + func testWithValueRethrows() { + struct TestError: Error {} + + let loopBound = NIOLoopBoundBox(0, eventLoop: loop) + XCTAssertThrowsError( + try loopBound.withValue { boundValue in + XCTAssertEqual(0, boundValue) + boundValue = 10 + throw TestError() + } + ) + + XCTAssertEqual(10, loopBound.value, "Ensure value is set even if we throw") + } + // MARK: - Helpers func sendableBlackhole(_ sendableThing: S) {} From 37e5c1836601406f006d8cd776f7eb77ebd46252 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 30 Sep 2025 17:58:33 +0200 Subject: [PATCH 2/4] 6.0 only --- Sources/NIOCore/NIOLoopBound.swift | 2 ++ Tests/NIOPosixTests/NIOLoopBoundTests.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Sources/NIOCore/NIOLoopBound.swift b/Sources/NIOCore/NIOLoopBound.swift index 699d3e80d0..7f73ec23c4 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -176,6 +176,7 @@ public final class NIOLoopBoundBox: @unchecked Sendable { } } + #if compiler(>=6.0) /// Safely access and potentially modify the contained value with a closure. /// /// This method provides a way to perform operations on the contained value while ensuring @@ -194,4 +195,5 @@ public final class NIOLoopBoundBox: @unchecked Sendable { defer { self._value = value } return try handler(&value) } + #endif } diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index 58a4548ba3..30f260ff33 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -116,6 +116,7 @@ final class NIOLoopBoundTests: XCTestCase { XCTAssertTrue(loopBoundBox.value.mutateInPlace()) } + #if compiler(>=6.0) func testWithValue() { var expectedValue = 0 let loopBound = NIOLoopBoundBox(expectedValue, eventLoop: loop) @@ -143,6 +144,7 @@ final class NIOLoopBoundTests: XCTestCase { XCTAssertEqual(10, loopBound.value, "Ensure value is set even if we throw") } + #endif // MARK: - Helpers func sendableBlackhole(_ sendableThing: S) {} From f1688e3ed0a658da9b5818efd01b9536b1cc5252 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 1 Oct 2025 10:11:52 +0200 Subject: [PATCH 3/4] Code review --- Sources/NIOCore/NIOLoopBound.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NIOCore/NIOLoopBound.swift b/Sources/NIOCore/NIOLoopBound.swift index 7f73ec23c4..dd6ad5fb0f 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -189,11 +189,11 @@ public final class NIOLoopBoundBox: @unchecked Sendable { /// - Returns: The value returned by the `handler` closure. /// - Note: This method is particularly useful when you need to perform read and write operations /// on the value because it reduces the on EventLoop checks. - public func withValue(_ handler: (inout Value) throws(E) -> T) throws(E) -> T { + public func withValue( + _ handler: (inout Value) throws(Failure) -> Success + ) throws(Failure) -> Success { self.eventLoop.preconditionInEventLoop() - var value = self._value - defer { self._value = value } - return try handler(&value) + return try handler(&self._value) } #endif } From 1e41037982e3e7756f55b3c53e2a5a61f7d1041d Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 2 Oct 2025 12:28:50 +0200 Subject: [PATCH 4/4] PR comment --- Sources/NIOCore/NIOLoopBound.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/NIOCore/NIOLoopBound.swift b/Sources/NIOCore/NIOLoopBound.swift index dd6ad5fb0f..2d0bf87874 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -176,7 +176,6 @@ public final class NIOLoopBoundBox: @unchecked Sendable { } } - #if compiler(>=6.0) /// Safely access and potentially modify the contained value with a closure. /// /// This method provides a way to perform operations on the contained value while ensuring @@ -189,11 +188,11 @@ public final class NIOLoopBoundBox: @unchecked Sendable { /// - Returns: The value returned by the `handler` closure. /// - Note: This method is particularly useful when you need to perform read and write operations /// on the value because it reduces the on EventLoop checks. + @inlinable public func withValue( _ handler: (inout Value) throws(Failure) -> Success ) throws(Failure) -> Success { self.eventLoop.preconditionInEventLoop() return try handler(&self._value) } - #endif }