-
Notifications
You must be signed in to change notification settings - Fork 650
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
ecosystem issue: EventLoopGroupProvider and the 'similar create or share ELG' pattern #2142
Comments
Hmm, I agree that the pattern is ugly, but do we think it rises to "really bad" in a way that justifies having a singleton event loop group? |
I hate global singletons with a passion. But:
So I think the prior art from the Apple SDKs and the lack of real alternatives lead to me believe that it's the least of all the evils. Any better ideas? |
I mean, the simplest idea is the one that we have been falling back to all along: creating the ELG is the app's business, not the business of any library. This is coherent (apps have lifecycles, libraries don't) and customizable. It's also easier to manage and test. |
I somewhat agree that the |
To chime in here - in the world where Swift Concurrency is everywhere and libraries like Vapor, AHC etc don't expose or have ELGs in their public interfaces, forcing it to be passed around is confusing for an end user, which I think is the point @weissi was making. Is this something that goes solved eventually with custom executors? |
Ideologically, I agree with you. Practically, I don't. People just don't want to carry random amount of stuff through their whole application. That's also why we created the ELGProvider pattern in AHC, it was just not acceptable to force everybody to
Well, certain objects (like
Exactly. I think Vapor did this really neatly by just (by default) owning the ELG in the App. And because Vapor's a framework that works just fine. You create your AsyncHTTPClient and other libraries (that aren't the application framework) this is much harder because they shouldn't [resources]/can't [lifecycle] all own their ELGs for every In this issue, I'd like to discuss the use case for libraries, i.e. things that might be used as an implementation detail in some other library/application where it'd be awkward to require the initialisation of something in main that then needs to be passed around. Right now, this is actually even harder because of issues like
|
An implicit event loop group is still going to require someone to choose what the ELG is, or we'll commit a layering violation. Additionally, Swift provides no mechanism for running module constructors so in practice it is entirely possible that none of these modules have executed any code at the point when someone tries to use a default implicit ELG, at which point the only thing we can do is crash (again, We could work around that by having each module define its own implicit global event loop group (and then the library just has to choose which of those implicit loops it wants to use), but at that stage we end up with the result that there are several implicit global event loop groups. I'm honestly not sure this solution is better than the problem we currently have. |
@Lukasa or we could have a new repo, let's call it Then AHC and others can depend on just WDYT? That'd also keep the singletons out of the core NIO repos. |
Yup, we can do that, but then we get two new problems:
|
I'm very open to better, more open and composable solutions but I do think we need to address the real issue here which can probably be summed up as There is nothing good that a library can do today. Why?
|
I think my position is that we have no good options. It is not at all clear to me that replacing an ugly cleanup pattern with implicit global state solves more problems than it creates. We'll certainly have different problems, but that's not the same as fewer. We're fundamentally trying to thread a needle that no-one else has to thread: we want to make it possible to use libraries without any configuration, but enable extensive configuration when users want it, with a distributed set of possible implementations where users may want to avoid having any particular one of them present. That's just gonna be hard. We're probably going to have to decide what problem we think is most important and accept that we're going to rule out a few others. |
My position is that we have to do something because it forces this bad API choice on everybody who consumes AHC or other libraries using this pattern. And fact is that with Swift Concurrency being the future, most of the code is running on a global singleton pool anyway, already. |
Where are we in this discussion? I haven't seen much reference to this discussion in the SSWG meeting notes since June, and the last discussion was had in June. |
### Motivation: SwiftNIO allows and encourages to precisely manage all operating system resources like file descriptors & threads. That is a very important property of the system but in many places -- especially since Swift Concurrency arrived -- many simpler SwiftNIO programs only require a single, globally shared EventLoopGroup. Often even with just one thread. Long story short: Many, probably most users would happily trade precise control over the threads for not having to pass around `EventLoopGroup`s. Today, many of those users resort to creating (and often leaking) threads because it's simpler. Adding a `.globalSingle` static var which _lazily_ provides singleton `EventLoopGroup`s and `NIOThreadPool`s is IMHO a much better answer. Finally, this aligns SwiftNIO a little more with Apple's SDKs which have a lot of global singletons that hold onto system resources (`Dispatch`'s thread pool, `URLSession.shared`, ...). At least in `Dispatch`'s case the Apple SDKs actually make it impossible to manage the resources, there can only ever be one global pool of threads. That's fine for app development but wouldn't be good enough for certain server use cases, therefore I propose to add `NIOSingleton`s as an _option_ to the user. Confident programmers (especially in libraries) are still free and encouraged to manage all the resources deterministically and explicitly. Companion PRs: - apple/swift-nio-transport-services#180 - swift-server/async-http-client#697 ### Modifications: - Add the `NIOSingletons` type - Add `MultiThreadedEventLoopGroup.singleton` - Add `NIOThreadPool.singleton` ### Result: - Easier use of NIO that requires fewer parameters for users who don't require full control. - Helps with #2142 - Fixes #2472 - Partially addresses #2473
The NIO ecosystem suffers from one composability issue:
EventLoopGroupProvider
and similar constructs. The issue is that takingEventLoopGroupProvider
(or similar) means that a library can either own or not own anEventLoopGroup
.That doesn't sound too bad but unfortunately, it's very bad for the shutdown case.
A library (like say AsyncHTTPClient) would obviously want to have a method called
thing.shutdown() -> EventLoopFuture<Void>
to match the other functions. Unfortunately, that can't be done because the library may own theEventLoopGroup
, thereforeshutdown()
has to may have to shut it down which then however means that there is no moreEventLoop
left to run the future on.The current hack is to instead provide a
shutdown(group: DispatchGroup, _ completion: @escaping (Error?) -> Void)
method. But that's both ugly (doesn't fit the rest) and wasteful (needs to spin up a dispatch thread for no reason).Unfortunately, this issue won't just magically be fixed by the universal adoption of Swift Concurrency either. Yes, it's possible to provide a
func shutdown() async throws -> Void
but that'll then bounce from a Concurrency executor to the EventLoop to a DispatchQueue thread back to some Concurrency executor. That's still not great.Also, there's another issue if
EventLoop(Group)
s frequently go away: What do we do with people who caught a reference to an EL(G) and then (after the shutdown)execute
stuff on it? Currently we print this weird warning that we'll crash in future releases. That's cool if EL(G)s are mostly global/created in main but it really doesn't work if they're frequently created/destroyed.Also there's a question w.r.t. Custom Executors (once they actually arrive in Swift): Will they support this use case?
My recommendations are:
EventLoopGroupProvider
HTTPClient
) creates its own EL(G) threadsIf we did deprecate
EventLoopGroupProvider
and the whole pattern, what should users do who don't already have an EL(G)? This will also become even more prevalent in the future where every user-facing API will be Swift Concurrency?The only solution I can come up with is for either SwiftNIO itself or every library lazily creating an internal ELG that will never be shut down. In other words: A global EL(G) singleton that gets created on first use, much like Dispatch or Swift Concurrency work. From a resource perspective and also alignment with Swift Concurrency it might actually make sense if SwiftNIO owned that singleton EL(G). That'd mean the new pattern to replace
EventLoopGroupProvider
would be.shared(elg)
or.global
.The alternative to SwiftNIO owning one global ELG would be ofc for every library owning its own global ELG that could maybe just have 1 thread. I.e. AsyncHTTPClient would internally have a singleton ELG with just one thread.
What do you all think about this? None of this is fully baked but I'm really quite sure that the
EventLoopGroupProvider
(and similar) pattern is really bad.The text was updated successfully, but these errors were encountered: