Skip to content
This repository was archived by the owner on Aug 5, 2022. It is now read-only.

Swift Concurrency adoption guidelines for Swift Server Libraries #70

Merged

Conversation

fabianfett
Copy link
Member

This is a first draft for "Swift Concurrency adoption guidelines for Swift Server Libraries" that was written by @ktoso and me. Looking forward to your input.

@ktoso
Copy link
Contributor

ktoso commented Sep 16, 2021

Some follow ups we want to do:

Copy link

@ddossot ddossot left a comment

Choose a reason for hiding this comment

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

Great stuff, dropped a few comments.

@ktoso
Copy link
Contributor

ktoso commented Sep 17, 2021

Thanks David!

@kmahar kmahar requested a review from patrickfreed September 17, 2021 01:10
Copy link
Contributor

@kmahar kmahar left a comment

Choose a reason for hiding this comment

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

looks really good and I think this will be very helpful to library authors (including myself)! thank you both @fabianfett and @ktoso for your work on this.

the vast majority of my comments are just formatting / wording nits, adding links etc. to make things a little more readable.

@@ -0,0 +1,179 @@
# Swift Concurrency adoption guidelines for Swift Server Libraries
Copy link
Contributor

Choose a reason for hiding this comment

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

may be nice to add an "index" at the end before we merge so people can see the high level outline/jump to particular sections as this is getting rather lengthy

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point, will do

@ktoso
Copy link
Contributor

ktoso commented Sep 17, 2021

Thanks a lot for the edits @kmahar !

Co-authored-by: Kaitlin Mahar <kaitlinmahar@gmail.com>

Most of these issues have been resolved on today’s `main` branch of the compiler, and are expected to land in the next Swift 5.5 releases. It may be worthwhile waiting for adoption until the next version(s) after 5.5.0.

For example, one of such capabilities is the ability for tuples of `Sendable` types to conform to `Sendable` as well. We recommend holding off adoption of `Sendable` until this patch lands in Swift 5.5 (which should be relatively soon). With this change, the difference between Swift 5.5 with `-warn-concurrency` enabled and Swift 6 mode should be very small, and manageable on a case by case basis.

Choose a reason for hiding this comment

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

It would be better to put an expected date in rather than relatively soon or this guide will not read well in a few months time.


### End-user code breakage

It is expected that Swift 6 will break some code. As mentioned Swift NIO 3 is also going to be released sometime around Swift 6 dropping. Keeping this in mind, it might be a good idea to align major version releases around the same time, along with updating version requirements to Swift 6 and NIO 3 in your libraries.

Choose a reason for hiding this comment

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

I wonder if "swift 6 dropping" is a term that everyone will recognise. eg, wondering what that would translate to if I ran this through google translate to a non-english language.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a good point, a bit too "slang-y" I guess, thanks! I'll reword

Copy link
Member

@adam-fowler adam-fowler left a comment

Choose a reason for hiding this comment

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

From a first read looks good. I've added a couple of questions.
The future directions section is useful for people to plan ahead.

@adam-fowler
Copy link
Member

  • example with async sequence

Soto has an AsyncSequence sample used for paginated results. It is full of generics so might not be the simplest example though. https://github.com/soto-project/soto-core/blob/async-await/Sources/SotoCore/AWSClient%2BPaginate%2Basync.swift

In such situations, it may be helpful to utilize the following trick to be able to share the same `Container` declaration between both Swift versions of the library:

```swift
#if compiler(>=5.5)
Copy link
Contributor

Choose a reason for hiding this comment

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

compiler is intended here and not swift?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you remind me @Lukasa which one is right here?

Copy link
Contributor

Choose a reason for hiding this comment

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

So in the end we need #if swift(>=5.5) && canImport(_Concurrency)

Copy link
Contributor

@patrickfreed patrickfreed left a comment

Choose a reason for hiding this comment

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

Looks great! I only have a few comments / questions


To allow an easy transition to async code, SwiftNIO offers a number of helper methods on `EventLoopFuture` and `-Promise`. Those live in the `_NIOConcurrency` module and will move to `NIOCore` once Swift concurrency is released.

On every `EventLoopFuture` you can call `.get()` to transition the future into an `await`-able invocation. If you want to translate async/await calls to an `EventLoopFuture` we recommend the following pattern:
Copy link
Contributor

Choose a reason for hiding this comment

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

I know an example for this will be relatively trivial, but I think it would still be useful to include here. As a side benefit it'll highlight the fact that any such method will need to be throws as @adam-fowler points out above.

As part of that, it'll read a little clearer if the sentence about going the other direction (async -> EventLoopFuture) is broken off into its own line following the new example.


While subject to change, it is likely that Swift NIO will cut a 3.0 release in the months after Swift 6.0, at which point in time Swift will have enabled “full” `Sendable` checking.

Do not expect NIO to suddenly become “more async”, NIO’s inherent design principles are about performing small tasks on the event loop and using Futures for any async operations. The design of NIO is not expected to change. It is crucial to its high performance networking design. Channel pipelines are not expected to become “async”.
Copy link
Contributor

Choose a reason for hiding this comment

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

The design of NIO is not expected to change. It is crucial to its high performance networking design.

This line seems to imply that async cannot be used without significant overhead, is this the case? If so, should we include guidance in this document about when not to completely adopt async, as NIO has opted not to? Or maybe a note saying NIO is a special case due to its low level nature, and that most/all other libraries should try to become more async?

The reason I mention this is that the majority of the document focuses on encouraging adoption of async, but at the very end here we have a note about how NIO will largely not be doing so, which may be a bit contradictory. A little bit further clarification could help with that though.

Copy link

Choose a reason for hiding this comment

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

NIO is not doing so because of the concerns around the “bottom” of the async pyramid. async/await is built on top of a co-operative thread pool within the Swift runtime. This thread pool must not be blocked by any operation, because doing so will starve the pool and prevent further progress of other async tasks.

This poses a problem for I/O. At some point, someone somewhere has to block a thread waiting for more I/O events, either in an I/O syscall or in something like epoll_wait. This is how NIO works: each of the event loop threads ultimately blocks on epoll_wait. We can’t do that inside the cooperative thread pool, as to do so would starve it for other async tasks, so we’d have to do so on a different thread.

This would mean we’d have a thread-hop between each I/O operation and dispatching it onto the async/await pool. This is not acceptable for high performance I/O: the context switch for each I/O op is too expensive. As a result, until we can safely block a thread for ourselves (with concurrent executors) we simply cannot use the async/await pattern in NIO code.

The rule of thumb is: if you need the absolute fastest computation on data from the network, you write ChannelHandlers and do your I/O on event loops. If you can tolerate slightly less performance there, likely because your business logic is more substantial, then async/await will repay you in ease of use much more than the (small) amount it’ll cost you in perf.

Copy link
Contributor

Choose a reason for hiding this comment

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

That all makes sense, thanks for the detailed answer! I think it may be beneficial then to reword the existing paragraph to include some of this context and to be bit more explanatory rather than declarative (i.e. "NIO's API will largely remain the same because..." rather than "don't expect NIO's API to change..."). This will also help library authors decide when they might need to use NIO's facilities more directly rather than higher level async/await ones.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, happy to take some of the wording into the doc 👍

In order to have code using concurrency along with code not using concurrency, you may have to `#if` guard certain pieces of code. The correct way to do so is the following:

```swift
#if compiler(>=5.5) && canImport(_Concurrency)
Copy link
Contributor

Choose a reason for hiding this comment

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

There are bunch of places in this guide which are missing && canImport(_Concurrency). There are also places where swift(>=5.5) instead of compiler(>=5.5).

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's worth adding a note about @available requirements too.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks done


## What you can do right now

### `#if` guarding code using Concurrency
Copy link
Contributor

Choose a reason for hiding this comment

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

The subsections are roughly categorised as API, implementation and preparing for the future, so I think the "API design" and "SwiftNIO Helper Functions" sections should come before this one.

Copy link
Contributor

Choose a reason for hiding this comment

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

done thanks


If a function cannot fail but was using futures before, it should not include the `throws` keyword in its new incarnation.

Such adoption can begin immediately, and should not cause any issues to existing users of existing libraries.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there are broader changes that libraries can make as well, e.g. AsyncSequence based APIs rather than delegate based APIs, for example.

It may also be worth calling out that given we're on the road to Swift 6, libraries and the ecosystem as a whole are going to have to go through major version bumps so adding async-await APIs now can help lessen the pain of going through a major version change later because new APIs can be adopted incrementally.


The NIO team will however use the chance to remove deprecated APIs and improve some APIs. The scope of changes should be comparable to the NIO1 → NIO2 version bump. If your SwiftNIO code compiles today without warnings, chances are high that it will continue to work without modifications in NIO3.

After the release of NIO3, NIO2 will see bug fixes only.
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll fix security vulnerabilities too!

Copy link
Contributor

Choose a reason for hiding this comment

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

thanks will add


### Guidance for library users

As soon as Swift 6 comes out, we recommend using the latest Swift 6 toolchains, even if using the Swift 5.5.n language mode (which may yield only warnings rather than hard failures on failed Sendability checks).
Copy link
Contributor

Choose a reason for hiding this comment

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

Worth saying why we make this recommendation

Copy link
Contributor

Choose a reason for hiding this comment

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

thanks

As soon as Swift 6 comes out, we recommend using the latest Swift 6 toolchains, even if using the Swift 5.5.n language mode (which may yield only warnings rather than hard failures on failed Sendability checks). This will result in better warnings and compiler hints, than just using a 5.5 toolchain.

-> EventLoopFuture<Result> {
let promise = context.eventLoop.makePromise(of: Out.self)
promise.completeWithTask {
try await yourMethod(yourInputs)

Choose a reason for hiding this comment

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

Question about this; from what I can tell, this won't run yourMethod on an EventLoop; (assuming because I tried an example and MultiThreadedEventLoopGroup.currentEventLoop is nil).

Is there a way to have async tasks run on an event loop?

I ask since I thought that ideally during a request's lifecycle it won't be hopping off the EventLoop it's received on, even if running async tasks like a database lookup.

I might be confused on what's going on here too 🙂

Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to have async tasks run on an event loop?

Not today; that's what upcoming work on custom executors in Swift Concurrency would allow.

Choose a reason for hiding this comment

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

The specific use case is that when running database queries, I've got a dict of connection pools, one for each EventLoop.

When a query is made, a pool is found using the current event loop's objectidentifier. But it seems like with async functions, they aren't running on an event loop and so if they need to run a database query it's not possible to look up a pool based on that.

Copy link
Contributor

Choose a reason for hiding this comment

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

that's correct

Co-authored-by: George Barnett <gbarnett@apple.com>
ktoso and others added 2 commits October 7, 2021 21:23
Co-authored-by: Patrick Freed <patrick.freed@mongodb.com>
Co-authored-by: Yim Lee <yim_lee@apple.com>
@ktoso
Copy link
Contributor

ktoso commented Nov 1, 2021

Going through including feedback last time and will merge, thanks for all the comments!

@ktoso
Copy link
Contributor

ktoso commented Nov 8, 2021

Ok i think i applied all changes. Please do feel free to send in PRs and keep this guide growing with more patterns and ideas!

@ktoso
Copy link
Contributor

ktoso commented Nov 8, 2021

Meh can't edit this PR branch, so will merge and PR followups

@ktoso ktoso merged commit 60ff832 into swift-server:main Nov 8, 2021
fabianfett pushed a commit to swift-server/swift-aws-lambda-runtime that referenced this pull request Dec 6, 2021
Adopt Swift Concurrency adoption guidelines for Swift Server Libraries (swift-server/guides#70).

- Use #if compiler(>=5.5) && canImport(_Concurrency) to judge if Concurrency is available
- Some clean up
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.