Skip to content

Add Result handling to Commands and EntityCommands #17043

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

Merged
merged 34 commits into from
Jan 7, 2025

Conversation

JaySpruce
Copy link
Member

@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 <[email protected]>
Date:   Sun Dec 29 17:17:44 2024 -0600

    ci docs

commit 492e558
Author: JaySpruce <[email protected]>
Date:   Sun Dec 29 16:59:31 2024 -0600

    messed up the conflict resolve

commit 74592fa
Merge: e0c6d65 0f2b2de
Author: JaySpruce <[email protected]>
Date:   Sun Dec 29 16:48:37 2024 -0600

    Merge branch 'main' into refactor_hierarchy_commands

commit e0c6d65
Author: JaySpruce <[email protected]>
Date:   Sun Dec 29 13:28:07 2024 -0600

    refactor to remove structs
@JaySpruce
Copy link
Member 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.

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
Member 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
Member 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
Member 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?
mrchantey pushed a commit to mrchantey/bevy that referenced this pull request Feb 4, 2025
…ove `UnsafeWorldCell` from error (bevyengine#17115)

## Objective

The error `EntityFetchError::NoSuchEntity` has an `UnsafeWorldCell`
inside it, which it uses to call
`Entities::entity_does_not_exist_error_details_message` when being
printed. That method returns a `String` that, if the `track_location`
feature is enabled, contains the location of whoever despawned the
relevant entity.

I initially had to modify this error while working on bevyengine#17043. The
`UnsafeWorldCell` was causing borrow problems when being returned from a
command, so I tried replacing it with the `String` that the method
returns, since that was the world cell's only purpose.

Unfortunately, `String`s are slow, and it significantly impacted
performance (on top of that PR's performance hit):
<details>
<summary>17043 benchmarks</summary>

### With `String`

![error_handling_insert_slow](https://github.com/user-attachments/assets/5629ba6d-69fc-4c16-84c9-8be7e449232d)

### No `String`

![error_handling_insert_fixed](https://github.com/user-attachments/assets/6393e2d6-e61a-4558-8ff1-471ff8356c1c)

</details>

For that PR, I just removed the error details entirely, but I figured
I'd try to find a way to keep them around.

## Solution

- Replace the `String` with a helper struct that holds the location, and
only turn it into a string when someone actually wants to print it.
- Replace the `UnsafeWorldCell` with the aforementioned struct.
- Do the same for `QueryEntityError::NoSuchEntity`.

## Benchmarking

This had some interesting performance impact:

<details>
<summary>This PR vs main</summary>


![dne_rework_1](https://github.com/user-attachments/assets/05bf91b4-dddc-4d76-b2c4-41c9d25c7a57)

![dne_rework_2](https://github.com/user-attachments/assets/34aa76b2-d8a7-41e0-9670-c213207e457d)

![dne_rework_3](https://github.com/user-attachments/assets/8b9bd4e4-77c8-45a7-b058-dc0dfd3dd323)

</details>

## Other work

`QueryEntityError::QueryDoesNotMatch` also has an `UnsafeWorldCell`
inside it. This one would be more complicated to rework while keeping
the same functionality.

## Migration Guide

The errors `EntityFetchError::NoSuchEntity` and
`QueryEntityError::NoSuchEntity` now contain an
`EntityDoesNotExistDetails` struct instead of an `UnsafeWorldCell`. If
you were just printing these, they should work identically.

---------

Co-authored-by: Benjamin Brienen <[email protected]>
mrchantey pushed a commit to mrchantey/bevy that referenced this pull request Feb 4, 2025
…17043)

## Objective

Fixes bevyengine#2004
Fixes bevyengine#3845
Fixes bevyengine#7118
Fixes bevyengine#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.
mrchantey pushed a commit to mrchantey/bevy that referenced this pull request Feb 4, 2025
# Objective

Rework / build on bevyengine#17043 to simplify the implementation. bevyengine#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?
@alice-i-cecile
Copy link
Member

Thank you to everyone involved with the authoring or reviewing of this PR! This work is relatively important and needs release notes! Head over to bevyengine/bevy-website#1979 if you'd like to help out.

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