-
-
Notifications
You must be signed in to change notification settings - Fork 219
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
[WIP] Async Signals #1043
base: master
Are you sure you want to change the base?
[WIP] 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.
godot-core/src/builtin/signal.rs
Outdated
/// The guaranteed signal future will always resolve, but might resolve to `None` if the owning object is freed | ||
/// before the signal is emitted. | ||
/// | ||
/// This is inconsistent with how awaiting signals in Godot work and how async works in rust. The behavior was requested as part of some | ||
/// user feedback for the initial POC. | ||
pub struct GuaranteedSignalFuture<R: FromSignalArgs> { | ||
state: Arc<Mutex<(GuaranteedSignalFutureState<R>, Option<Waker>)>>, | ||
callable: GuaranteedSignalFutureResolver<R>, | ||
signal: Signal, | ||
} |
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.
Please keep the "brief" part limited to 1 line and as concise as possible, so that it fits into module-level doc overviews.
The behavior was requested as part of some feedback for the initial POC"
This is too vague, and makes it very hard for readers to track those reasons down. Could you instead document the design rationale here? You can also include a link to this PR, but generally, the information should be self-contained.
Capitalization nitpick: "Rust" is always uppercase.
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 definitely needs to be revised, but I would like to first determine if we really want to keep it. Personally, I don't see a real-world use case for it. @jrb0001 requested it, so I'm currently waiting for more feedback from them (See #1043 (comment)). But please let me know if you have an opinion on 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.
Agreed. Also, for new features, it's often better to start minimal and then extend over time, once there's real user demand.
But let's see what @jrb0001 has to say 🙂
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? 🙂 |
ExamplesSignalFuturelet node = Node::new_gd();
// might never complete and should be manually canceled to avoid memory leak.
godot_task(async move {
let children = node.get_children();
// might never resolve if the node gets freed before the signal is emitted.
let _: () = Signal::from_object_signal(&node, "tree_entered").to_future().await;
children.iter_shared().for_each(|child| ... );
}); GuaranteedSignalFuturelet node = Node::new_gd();
// will always complete.
godot_task(async move {
let children = node.get_children();
// should always resolve.
let Some(_): Option<()> = Signal::from_object_signal(&node, "tree_entered").to_guaranteed_future().await else {
// The singnal object was freed.
return;
};
children.iter_shared().for_each(|child| ... );
}); SignalFuture + panic (not yet implemented)let node = Node::new_gd();
// always completes but might panic if the node is freed before the signal is emitted.
godot_task(async move {
let children = node.get_children();
// might panic. The async runtime will catch the panic, print it and discard the task.
let _: () = Signal::from_object_signal(&node, "tree_entered").to_future().await;
children.iter_shared().for_each(|child| ... );
});
You can't compare GDScript and Rust like this. In GDScript has a script runtime that directly integrates with the In Rust, both the
In the GDScript runtime, they store the pending function states inside the owning script and cancel them when the script is destroyed. The closest we can get to something like that, is to store
I like the name, but the impression you get from the current name is what it does, it always resolves, but it might resolve to |
766bc95
to
14a7148
Compare
Was going through the PR since it was posted in the discord channel.
fn to_future<R>() -> impl Future<Output = Option<R>> + 'static {
// Since we have a FnMut requirement we cannot use oneshot channels here
// tokio channels are just an example here (we can use any channel that gives us sync tx and async rx)
let (tx, rx) = tokio::sync::mpsc::channel(1);
let callable = Callable::from_local_fn("SignalFuture::resolve", move |_args| {
let _ignore = tx.blocking_send(R::from_args(_args));
Ok(Variant::nil())
});
async move {
rx.recv().await
}
} I believe this might solve the problem with futures getting resolved or no, since the channels will get cleaned up even if the signal isn't fired. We won't have to worry about leaks as long as the executor/async runtime shuts down gracefully. Since we are using existing channel implementations, we have lesser technical debt in godot_core as well.
Please let me know what you'll think |
After a bit of testing, i found minor bug with Codeuse std::future::Future;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use futures_util::future::{select_all, FutureExt as _};
use godot::classes::{Control, IControl};
use godot::prelude::*;
struct TestAsync;
#[gdextension]
unsafe impl ExtensionLibrary for TestAsync {}
#[derive(GodotClass)]
#[class(init, base = Control)]
struct NodeTestAsync {
base: Base<Control>,
}
#[godot_api]
impl IControl for NodeTestAsync {
fn ready(&mut self) {
let this = self.to_gd();
let signals = (0..this.get_child_count())
.filter_map(|i| Some(Signal::from_object_signal(&this.get_child(i)?, "pressed")))
.collect::<Vec<_>>();
// Waits all child buttons and reports if they're being pressed.
godot_task(AssertUnwindSafe(async move {
fn wait_for_signal(
i: usize,
s: &Signal,
) -> Pin<Box<dyn '_ + Future<Output = Option<usize>>>> {
// Without fuse, to_guaranteed_future hangs
Box::pin(
async move {
println!("Wait: {i}");
s.to_guaranteed_future::<()>().await;
println!("Done: {i}");
Some(i)
}
.fuse(),
)
}
let mut futs = signals
.iter()
.enumerate()
.map(|(i, s)| wait_for_signal(i, s))
.collect::<Vec<_>>();
println!("Start");
while !futs.is_empty() {
let i;
(i, _, futs) = select_all(futs).await;
if let Some(i) = i {
println!("{i}");
futs.push(wait_for_signal(i, &signals[i]));
}
}
}));
}
}
For ergonomic reason, we should reflect GDScript's convention as much as possible. Manually managing handles is unusual even for other async runtimes like Tokio. We should be able to spawn a new task and forget about it, similiar to daemon thread. Shutdown sequence can be done like what Tokio did, using cancellation token to signal every outstanding tasks that we need to do cleanup. The runtime can then loops until all tasks finished.
My idea is to return |
@coder137 to address your comment:
We could, but that would be more overhead than using the engine. It would also require an additional dependency, while Godot already provides all the necessary components.
This does exactly the same thing as the
And the same applies to the current state of this PR. The problem is with the
We can do that, but I would like to provide a way to execute futures without requiring users to include an external dependency. I also don't see what it would solve right now.
Thanks for the report, I will see what is going on there.
Yes, and I'm all for that, as long as it's technically possible.
The issue we are discussing has nothing to do with the runtime. If you use the Side note: since GDScript only cleans up pending function states when a
I see, yes that is an alternative that would be more descriptive. |
14a7148
to
aee81dd
Compare
@Dheatly23 After some recent refactoring, the async move {
println!("Wait: {i}");
s.to_guaranteed_future::<()>().await;
println!("Done: {i}");
Some(i)
} This kind of code is quite reckless, as it treats "resolved because signal fired" and "resolved because signal object was freed" as one and the same thing. |
Oh, i forgot to add With regards to my original complaint, i think it should be resolved by making I disagree on making let this = self.to_gd();
godot_task(async move {
// Do other tasks, wait for signal, etc.
// Access object, if object does not exist it should panics.
this.bind();
}) I quite like the @coder137 suggestion of decoupling signal future and async runtime. So user can essentially bring-your-own-async-runtime (Tokio, |
As I wrote in an earlier comment, I do agree with this and I think it could make sense to panic the future if the signal object is freed. The panic would be printed to
the let this = self.to_gd();
let _ = godot_task(async move {
// Do other tasks, wait for signal, etc.
// Access object, if object does not exist it should panics.
this.bind();
}); The intention was that it would highlight that the task handle can be of importance. And hopefully, people would make an informed decision on what they want to do with it. But making it
They are already decoupled. You should be able to use the futures with any runtime you like. |
714ab59
to
933d17b
Compare
933d17b
to
ff96707
Compare
After all the discussions about the futures in this PR, I have now made the following refactoring:
|
To understand this better, under what conditions would we receive In the PR I see it says if the Signal object is freed before the signal was emitted. Wouldn't the signal future also get freed in that case? |
@coder137 See the |
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