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

Configurable error handling in commands #2004

Closed
alice-i-cecile opened this issue Apr 24, 2021 · 8 comments · Fixed by #17043
Closed

Configurable error handling in commands #2004

alice-i-cecile opened this issue Apr 24, 2021 · 8 comments · Fixed by #17043
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible C-Usability A targeted quality-of-life change that makes Bevy easier to use

Comments

@alice-i-cecile
Copy link
Member

What problem does this solve or what need does it fill?

Commands (like removing components or despawning entities) may fail for various reasons. Right now, for the sake of idempotency (the same function applied twice should do the same thing as the function applied once), robustness and not spamming the user endlessly, these silently fail.

This is not always the desired behavior, and should be configurable at the call site.

Possible solutions

Approach to config

Global config is convenient for reducing boilerplate, but the desired behavior may fail by call site.

Local config is useful for both debugging and persistent configuration.

Default behavior

This could panic, warn or fail silently.

Patterns

More methods: this is bad for API explosion.

Extra argument: this is very verbose for common operations because Rust doesn't have default arguments

Builder pattern: nice, as long as we don't have to use it.

Additional context

@Frizi, @BoxyUwU and myself ran into this while discussing new commands for relations in bevyengine/rfcs#18

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible S-Needs-Triage This issue needs to be labelled core A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use and removed S-Needs-Triage This issue needs to be labelled labels Apr 24, 2021
@Frizi
Copy link
Contributor

Frizi commented Apr 24, 2021

The "robustness" argument actually backfires here in some circumstances. Things might not get done for various reason, but the logic might rely on them being done. That means we get extremally hard to detect bugs, and often impossible to solve when no opportunity to handle those failure modes is given.

An especially involved example comes from drafted relations RFC:

commands.entity(target_mass).change_target::<Spring>(m1, m2);
 

Here we intend to modify Spring relation target_mass -> m1 to target_mass -> m2. There are multiple ways it can fail:

  • the new target m2 might not exist at command execution time, what should we do with relation target_mass -> m1?
  • the old target m1 might already be deleted, which also deletes it's relations. We end up with no relation on pointing to m2.
  • any other combination of m1, m2, or target_mass can be deleted
  • there might be no relation to move (relation target_mass -> m1 was not there to begin with)

Doing nothing at all in all those situations by default is a little scary. Some of those failures might be unexpected, and influence correctness of implemented game logic if they happen. Saying "things might get done if all stars align" sounds very fragile and hard to rely on.

Also in some cases, there are still decisions to be made at the last second, like what to do with the relation that was supposed to be moved out, but there is nowhere to go?

One way to fix that would be to allow adding extra FnOnce + 'commands closure parameter to handle those failures.

commands.entity(target_mass)
	.change_target::<Spring>(m1, m2)
	.on_failure(|failure_mode| {
		match failure_mode {
			// when move failed, remove relation that was supposed to be moved
			ChangeTargetFailure::NewTargetMissing(decision) => decision.remove_relation(),
			// expected, do nothing
			ChangeTargetFailure::OldTargetMissing | ChangeTargetFailure::RelationMissing => {},
			_ => panic!("unexpected failure {:?}", failure_mode)
		}
	});

A lot of things to bikeshed here of course, but I hope this gets the general idea through.

This would allow logging on some unexpected behaviour, panic on straight up algorithm correctness violations, or do last-second decisions on what action to take. This of course applies not only to relations, but things like moving a component between entities or other actions that can fail.

Things that can be done are of course limited by the closure lifetime, but this can be further expanded if needed. Adding some more context to that closure could possibly allow more complex behaviours in response to fails, e.g. by allowing to issue more commands.

@TehPers
Copy link
Contributor

TehPers commented Apr 27, 2021

I like the idea of having a callback for failures. This means that something more robust can be built with MPSC using Sender in the callback and another system that reads for failures from a Receiver. That way if a command fails, you can have more advanced error handling like updating the game state to an error state or changing how some entities are spawned or such.

@cart
Copy link
Member

cart commented Apr 27, 2021

In theory these callbacks could have arbitrary World access, as they would be executed during command application. They could even be Systems (although this would likely incur overhead if every entity needs to initialize the same system). That flexibility might also change if we ever adopt more granular command synchronization (ex: the "stageless" design we've been discussing).

@NathanSWard
Copy link
Contributor

We should note #2219 as being an example of this issue directly affecting a developer and causing a quite bad UX.
Once I get settled on a better bevy-development-workflow, this appears to be once of the issues we should prioritize.
I have some testing going locally for possible solutions to this and may present them in the near future.

@mockersf
Copy link
Member

They could even be Systems

I think that's a very good idea. We should do #2192 first, and then error handling could build on that?

@NathanSWard
Copy link
Contributor

I think that's a very good idea. We should do #2192 first, and then error handling could build on that?

I'm not the biggest fan of this necessarily, mostly because then we have to wrap the error handler in a Box where a currently implementation I'm working on doesn't require this extra allocation.
Also, a Command might have a specific Error type that it wants to hand the error_handler but this won't be easily done if the handler has to be a system.

@NathanSWard
Copy link
Contributor

If anyone if curious, I have an initial implementation of this at #2241

@alice-i-cecile
Copy link
Member Author

I've implemented a few simple methods (try_insert, try_remove and try_add_child) in this PR: Leafwing-Studios/Emergence#765 externally.

Feel free to steal it directly (MIT license) either for Bevy or your own projects. Works perfectly for what I need.

@alice-i-cecile alice-i-cecile modified the milestones: 0.11, 0.12 Jun 19, 2023
@alice-i-cecile alice-i-cecile removed this from the 0.12 milestone Oct 17, 2023
github-merge-queue bot pushed a commit that referenced this issue Jan 7, 2025
## 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.
mrchantey pushed a commit to mrchantey/bevy that referenced this issue 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.
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-Feature A new feature, making something new possible C-Usability A targeted quality-of-life change that makes Bevy easier to use
Projects
None yet
6 participants