-
-
Notifications
You must be signed in to change notification settings - Fork 218
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
Async Signals #1043
base: master
Are you sure you want to change the base?
Async Signals #1043
Conversation
c5893d7
to
e9838b5
Compare
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1043 |
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.
Thanks a lot, this is very cool!
From the title I was first worried this might cause many conflicts with #1000, but it seems like it's mostly orthogonal, which is nice 🙂
I have only seen the first 1-2 files, will review more at a later point. Is there maybe an example, or should we just check tests?
2877010
to
9687f3b
Compare
I am currently testing it with my project.
|
i'd guess it's related to using |
Shouldn't the hot-reload hack only leak memory? 🤔 @jrb0001 does the segfault occur on every hot-reload? |
I am not completely sure yet. It doesn't happen if there are no open scenes or if none of them contains a node which spawns a Future. It also doesn't seem to happen every single time if I close all scenes and then open one with a Future before triggering the hot-reload. In this case it panics with some scenes:
With another scene it segfaults in this scenario. Simply reopening the editor (same scene gets opened automatically) and then triggering a hot-reload segfaults for both scenes. With both executor + Future from this PR, the hot-reload issue doesn't happen at all?!? So the issue could also be in my code, let me debug it properly before you waste more time on it. I will do some more debugging later this week (probably weekend). I also finished testing the Future part of the PR and it works fine with both my old executor and your executor in my relatively simple usage. Unfortunately all my complex usages (recursion, dropping, etc.) need a The |
9687f3b
to
23179c6
Compare
Yeah, it's completely unnecessary now. Probably an old artifact. I removed the bound.
Can you elaborate what the issue here is? I'm also curious what your use-case for the |
@jrb0001 Do you have an idea what could have triggered this? The only thing that I can think of is that a waker got cloned and reused after the future resolved. The panic probably doesn't make any sense, since the waker can technically be called an infinite number of times. 🤔 |
071c97e
to
c58b657
Compare
@Bromeon I now added a way to test async tasks. I still need to deal with panics inside a |
c58b657
to
a406977
Compare
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've finally had some time to look more closely at this. Thanks so much for this great PR, outstanding work as always ❤️
Technically, we could unify the test execution of sync and async tasks, but I get the impression that it also would have some downsides. Keeping it separate adds a bit of duplication, but unifying it would force more complexity onto the execution of sync tasks.
I think you made the right choice here, it seems they're different enough to be treated differently. If it becomes bothersome in the future, we could always revise that decision; but I think keeping the sync tests simple is a good approach.
20a53b7
to
af7d58b
Compare
My experience seems to be the exact opposite of yours. Usually things like sockets and channels return With Godot this isn't only caused by intentionally disconnecting a signal, but also when a node is freed, which can happen at any time and on a large scale. I don't like the idea of having hundreds or maybe even thousands of stuck tasks after the player changed scenes a few times. I also think we shouldn't compare it to gdscript, for two reasons:
Your I unfortunately didn't get to do my debugging session due to sickness. I will let you know once I have some results, but that will most likely be towards the end of the week or even weekend. |
Thanks a lot for the detailed insights, @jrb0001 👍 I'm trying to see it from a user perspective. A user would then have to make a choice whether the basic future is enough or the guaranteed one is needed, which may be... not a great abstraction? How would you advise a library user to choose correctly here, without needing to know all the details? Does the choice even make sense, or should we sacrifice a bit of ergonomics for correctness? |
I get this point, but I wouldn't say the future gets stuck intentionally. If you create a Godot Object and don't free it, then it leaks memory. That is also not intentional. From my point of view, async tasks must be stored and canceled before freeing the Object, this is simply an inherited requirement from the manually managed I also think making the |
43b167c
to
766bc95
Compare
But isn't manually cancelling extends Button
func _pressed():
await get_tree().create_timer(1.0).timeout
print("Pressed one second before!") If the button got freed, the call simply drops without any cleanup code. But with your proposal we need to store all Small nitpick, but i disagree on naming it |
From the discussion, it's stated that the "guaranteed" future is less ergonomic to use than the regular one. At the same time, it seems like the regular one needs manual cleanup (thus being less ergonomic in its own way). To be on the same page, could someone post similar usage examples for each of them? 🙂 |
Since The reason I say this is because, user's might accidentally use |
@coder137
No, it happens as soon as the signal object is freed. Of course, if you wait for a signal until the game shutdown, this could also happen during shutdown. But during shutdown, all pending tasks are also being canceled and dropped before the engine cleans up the scene tree, so it's unlikely to happen. |
This is still the same behavior with the current commit, both the printed panic and the segfault.
So I guess there is a Another issue I am currently running into is that |
Gut feeling for the callable is that Godot is not reloading the custom callable and tries to access the invalid pointer after unloading the old library. I can look into this more.
Yes, this is intentional at the moment. There was the issue that signal futures inside an async block or function would only get created after the deferred initial poll from the What exact issue is it causing? |
I am calling With my old executor, I also abused async blocks without any awaits as a generic "call deferred" solution. Simply because it was easier than messing around with |
Can't you do something like this? let base = self.base_mut();
let handle = godot_task(...);
drop(base);
self.task_handles.push(handle); Unfortunately, I wasn't able to replicate the hot-reload issue on macOS yet. It might be Linux-specific. EDIT: can't replicate it in CI either. |
Yes, that worked. Thanks for the idea!
I think it is more likely something specific to my setup. I can't reproduce it with all my scenes, some just print panics without segfaulting. But for the affected scenes, it is 100% reproducible. I will try it in a clean project. |
Minimalistic repro project: executor-segfault.zip cd rust
cargo build --target x86_64-unknown-linux-gnu
$GODOT4_BIN --editor --path ../project/ &
# Wait until project is fully loaded.
touch game/src/lib.rs
cargo build --target x86_64-unknown-linux-gnu
# Focus editor window --> segfault. If you are not on linux, then just change the So it looks like this is caused by calling the |
e4d5aca
to
1f9069b
Compare
@jrb0001 it looks like your issue comes down to calling
But on linux in CI it works perfectly fine (perhaps the timing is slightly different?). I think you can create a new issue for this edge-case of deferred calling callables during deinitialization. |
1f9069b
to
6d1fa4a
Compare
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.
Thanks again for the continuous effort. Another review iteration 🙂
Some organizational questions:
-
This PR adds a ton of functionality (1.2 kLOC and counting). I think at this point it makes sense to have a dedicated module instead of spilling all into
godot::builtin
.
What aboutgodot::task
, similar to the previousgdnative::tasks
? We can always move it around if we find it fits better into another module, but that's much easier once it is its own module already. -
godot_task
should probably be called something that contains "spawn", just like with all other async runtimes andstd::thread
. Maybe even justspawn
. Another reason for the module, it could then be qualified asgodot::task::spawn(...)
. -
TrySignalFuture
-- would it make sense to name itFallibleSignalFuture
or so? A bit longer, but the type isn't named very often, and this seems to be the not-so-common case if I understood correctly.
Yes, I think this is a great idea.
I like this in combination with 1!
I was thinking about |
96f8c09
to
a3788ac
Compare
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 was thinking about
FallibleSignalFuture
as well, but it seemed a bit long. I'm fine with this name too.
Most of the time, people wouldn't name it directly, right? It may still appear as IDE hint on declarations, but even then, people may often directly .await
on it...
I fixed the CI issues btw, the Godot-stable artifacts expired.
93e008a
to
c0b01e6
Compare
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.
Thanks a lot! I also just checked the docs, the task
module looks super clean!
Added some more comments, but really just small things this time.
This starts to look very good -- if no other concerns are brought up, I'd suggest merging it on the weekend 🚀
30411fe
to
e5b215b
Compare
This has been developed last year in #261 and consists of two somewhat independent parts:
Signal
: an implementation of theFuture
trait for Godots signals.The
SignalFuture
does not depend on the async runtime and vice versa, but there is no point in having a future without a way to execute it.For limitations see: #261 (comment)
Example
TODOs
GuaranteedSignalFuture
. Should it be the default? (We keep it asTrySignalFuture
, the plain signal is a wrapper that panics in the error case.)CC @jrb0001 because they provided very valuable feedback while refining the POC.
Closes #261