Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NIOSendableBox: allow off-loop initialisation iff Value is Sendable #2753

Merged
merged 2 commits into from
Jun 27, 2024

Conversation

weissi
Copy link
Member

@weissi weissi commented Jun 26, 2024

Motivation:

NIOLoopBound and NIOLoopBoundBox are important tools for making mutable types Sendable.
The power of NIOLoopBoundBox is that it allows correct mutation, whilst remaining Sendable without locks and without having to use @unchecked if the mutable class makes sure to have all accesses (reading & writing) run on one particular EventLoop. This is safe as EventLoops guarantee sequentially consistent memory order across executions of different work items/events. Typically EventLoops achieve this by just being thread-bound.

These types are well used already but there's a small and common pattern which is safe but unsupported as of yet:

Initialise a NIOLoopBoundBox with a Sendable value off the loop. All further accesses (reads & writes) however happen on the loop. That's safe because of Swift's Definitive Initialisation (DI) but NIOLoopBoundBox didn't support this pattern (apart from makeEmptyBox which always initialises with nil).

Modifications:

  • Allow Sendable values to be provided whilst creating the type off loop.

Result:

  • Even more types can be safely & correctly made Sendable without using `@unchecked.

/// Contrary to ``init(_:eventLoop:)``, this method can be called off `eventLoop` because we know that `value` is ``Sendable``.
/// So we don't need to protect `value` itself, we just need to protect the ``NIOLoopBoundBox`` against mutations which we do because the ``value``
/// accessors are checking that we're on `eventLoop`.
public static func makeBoxSendingValue(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it weird to use a factory method here personally. I would prefer using an init here. I also see little value in having this constrained to Sendable since Sendable types can freely be shared between threads.

I expected a spelling like this

init(value: sending Value, eventLoop: any EventLoop) {}

This would allow moving an non-Sendable value into an EL's isolation region which is dynamically enforced by the LoopBoundBox.

Copy link
Member Author

@weissi weissi Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • factory is to get "more words", the inits are unconstrained but have to run on eventLoop. And the makeBoxSendingValue ones can be run off the EL with various constraints (either Sendable or sending)
  • sending isn't ready yet (Give NIOLoopBoundBox a sending off-EL initialiser once compiler is ready #2754)
  • there's a huge value for Sendable values. It's not about sending the values around, NIOLoopBoundBox is about mutating the values. Typically this is for thread-safe network clients/servers that have a state machine which only gets accessed on a particular EL. So current: var state: State // only access on self.eventLoop; new: let state: NIOLoopBound<State>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your goal but IMO NIOLoopBoundBox with an already Sendable value does not make sense. What you want here is just a plain old Box<T> that is conditionally Sendable on T. If the boxed value is already Sendable then the loop enforcement adds no value since the type is already thread safe. The storage might not i.e. the var but the only thing needed for that is to make it a let through adding reference semantics. let boxed = Box<MySendableType> is totally safe.

Copy link
Member Author

@weissi weissi Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not accurate.

  • class Box<T> { var value: T } cannot be Sendable (without synchronisation), you get mutation and sharing.
  • class NIOLoopBoundBox<T> { var value: T /* checks EL on all accesses */ } is always Sendable because it checks that all accesses are on the same EL. So you get mutation but no sharing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got no objection to the PR in its current state, but I'm not going to approve because I'd like to see @FranzBusch and @weissi get aligned here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with taking this as is.

@weissi weissi requested a review from FranzBusch June 26, 2024 18:21
@weissi
Copy link
Member Author

weissi commented Jun 27, 2024

19:46:31 Test Case 'NIOConcurrencyHelpersTests.testLoadAndStoreHammeBuild timed out (after 20 minutes). Marking the build as failed.

@weissi
Copy link
Member Author

weissi commented Jun 27, 2024

@swift-nio-bot test failed

@weissi weissi enabled auto-merge (squash) June 27, 2024 14:17
@weissi
Copy link
Member Author

weissi commented Jun 27, 2024

@swift-nio-bot test this please

@weissi weissi merged commit fc79798 into apple:main Jun 27, 2024
8 of 9 checks passed
@weissi weissi deleted the jw-sendable-box-init branch June 27, 2024 16:49
chkp-aviads added a commit to chkp-aviads/swift-nio that referenced this pull request Jul 21, 2024
* commit 'fc79798d5a150d61361a27ce0c51169b889e23de':
  NIOSendableBox: allow off-loop initialisation iff Value is Sendable (apple#2753)
  Throw an appropriate error from the writer when the channel closed (apple#2744)
  put snippet code inside @available function (apple#2750)
  fix link to NIOFileSystem from NIO index page (apple#2747)
  convert the NIOFileSystem example code to a Snippet (apple#2746)
  Silence warning about missing include in macOS builds (apple#2741)
  Correctly mark 304 as not having a response body (apple#2737)
  Update availability guard (apple#2739)
  Add API for setting last accessed and last modified file times (apple#2735)
  Add a fallback path if renameat2 fails (apple#2733)
  Release file handles back to caller on failure to take ownership (apple#2715)
  Add a version of 'write' for 'ByteBuffer' (apple#2730)
  Imrprove rename error (apple#2731)
  Remove storage indirection for FileSystemError (apple#2726)
  testSimpleMPTCP should not fail for ENOPROTOOPT (apple#2725)
  Fix race in TCPThroughputBenchmark (apple#2724)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants