-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Add Result
handling to Commands
and EntityCommands
#17043
Add Result
handling to Commands
and EntityCommands
#17043
Conversation
commit b6c4d28 Author: JaySpruce <jsprucebruce@gmail.com> Date: Sun Dec 29 17:17:44 2024 -0600 ci docs commit 492e558 Author: JaySpruce <jsprucebruce@gmail.com> Date: Sun Dec 29 16:59:31 2024 -0600 messed up the conflict resolve commit 74592fa Merge: e0c6d65 0f2b2de Author: JaySpruce <jsprucebruce@gmail.com> Date: Sun Dec 29 16:48:37 2024 -0600 Merge branch 'main' into refactor_hierarchy_commands commit e0c6d65 Author: JaySpruce <jsprucebruce@gmail.com> Date: Sun Dec 29 13:28:07 2024 -0600 refactor to remove structs
Pinging @alice-i-cecile as requested. Not totally polished, but I felt like this could use some feedback/cooperation in regards to other fallible stuff going on. |
/// | ||
/// # Note | ||
/// | ||
/// This won't clean up external references to the entity (such as parent-child relationships | ||
/// if you're using `bevy_hierarchy`), which may leave the world in an invalid state. | ||
#[track_caller] | ||
fn despawn(log_warning: bool) -> impl EntityCommand { | ||
fn despawn() -> impl EntityCommand<World> { | ||
#[cfg(feature = "track_change_detection")] |
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 change seems correct regardless, maybe we should split this out? Ditto the docs above.
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 think that would need the impl EntityCommand for impl FnOnce(EntityWorldMut)
changes. I do think that should be pulled out of this pr as that is something that could be considered separately.
I'm willing to eat a 5% (worst case) performance regression on Commands here. This is a huge usability problem with Bevy, and I'm convinced that we can't solve this without somehow regressing perf there. The big performance gains to be found here are in command batching anyways. |
I'd like to do a proper review of this, if I can find the time. Writing this as a reminder to myself. |
I agree with you, this approach makes a lot of sense. I do think we can find a more optimized solution though. Having proper error handling for commands is worth the 5% decrease in performance but I'm positive we can minimize it. I'll try to find time over the weekend to play around with this PR and give more based feedback. |
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 love the new module organization and the docs are straightforward and clear. Thank you so much; you've done a great job with this complex and important work!
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 don't have time to really look this over properly, so take this approval as you will. But this seems to be pretty much exactly what I was hoping 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.
I skimmed through the signatures of the new APIs, and I only have a couple small changes to suggest.
That said, you can otherwise consider this an approval.
fn with_error_handling( | ||
self, | ||
error_handler: Option<fn(&mut World, CommandError)>, | ||
) -> impl FnOnce(&mut World) + Send + 'static |
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 method strikes me as the sort of provided method which should return a wrapper struct, rather than an impl Trait
, like how Iterator
's provided methods do.
Even if we don't want to do a wrapper struct, the return type should be impl Command
, to line up with the documentation saying "Returns a new Command
".
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 tried impl Command
early on and it didn't work, but now it does. I guess I changed something that broke it. I'll do that real quick
/// footprint than `(Entity, Self)`. | ||
/// In most cases the provided implementation is sufficient. | ||
#[must_use = "commands do nothing unless applied to a `World`"] | ||
fn with_entity(self, entity: Entity) -> impl Command<(Result, CommandError)> |
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.
Same here as in my other comment - this strikes me as the sort of method which should probably return a wrapper struct (i.e. WithEntity<Self>
).
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 looked back over this, looking specifically at docs (since I mostly looked at the API structure on my other review), and found a couple small docs improvements that could be made.
} | ||
} | ||
|
||
impl<F> Command<Result> for F |
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.
Just so that I understand, Command
is only implemented for functions of the type
|&mut World| -> core::Result<(), Box<dyn core::Error>>
If I implemented my own custom Error enum, would
|&mut World| -> Result<(), MyError>
also implement Command?
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.
Nope, you would need to box it. Pretty sure ?
boxes things automatically if necessary. Unfortunate that the type gets erased, but I don't think there's a way around that
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 feel like we could add tests for the CommandOverride, or to have more examples on how the ErrorHandling would be used in practice.
(can be done in separate PRs as this is already huge)
I like the overall direction!
@JoJoJet points out in discord (https://discord.com/channels/691052431525675048/1325887475729698837/1325917241396564069) that we may want to use concrete error types if possible. |
I would also prefer concrete error types that get cast at the last minute if at all possible, but I don't feel that has to be done in this PR. |
As the person who recommended using boxes I would feel kind of bad to block on changing to yet something else. So personally I would push for doing that as future work. |
I only have a vague idea of how to implement that so this might not be true, but I think it would have to be less convenient for struct-based commands for that to work. I've tried to keep it so that, if you I don't know how much we still care about struct commands, I've just tried to avoid making them worse. If a decision-maker decides that they're not important, that's fine by me |
I think that we can overwhelmingly move away from struct-based commands :) All of our internal experiments to move away from them have gone smoothly. Even when you need to capture data / settings, closures work fine. I don't mind regressing their UX there. |
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 wrapped up my review. In general I think this is the right general direction but I think some things should change. I've put together a branch with fixes to my points below. I think we should merge this PR and then review and merge my PR (in reasonably quick succession to avoid churn for people following main).
- Command and EntityCommand always returning a
Result
is unnecessary and allows for a lot of weirdness. For example, this pr (likely accidentally) added a bunch of unnecessary error handlers to Commands that need no error handling. This could have been detected / disallowed if it was encoded in the type system. - As others have said, making commands generic on error type instead of boxing in-place is a good idea.
- I think we can make the EntityCommand and Command traits much simpler and remove the marker type. For EntityCommand, this hinges on moving to
EntityWorldMut
, which I strongly believe we should do anyway, as it allows batched entity commands to be applied with a single entity lookup, and it makes the declaration of most entity commands much simpler. - I'm not a fan of defining temporary error handlers on Commands. It adds more branching, makes Commands bigger / more expensive to initialize (note that we construct it at high frequencies / treat it like a pointer type), makes the code harder to follow, and introduces a bunch of additional functions. I think we should rely on a default error handler in
queue_fallible
in combination with callingcommands.queue(command.with_error_handler(handler))
directly on commands that need to opt out for some reason. This comes with the tradeoff of "command shorthand methods" on Commands only using the default error handler, but imo this is worth it.
My PR should be ready tomorrow. The branch works, but it needs a bit of polish + documentation.
I'm going to wait until your PR is up to merge this (to reduce breakage on main), but I'm considering this "ready-to-merge" :) |
# Objective Rework / build on #17043 to simplify the implementation. #17043 should be merged first, and the diff from this PR will get much nicer after it is merged (this PR is net negative LOC). ## Solution 1. Command and EntityCommand have been vastly simplified. No more marker components. Just one function. 2. Command and EntityCommand are now generic on the return type. This enables result-less commands to exist, and allows us to statically distinguish between fallible and infallible commands, which allows us to skip the "error handling overhead" for cases that don't need it. 3. There are now only two command queue variants: `queue` and `queue_fallible`. `queue` accepts commands with no return type. `queue_fallible` accepts commands that return a Result (specifically, one that returns an error that can convert to `bevy_ecs::result::Error`). 4. I've added the concept of the "default error handler", which is used by `queue_fallible`. This is a simple direct call to the `panic()` error handler by default. Users that want to override this can enable the `configurable_error_handler` cargo feature, then initialize the GLOBAL_ERROR_HANDLER value on startup. This is behind a flag because there might be minor overhead with `OnceLock` and I'm guessing this will be a niche feature. We can also do perf testing with OnceLock if someone really wants it to be used unconditionally, but I don't personally feel the need to do that. 5. I removed the "temporary error handler" on Commands (and all code associated with it). It added more branching, made Commands bigger / more expensive to initialize (note that we construct it at high frequencies / treat it like a pointer type), made the code harder to follow, and introduced a bunch of additional functions. We instead rely on the new default error handler used in `queue_fallible` for most things. In the event that a custom handler is required, `handle_error_with` can be used. 6. EntityCommand now _only_ supports functions that take `EntityWorldMut` (and all existing entity commands have been ported). Removing the marker component from EntityCommand hinged on this change, but I strongly believe this is for the best anyway, as this sets the stage for more efficient batched entity commands. 7. I added `EntityWorldMut::resource` and the other variants for more ergonomic resource access on `EntityWorldMut` (removes the need for entity.world_scope, which also incurs entity-lookup overhead). ## Open Questions 1. I believe we could merge `queue` and `queue_fallible` into a single `queue` which accepts both fallible and infallible commands (via the introduction of a `QueueCommand` trait). Is this desirable?
Objective
Fixes #2004
Fixes #3845
Fixes #7118
Fixes #10166
Solution
Command::with_error_handling
method. This wraps the relevant command in another command that, when applied, will apply the original command and handle any resulting errors.Command::apply
andEntityCommand::apply
now returnResult
.Command::with_error_handling
takes as a parameter an error handler of the formfn(&mut World, CommandError)
, which it passes the error to.CommandError
is an enum that can be eitherNoSuchEntity(Entity)
orCommandFailed(Box<dyn Error>)
.Closures
Result
, which will be passed towith_error_handling
.Commands
Commands::queue_fallible
andCommands::queue_fallible_with
, which callwith_error_handling
before queuing them (usingCommands::queue
will queue them without error handling).Commands::queue_fallible_with
takes anerror_handler
parameter, which will be used bywith_error_handling
instead of a command's default.command
submodule provides unqueued forms of built-in fallible commands so that you can use them withqueue_fallible_with
.error_handler
submodule that provides simple error handlers for convenience.Entity Commands
EntityCommand
now automatically checks if the entity exists before executing the command, and returnsNoSuchEntity
if it doesn't.EntityCommands::queue_with
takes anerror_handler
parameter, which will be used bywith_error_handling
instead of a command's default.entity_command
submodule provides unqueued forms of built-in entity commands so that you can use them withqueue_with
.Defaults
main
.Commands::override_error_handler
(or equivalent methods onEntityCommands
andEntityEntryCommands
).override_error_handler
takes an error handler (fn(&mut World, CommandError)
) and passes it to every subsequent command queued withCommands::queue_fallible
orEntityCommands::queue
._with
variants of the queue methods will still provide an error handler directly to the command.reset_error_handler
.Future Work
try
variants or just making them useless while they're deprecated),queue_fallible_with_default
could be removed, since its only purpose is to enable commands having different defaults.