From be7fd501a61ebcb509480ce05506862d3ee00b96 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 30 Sep 2025 17:53:07 +0200 Subject: [PATCH 1/5] 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 9f843b834bd..7165d9a9bc1 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -173,4 +173,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 2e6b51e4fcf..f8e161c8190 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -114,6 +114,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 9ba03b7e653d8fe2abe29af334ac580469e5252c Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 30 Sep 2025 17:58:33 +0200 Subject: [PATCH 2/5] 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 7165d9a9bc1..1bad96d1d54 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -174,6 +174,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 @@ -192,4 +193,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 f8e161c8190..aa0cd7d92d3 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -114,6 +114,7 @@ final class NIOLoopBoundTests: XCTestCase { XCTAssertTrue(loopBoundBox.value.mutateInPlace()) } + #if compiler(>=6.0) func testWithValue() { var expectedValue = 0 let loopBound = NIOLoopBoundBox(expectedValue, eventLoop: loop) @@ -141,6 +142,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 1ef7e2bd2d1dbb5138fdb1e8e05ab1f99aa3bd4c Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 1 Oct 2025 10:11:52 +0200 Subject: [PATCH 3/5] 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 1bad96d1d54..e969d7359ec 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -187,11 +187,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 76347196946eaaa1d7758aec4ce2a75d99e1da7e Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 2 Oct 2025 12:28:50 +0200 Subject: [PATCH 4/5] 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 e969d7359ec..40c9721d443 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -174,7 +174,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 @@ -187,11 +186,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 } From 5a21840e8901975ddb737624fec08a524cd24609 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Mon, 10 Nov 2025 12:13:18 +0100 Subject: [PATCH 5/5] Removed now unnecessary compiler guards. --- Tests/NIOPosixTests/NIOLoopBoundTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index aa0cd7d92d3..f8e161c8190 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -114,7 +114,6 @@ final class NIOLoopBoundTests: XCTestCase { XCTAssertTrue(loopBoundBox.value.mutateInPlace()) } - #if compiler(>=6.0) func testWithValue() { var expectedValue = 0 let loopBound = NIOLoopBoundBox(expectedValue, eventLoop: loop) @@ -142,7 +141,6 @@ final class NIOLoopBoundTests: XCTestCase { XCTAssertEqual(10, loopBound.value, "Ensure value is set even if we throw") } - #endif // MARK: - Helpers func sendableBlackhole(_ sendableThing: S) {}