-
-
Notifications
You must be signed in to change notification settings - Fork 271
Async Signals #1043
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
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 |
Bromeon
left a comment
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
Bromeon
left a comment
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? 🙂 |
e5b215b to
3c9bc50
Compare
|
Nice! This seems to work ❤️ |
|
@AsbjornOlling thanks for pointing this out. It was indeed not working as intended. |
|
It seems that let task_handle = task::spawn(async move {
let (ret,) = self.signals().active_card_ability().deref().to_future().await;
});error[E0599]: the method `to_future` exists for reference `&TypedSignal<'_, CardManager, (Gd<AbilityContext>,)>`, but its trait bounds were not satisfied
--> src\class\card_manager.rs:211:71
|
211 | let (ret,) = self.signals().active_card_ability().deref().to_future().await;
| ^^^^^^^^^ method cannot be called due to unsatisfied trait bounds
|
::: ..\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\library\core\src\cell.rs:311:1
|
311 | pub struct Cell<T: ?Sized> {
| -------------------------- doesn't satisfy `Cell<*mut __GdextClassInstance>: Sync`
|
= note: the following trait bounds were not satisfied:
`*mut card_ability_context::AbilityContext: Sync`
which is required by `(godot::prelude::Gd<card_ability_context::AbilityContext>,): Sync`
`Cell<*mut __GdextClassInstance>: Sync`
which is required by `(godot::prelude::Gd<card_ability_context::AbilityContext>,): Sync`
`*mut card_ability_context::AbilityContext: Send`
which is required by `(godot::prelude::Gd<card_ability_context::AbilityContext>,): Send`
`*mut __GdextClassInstance: Send`
which is required by `(godot::prelude::Gd<card_ability_context::AbilityContext>,): Send`
|
This comes from the pub struct SignalFuture<R: ParamTuple + Sync + Send>(FallibleSignalFuture<R>);Those bounds are unnecessary for signals that are emitted on the main thread. (Awaiting must anyway happen on the main thread). Was the intention here to support also signals emitted on other threads, as a cross-thread communication mechanism? If yes, we should probably add this later -- might need more thought regarding thread safety, and probably some version of #18. |
Yes, it's basically impossible to tell where a signal will be emitted, since any signal can be emitted from any thread. We also use a For |
3c9bc50 to
43a1419
Compare
Bromeon
left a comment
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 so much for this massive feature, @TitanNano!
The current implementation seems solid enough to be merged. Hopefully this allows more people to test it out -- thanks also to everyone who has voiced their concerns in this thread, I encourage you to open new discussions where appropriate 🙂 regarding the Gd issue, I definitely think this should be discussed, but such changes can happen in follow-up PRs.
🚀
43a1419 to
8f4122e
Compare
|
Thanks, everyone, for the great and productive feedback. |
|
Do we have any examples of how to use this? I tried using it, but I get an error saying there is no tokio runtime and that a tokio runtime is needed. |
The integration tests added in this PR should give you an idea 🙂 |
This means you are using a crate that depends on Tokio. Godot-rust does not use Tokio. You can either look for a crate that does not require Tokio or use the |
This has been developed last year in #261 and consists of two somewhat independent parts:
Signal: an implementation of theFuturetrait for Godots signals.The
SignalFuturedoes 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