-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Virtual async events 2 #12372
Virtual async events 2 #12372
Conversation
This is going to need #12371 as a prerequisite. The test suite will fail without that fix. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some quick comments for now.
@@ -924,8 +957,8 @@ when defined(windows) or defined(nimdoc): | |||
## receiving notifications. | |||
registerWaitableEvent(fd, cb, FD_WRITE or FD_CONNECT or FD_CLOSE) | |||
|
|||
template registerWaitableHandle(p, hEvent, flags, pcd, timeout, | |||
handleCallback) = | |||
proc registerWaitableHandle(p: PDispatcher, hEvent: Handle, flags: DWORD, pcd: PostCallbackDataPtr, timeout: int, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice :)
lib/pure/asyncdispatch.nim
Outdated
VirtualFD* = distinct int | ||
|
||
VirtualAsyncEventImpl = object | ||
triggered: bool | ||
when compileOption("threads"): | ||
eventLock: RLock | ||
p: PDispatcher | ||
vFD: VirtualFD | ||
cb: Callback | ||
VirtualAsyncEvent* = ptr VirtualAsyncEventImpl | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are defined twice. Can you reduce the duplication?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly no. I tried.
TLDR; It has a type dependency loop that forces the duplication.
It might be possible with {.experimental: "codeReordering".}
, but here is the problem:
First, the VirtualAsyncEventImpl
keeps a reference to a pDispatcher
here:
Nim/lib/pure/asyncdispatch.nim
Line 292 in 264bfee
p: PDispatcher |
and here:
Nim/lib/pure/asyncdispatch.nim
Line 1143 in 264bfee
p: PDispatcher |
Which requires VirtualAsyncEventImpl
must be declared after PDispatcher
.
But,
'VirtualEventDispatcherkeeps references to
VirtualAsyncEvent` here:
Nim/lib/pure/asyncdispatch.nim
Line 262 in 264bfee
virtualHandles: Table[VirtualFD, VirtualAsyncEvent] # pseudo handles for custom AsyncEvents. |
and here:
Nim/lib/pure/asyncdispatch.nim
Line 1153 in 264bfee
virtualHandles: Table[VirtualFD, VirtualAsyncEvent] # pseudo handles for custom AsyncEvents. |
And PDispatcher
keeps a reference to VirtualEventDispatcher
here:
Nim/lib/pure/asyncdispatch.nim
Line 259 in 264bfee
vd: VirtualEventDispatcher |
and here:
Nim/lib/pure/asyncdispatch.nim
Line 1150 in 264bfee
vd: VirtualEventDispatcher |
Which requires VirtualAsyncEventImpl
must be declared before PDispatcher
.
It's a type dependency loop! The only way this is allowed, is if both VirtualAsyncEventImpl
and PDispatcher
are declared in the same type definition block.
Because of the when defined(windows):
split,
PDispatcher
is declared twice, so VirtualAsyncEventImpl
must also be declared twice.
Edit: I got the reason why this exists wrong. It has nothing to do with the lazy loading stuff. (Sorry, it's a complicated PR and it's been a few days. Hard to keep it all in my head.)
Even simpler:
The VirtualEventDispatcher
needs to hold the physical AsyncEvent
(it needs the physical event to multiplex on), and the PDispatcher
needs to hold a VirtualEventDispatcher
. So all three types rely on each other, and must be declared in the same type block.
If you declare the VirtualEventDispatcher
first, then AsyncEvent
hasn't been defined yet, and VritualEventDispatcher
will error.
If you declare AsyncEvent
first, then VirtualEventDispatcher
hasn't been declared yet, and PDispatcher
will error (note: that PDispatcher
and AsyncEvent
are declared together in the same type block).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dom96,
Sorry :/
This is a terrible and convoluted explanation. Hit me up on irc if you can. I might be able to explain it better by walking through it with you live.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Araq, I haven't been able to catch you on irc the last few days. But I would like to get your thoughts on this type dependency nonsense. I don't see a way around it, but maybe an expert(TM) can help :-P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I talked to @Araq briefly about this on IRC. He didn't look too hard, but generally this kind of thing will happen in x-platform situations until we get recursive module definitions. So it's a necessary evil for now.
lib/pure/asyncdispatch.nim
Outdated
@@ -1851,6 +1907,105 @@ proc send*(socket: AsyncFD, data: string, | |||
|
|||
return retFuture | |||
|
|||
proc deInitVirtualEventDispatcher(p: PDispatcher) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be called close
and take a VirtualEventDispatcher
.
lib/pure/asyncdispatch.nim
Outdated
withLockIfThreads ev.eventLock: | ||
if ev.triggered: | ||
ev.triggered = false | ||
p.callbacks.addLast(vcbFactory(vfd)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really want initVirtualEventDispatcher
to take a VirtualEventDispatcher
object, and not a PDispatcher
object.
The main reason I need the PDispatcher
here is because I can think of no other way to add a virtual callback into Dispatcher callback queue.
I could have VirtualEventDispatcher
maintain a separate dispatch queue. But that could cause all kinds of problems, because callback order matters. As seen here for example: #7197
It is just much safer to insert the callback into the parent PDispatcher
queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you use getGlobalDispatcher?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. Two reasons:
1.) It's not thread safe.
2.) Related to 1, just like physical AsyncEvents
, VirtualAsyncEvents
are not assigned a dispatcher by default. They can technically be assigned to different dispatchers.
The API allows for this, so it has to be accounted for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify this further:
It's ok to use getGlobalDispatcher()
on creation. see:
Nim/lib/pure/asyncdispatch.nim
Line 1983 in 264bfee
proc addEvent*(ev: VirtualAsyncEvent, cb: Callback) = |
But the trigger event may be called from a different thread with a completely different dispatcher!
In that case, we need a way to trace back to the owning PDispatcher
instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed my mind. I thought about this more, and getGlobalDispatcher
is thread safe (it returns a thread local global.)
I changed the code to use getGlobalDispatcher
and removed the closure requirement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More comments, I like this approach overall much better than the previous, so I think we'll be able to get this merged soon :)
@@ -0,0 +1,27 @@ | |||
discard """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it would make sense to put all these tests into a virtualevent
subdirectory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be cool, does testament support this?
try: | ||
runForever() | ||
except ValueError: | ||
echo "runForever should throw ValueError, this is expected" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this expected? Add a comment in code to explain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I stole it from here (where it also doesn't have a comment to explain):
https://github.com/nim-lang/Nim/blob/devel/tests/async/t8982.nim
@narimiran is who you need to yell at for not commenting their code! /rant :-P
The answer is that runOnce()
will throw an exception when it has no more pending operations:
Nim/lib/pure/asyncdispatch.nim
Line 361 in 1e5a071
raise newException(ValueError, |
This is basically a hacky alternative to drain()
Remember I discussed this in this comment: #12371 (comment)
This test was the reason I started looking into drain()
in the first place, and discovered #12371
Once #12371 is merged I think I can refactor this test to use drain()
|
||
proc close*(ev: VirtualAsyncEvent) = | ||
## Closes event ``ev``. | ||
doAssert(ev.vFD != InvalidVirtualFD, "Must unregister Event before you close it!") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC for pretty much all other things in asyncdispatch, the close
proc will implicitly unregister the FD. Please keep that consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this is safer than what happens in other places in asyncdispatch.
If you look at AsyncEvent
, the following happens:
It calls close
on the underlying ioselector
, which closes it at the kernel level, and dealloc's the memory, but it doesn't actually unregister the FD from the select dispatcher:
Nim/lib/pure/asyncdispatch.nim
Line 1546 in 233455a
proc close*(ev: AsyncEvent) = |
Nim/lib/pure/ioselects/ioselectors_epoll.nim
Line 104 in 233455a
proc close*[T](s: Selector[T]) = |
Maybe that is a bug?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sigh
Yeah, it's not consistent with sockets at all then...
Nim/lib/pure/asyncdispatch.nim
Lines 1320 to 1334 in 5288b54
proc closeSocket*(sock: AsyncFD) = | |
let selector = getGlobalDispatcher().selector | |
if sock.SocketHandle notin selector: | |
raise newException(ValueError, "File descriptor not registered.") | |
let data = selector.getData(sock.SocketHandle) | |
sock.unregister() | |
sock.SocketHandle.close() | |
# We need to unblock the read and write callbacks which could still be | |
# waiting for the socket to become readable and/or writeable. | |
for cb in data.readList & data.writeList: | |
if not cb(sock): | |
raise newException( | |
ValueError, "Expecting async operations to stop when fd has closed." | |
) |
# send the signal to wake up the dispatcher thread. | ||
trigger(ev.vd.virtualMuxHandle) | ||
|
||
proc addEvent*(ev: VirtualAsyncEvent, cb: Callback) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Argh, I know you're being consistent with the AsyncEvent API here, but it annoys me that this API isn't consistent with the socket API...
In any case, it might be worth to add a signal
async proc that works for both VirtualAsyncEvent
and AsyncEvent
. We aim to avoid callbacks after all...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is arguably a lower level api that you wouldn't use in async level code.
The FlowVar -> Promise integration for example. A user never actually sees AsyncEvent
.
IDK if it's necessary, but it might be a nice convenience to have some api that wraps a AsyncEvent
/ VirtualAsyncEvent
in a Promise for you.
I'd rather focus on getting the fundamentals in and making that a separate PR, since I consider that sugar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's important to think about how the API will evolve, remember that once this is released we won't be able to change it in a breaking manner.
# The main reason we need this is to make hasPendingOperations still work | ||
# without modifying the ioselector code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you elaborate on this? Don't really understand why this lazy loading is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has to do with hasPendingOperations()
and the multiplexer io select
object.
The simplest approach is to simply create a singleton AsyncEvent
object attached to the VirtualEventDispatcher
, which you initialize on creation.
This AsyncEvent
essentially lives forever and waits for VirtualEvents
to trigger it.
This has one big problem. The lives forever part. The io selector
is never closed. It must remain alive. This means the underlying ioselector implementation will always have at least one open io select
that it is listening on.
If any code relies on all io selects
being closed before continuing, that code will never be able to run. hasPendingOperations()
and any code that relies on it is a prime example of code like this.
In #12232 I "fixed" this problem by modifying all of the io_selector implementations and asyncdispatch.nim
to treat a single open io select
as the same as 0 open io selects
(no pending events).
After thinking about it, I realized that has approach has big problems:
-
First, it is a very dramatic and invasive change to the ioselector implementations. That code is foundational, so I want to be very careful with changes that I make there. The changes in Virtual async events #12232 seemed too big, and not safe.
-
Second, It's possible that code might rely on the process actually having all physical kernel
io selectors
closed (for a graceful shutdown or resource management for example). If we treat 1 openio selector
the same as 0, we are essentially lying to the user, and that is not ok IMO.
I don't love lazy loading, because it is non-deterministic, but in this case, I think it's the right tool for the job. This approach is a modular solution to all of the above problems:
- It works with 0 changes to the underyling
ioselector
code - It works with 0 changes to the core asyncdispatch code.
- By core I mean no changes to
AsyncEvent
or any core run loop code such asrunOnce()
- By core I mean no changes to
hasPendingOperations()
works correctly with 0 changes and without lying to the user- All changes are contained within the
VirtualEvent
additions.
All of that adds up to make this a much safer PR.
lib/pure/asyncdispatch.nim
Outdated
proc initVirtualEventDispatcher(p: PDispatcher) = | ||
p.vd.virtualMuxHandle = newAsyncEvent() | ||
|
||
proc virtualMuxCB(fd: AsyncFD): bool {.closure,gcsafe.} = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
proc virtualMuxCB(fd: AsyncFD): bool {.closure,gcsafe.} = | |
proc nativeEventCB(nativeFD: AsyncFD): bool {.closure, gcsafe.} = |
lib/pure/asyncdispatch.nim
Outdated
|
||
proc vcbFactory(vfd: VirtualFD) : proc() {.closure, gcsafe.} = | ||
let ev = p.vd.virtualHandles[vfd] | ||
result = proc() {.closure,gcSafe.} = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: move the proc () ...
down to the next line and indent it.
lib/pure/asyncdispatch.nim
Outdated
# add them to the callback list. | ||
# Not very efficient, but requires the least coordination between threads. | ||
|
||
proc vcbFactory(vfd: VirtualFD) : proc() {.closure, gcsafe.} = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
proc vcbFactory(vfd: VirtualFD) : proc() {.closure, gcsafe.} = | |
proc vcbFactory(vfd: VirtualFD): proc() {.closure, gcsafe.} = |
But also, I would probably try to avoid this being a closure (and use getGlobalDispatcher if possible like suggested below).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as I mention here: #12372 (comment)
it's not thread-safe or consistent with the API to use getGlobalDispatcher
.
A closure is the easiest way to get access to the correct dispatcher given those constraints.
As a general note:
getGlobalDispatcher
is not appropriate for a threaded environment. You cannot assume there is only a single asyncdispatcher
in a threaded application. IMO this might cause a lot of problems in the future.
lib/pure/asyncdispatch.nim
Outdated
@@ -1851,6 +1909,105 @@ proc send*(socket: AsyncFD, data: string, | |||
|
|||
return retFuture | |||
|
|||
proc close(vd: VirtualEventDispatcher) = | |||
assert vd.virtualHandles.len == 0, "Cannot de-Init Virtual Event Dispacter. There are still Pending Events." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
## | ||
## New ``VirtualAsyncEvent`` object is not automatically registered with | ||
## dispatcher like ``AsyncSocket``. | ||
result = cast[VirtualAsyncEvent](allocShared0(sizeof(VirtualAsyncEventImpl))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can't get around this being shared?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's meant for multithreading so no :-P
to be more specific:
the trigger
proc for VirtualAsyncEvent
is meant to be callable from other threads by design. So this is shared memory by design.
…vent As suggested by @dom96. This leaves the original AsyncEvent api intact. Also includes a small test suite for both AsyncEvent and VirtualAsyncEvent
- changed deInitVirtualEventDispatcher(p: PDispatcher) to close(vd: VirtualEventDispatcher) - changed VirtualEventDispatcher to a ref object and cleaned up some code related to this change
3b05eab
to
2fabbb9
Compare
This pull request has been automatically marked as stale because it has not had recent activity. If you think it is still a valid PR, please rebase it on the latest devel; otherwise it will be closed. Thank you for your contributions. |
This pull request has been automatically marked as stale because it has not had recent activity. If you think it is still a valid PR, please rebase it on the latest devel; otherwise it will be closed. Thank you for your contributions. |
This pull request has been automatically marked as stale because it has not had recent activity. If you think it is still a valid PR, please rebase it on the latest devel; otherwise it will be closed. Thank you for your contributions. |
Implemented VirtualAsyncEvent separate from AysncEvent
As suggested by @dom96. This leaves the original AsyncEvent API intact.
This also includes a small test suite for both AsyncEvent and VirtualAsyncEvent.
The code is mostly the same as #12232 except that it uses a single AsyncEvent as the multiplexer, instead of reaching into the global ioselector directly.
Pros:
Cons:
A Few Notes:
I still think the original AsyncEvent is inefficient and has some issues.
A very rough performance measure of the test suite shows that VirtualAsyncEvent is much faster than the old AsyncEvent, as well as being able to handle an order of magnitude more events than the old AsyncEvent. 😛
Knowledgeable users will be able decide which version they want to use.
Unlike the previous version, Make Flowvar compatible with Async #11724 will no longer transparently work.
Calling @zevv, @dom96, @Araq, @cheatfate for code review and thoughts.