Skip to content
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

Merged
merged 34 commits into from
Jan 7, 2025

Conversation

JaySpruce
Copy link
Contributor

@JaySpruce JaySpruce commented Dec 30, 2024

Objective

Fixes #2004
Fixes #3845
Fixes #7118
Fixes #10166

Solution

  • The crux of this PR is the new 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.
  • To enable this, Command::apply and EntityCommand::apply now return Result.
  • Command::with_error_handling takes as a parameter an error handler of the form fn(&mut World, CommandError), which it passes the error to.
    • CommandError is an enum that can be either NoSuchEntity(Entity) or CommandFailed(Box<dyn Error>).

Closures

  • Closure commands can now optionally return Result, which will be passed to with_error_handling.

Commands

  • Fallible commands can be queued with Commands::queue_fallible and Commands::queue_fallible_with, which call with_error_handling before queuing them (using Commands::queue will queue them without error handling).
  • Commands::queue_fallible_with takes an error_handler parameter, which will be used by with_error_handling instead of a command's default.
  • The command submodule provides unqueued forms of built-in fallible commands so that you can use them with queue_fallible_with.
  • There is also an 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 returns NoSuchEntity if it doesn't.
  • Since all entity commands might need to return an error, they are always queued with error handling.
  • EntityCommands::queue_with takes an error_handler parameter, which will be used by with_error_handling instead of a command's default.
  • The entity_command submodule provides unqueued forms of built-in entity commands so that you can use them with queue_with.

Defaults

  • In the future, commands should all fail according to the global error handling setting. That doesn't exist yet though.
  • For this PR, commands all fail the way they do on main.
  • Both now and in the future, the defaults can be overridden by Commands::override_error_handler (or equivalent methods on EntityCommands and EntityEntryCommands).
  • override_error_handler takes an error handler (fn(&mut World, CommandError)) and passes it to every subsequent command queued with Commands::queue_fallible or EntityCommands::queue.
  • The _with variants of the queue methods will still provide an error handler directly to the command.
  • An override can be reset with reset_error_handler.

Future Work

  • After a universal error handling mode is added, we can change all commands to fail that way by default.
    • Once we have all commands failing the same way (which would require either the full removal of 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.

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
@JaySpruce
Copy link
Contributor Author

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.

@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Release-Note Work that should be called out in the blog due to impact X-Contentious There are nontrivial implications that should be thought through labels Dec 30, 2024
@alice-i-cecile alice-i-cecile added S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Dec 30, 2024
///
/// # 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")]
Copy link
Member

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.

Copy link
Contributor

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.

@alice-i-cecile
Copy link
Member

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.

@NthTensor
Copy link
Contributor

I'd like to do a proper review of this, if I can find the time. Writing this as a reminder to myself.

@alice-i-cecile
Copy link
Member

@EngoDev can I get your opinions on this work? I think I prefer the basic idea here over that of #11184. Making commands fallible by default (with commands that can't fail always returning Ok(())) feels like the right direction.

@EngoDev
Copy link

EngoDev commented Dec 31, 2024

@EngoDev can I get your opinions on this work? I think I prefer the basic idea here over that of #11184. Making commands fallible by default (with commands that can't fail always returning Ok(())) feels like the right direction.

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.

@alice-i-cecile alice-i-cecile added this to the 0.16 milestone Jan 6, 2025
Copy link
Member

@alice-i-cecile alice-i-cecile left a 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!

Copy link
Contributor

@NthTensor NthTensor left a 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.

Copy link
Contributor

@LikeLakers2 LikeLakers2 left a 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
Copy link
Contributor

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".

Copy link
Contributor Author

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)>
Copy link
Contributor

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>).

Copy link
Contributor

@LikeLakers2 LikeLakers2 left a 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
Copy link
Contributor

@cBournhonesque cBournhonesque Jan 6, 2025

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?

Copy link
Contributor Author

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

Copy link
Contributor

@cBournhonesque cBournhonesque left a 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!

LikeLakers2
LikeLakers2 approved these changes Jan 6, 2025
@cart cart self-requested a review January 6, 2025 21:20
@NthTensor
Copy link
Contributor

@JoJoJet points out in discord (https://discord.com/channels/691052431525675048/1325887475729698837/1325917241396564069) that we may want to use concrete error types if possible.

@alice-i-cecile
Copy link
Member

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.

@NthTensor
Copy link
Contributor

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.

@JaySpruce
Copy link
Contributor Author

JaySpruce commented Jan 7, 2025

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 impl Command for SomeStruct, you still only have to implement apply and it'll just work. To have concrete errors, I think structs would have to do more work to implement Command.

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

@alice-i-cecile
Copy link
Member

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.

Copy link
Member

@cart cart left a 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).

  1. 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.
  2. As others have said, making commands generic on error type instead of boxing in-place is a good idea.
  3. 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.
  4. 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 calling commands.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.

@alice-i-cecile
Copy link
Member

alice-i-cecile commented Jan 7, 2025

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" :)

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 7, 2025
@cart cart mentioned this pull request Jan 7, 2025
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jan 7, 2025
Merged via the queue into bevyengine:main with commit ee44141 Jan 7, 2025
29 checks passed
@JaySpruce JaySpruce deleted the command_error_handling branch January 8, 2025 01:52
github-merge-queue bot pushed a commit that referenced this pull request Jan 10, 2025
# 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?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
9 participants