-
-
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
Fallible systems #16589
Fallible systems #16589
Conversation
Even without the actual error handling benefits this provides, just having a more blessed way to use |
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 agree this probably needs an example, but I like the approach. Opens up the possibility of having error handlers in the future, which would resolve the to-panic or not to-panic debate entirely. This also lays the groundwork for how fallibility in Command
s could work. Really nice work!
It was mentioned in Discord, but I'll include it here for posterity: with fallible systems getting first-class treatment, there may be room to consider removing the panicking variants of certain functions (e.g., |
Co-authored-by: Zachary Harrold <[email protected]>
Co-authored-by: Zachary Harrold <[email protected]>
Co-authored-by: Zachary Harrold <[email protected]>
That's in line with the third point Cart proposed in #14275 (comment). He indicated then that it was important to land all the related changes in a single release cycle, and I agree. This PR provides his (1), what (2) and (3) look like is up to @alice-i-cecile and the other designated ecs experts. |
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.
Amazing to see how straightforward this is, all things considered. Very excited!
Why? This just makes the code more jarring compared to the rest of the Rust ecosystem, and more cognitive load to switch between returning nothing vs. returning some value. It'd make sense if it's something useful like https://docs.rs/anyhow/latest/anyhow/fn.Ok.html |
In this I am trying to defer to my understanding of Cart's preferences. He uses a const in the linked issue, and I believe has expressed that |
)); | ||
|
||
// Create a new sphere mesh: | ||
let mut sphere_mesh = Sphere::new(1.0).mesh().ico(7)?; |
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.
What a sight to behold. Once proper (user configurable) handlers are added in a follow-up this will be perfect. Bevy APIs can be simplified and made more reliable without any loss in ergonomics (IMO). Adding Ok(())
at the end of a system is a small price to pay that (hopefully) Rust will solve on its own (since the issue isn't specific to Bevy)
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.
Yeah :) this example is really just a minimal placeholder. Once we have handlers hooked up, I intend to go through and update all the examples to use this style (where it makes sense).
|
Alright, I added the most basic tests and examples in the world. There will be more to do there when handlers are hooked up. Ready for review. |
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.
@nth this is good to go once it's merge-conflict free. I do prefer the "everything is fallible" approach by WrongShoe, but that's easily left to a follow-up refactor. Let's get the ball rolling here.
# Objective - First step for #16718 - #16589 introduced an api that can only ignore errors, which is risky ## Solution - Panic instead of just ignoring the errors ## Testing - Changed the `fallible_systems` example to return an error ``` Encountered an error in system `fallible_systems::setup`: TooManyVertices { subdivisions: 300, number_of_resulting_points: 906012 } Encountered a panic in system `fallible_systems::setup`! Encountered a panic in system `bevy_app::main_schedule::Main::run_main`! ```
Might be a bit late on this discussion, but this particular implementation feels a bit intrusive to me. Can we instead implement IntoSystemConfigs on systems that returns |
# Objective - #16589 added an enum to switch between fallible and infallible system. This branching should be unnecessary if we wrap infallible systems in a function to return `Ok(())`. ## Solution - Create a wrapper system for `System<(), ()>`s that returns `Ok` on the call to `run` and `run_unsafe`. The wrapper should compile out, but I haven't checked. - I removed the `impl IntoSystemConfigs for BoxedSystem<(), ()>` as I couldn't figure out a way to keep the impl without double boxing. ## Testing - ran `many_foxes` example to check if it still runs. ## Migration Guide - `IntoSystemConfigs` has been removed for `BoxedSystem<(), ()>`. Either use `InfallibleSystemWrapper` before boxing or make your system return `bevy::ecs::prelude::Result`.
# Objective Error handling in bevy is hard. See for reference bevyengine#11562, bevyengine#10874 and bevyengine#12660. The goal of this PR is to make it better, by allowing users to optionally return `Result` from systems as outlined by Cart in <bevyengine#14275 (comment)>. ## Solution This PR introduces a new `ScheuleSystem` type to represent systems that can be added to schedules. Instances of this type contain either an infallible `BoxedSystem<(), ()>` or a fallible `BoxedSystem<(), Result>`. `ScheuleSystem` implements `System<In = (), Out = Result>` and replaces all uses of `BoxedSystem` in schedules. The async executor now receives a result after executing a system, which for infallible systems is always `Ok(())`. Currently it ignores this result, but more useful error handling could also be implemented. Aliases for `Error` and `Result` have been added to the `bevy_ecs` prelude, as well as const `OK` which new users may find more friendly than `Ok(())`. ## Testing - Currently there are not actual semantics changes that really require new tests, but I added a basic one just to make sure we don't break stuff in the future. - The behavior of existing systems is totally unchanged, including logging. - All of the existing systems tests pass, and I have not noticed anything strange while playing with the examples ## Showcase The following minimal example prints "hello world" once, then completes. ```rust use bevy::prelude::*; fn main() { App::new().add_systems(Update, hello_world_system).run(); } fn hello_world_system() -> Result { println!("hello world"); Err("string")?; println!("goodbye world"); OK } ``` ## Migration Guide This change should be pretty much non-breaking, except for users who have implemented their own custom executors. Those users should use `ScheduleSystem` in place of `BoxedSystem<(), ()>` and import the `System` trait where needed. They can choose to do whatever they wish with the result. ## Current Work + [x] Fix tests & doc comments + [x] Write more tests + [x] Add examples + [X] Draft release notes ## Draft Release Notes As of this release, systems can now return results. First a bit of background: Bevy has hisotrically expected systems to return the empty type `()`. While this makes sense in the context of the ecs, it's at odds with how error handling is typically done in rust: returning `Result::Error` to indicate failure, and using the short-circuiting `?` operator to propagate that error up the call stack to where it can be properly handled. Users of functional languages will tell you this is called "monadic error handling". Not being able to return `Results` from systems left bevy users with a quandry. They could add custom error handling logic to every system, or manually pipe every system into an error handler, or perhaps sidestep the issue with some combination of fallible assignents, logging, macros, and early returns. Often, users would just litter their systems with unwraps and possible panics. While any one of these approaches might be fine for a particular user, each of them has their own drawbacks, and none makes good use of the language. Serious issues could also arrise when two different crates used by the same project made different choices about error handling. Now, by returning results, systems can defer error handling to the application itself. It looks like this: ```rust // Previous, handling internally app.add_systems(my_system) fn my_system(window: Query<&Window>) { let Ok(window) = query.get_single() else { return; }; // ... do something to the window here } // Previous, handling externally app.add_systems(my_system.pipe(my_error_handler)) fn my_system(window: Query<&Window>) -> Result<(), impl Error> { let window = query.get_single()?; // ... do something to the window here Ok(()) } // Previous, panicking app.add_systems(my_system) fn my_system(window: Query<&Window>) { let window = query.single(); // ... do something to the window here } // Now app.add_systems(my_system) fn my_system(window: Query<&Window>) -> Result { let window = query.get_single()?; // ... do something to the window here Ok(()) } ``` There are currently some limitations. Systems must either return `()` or `Result<(), Box<dyn Error + Send + Sync + 'static>>`, with no in-between. Results are also ignored by default, and though implementing a custom handler is possible, it involves writing your own custom ecs executor (which is *not* recomended). Systems should return errors when they cannot perform their normal behavior. In turn, errors returned to the executor while running the schedule will (eventually) be treated as unexpected. Users and library authors should prefer to return errors for anything that disrupts the normal expected behavior of a system, and should only handle expected cases internally. We have big plans for improving error handling further: + Allowing users to change the error handling logic of the default executors. + Adding source tracking and optional backtraces to errors. + Possibly adding tracing-levels (Error/Warn/Info/Debug/Trace) to errors. + Generally making the default error logging more helpful and inteligent. + Adding monadic system combininators for fallible systems. + Possibly removing all panicking variants from our api. --------- Co-authored-by: Zachary Harrold <[email protected]>
# Objective - First step for bevyengine#16718 - bevyengine#16589 introduced an api that can only ignore errors, which is risky ## Solution - Panic instead of just ignoring the errors ## Testing - Changed the `fallible_systems` example to return an error ``` Encountered an error in system `fallible_systems::setup`: TooManyVertices { subdivisions: 300, number_of_resulting_points: 906012 } Encountered a panic in system `fallible_systems::setup`! Encountered a panic in system `bevy_app::main_schedule::Main::run_main`! ```
# Objective - bevyengine#16589 added an enum to switch between fallible and infallible system. This branching should be unnecessary if we wrap infallible systems in a function to return `Ok(())`. ## Solution - Create a wrapper system for `System<(), ()>`s that returns `Ok` on the call to `run` and `run_unsafe`. The wrapper should compile out, but I haven't checked. - I removed the `impl IntoSystemConfigs for BoxedSystem<(), ()>` as I couldn't figure out a way to keep the impl without double boxing. ## Testing - ran `many_foxes` example to check if it still runs. ## Migration Guide - `IntoSystemConfigs` has been removed for `BoxedSystem<(), ()>`. Either use `InfallibleSystemWrapper` before boxing or make your system return `bevy::ecs::prelude::Result`.
Objective
Error handling in bevy is hard. See for reference #11562, #10874 and #12660. The goal of this PR is to make it better, by allowing users to optionally return
Result
from systems as outlined by Cart in #14275 (comment).Solution
This PR introduces a new
ScheuleSystem
type to represent systems that can be added to schedules. Instances of this type contain either an infallibleBoxedSystem<(), ()>
or a fallibleBoxedSystem<(), Result>
.ScheuleSystem
implementsSystem<In = (), Out = Result>
and replaces all uses ofBoxedSystem
in schedules. The async executor now receives a result after executing a system, which for infallible systems is alwaysOk(())
. Currently it ignores this result, but more useful error handling could also be implemented.Aliases for
Error
andResult
have been added to thebevy_ecs
prelude, as well as constOK
which new users may find more friendly thanOk(())
.Testing
Showcase
The following minimal example prints "hello world" once, then completes.
Migration Guide
This change should be pretty much non-breaking, except for users who have implemented their own custom executors. Those users should use
ScheduleSystem
in place ofBoxedSystem<(), ()>
and import theSystem
trait where needed. They can choose to do whatever they wish with the result.Current Work
Draft Release Notes
As of this release, systems can now return results.
First a bit of background: Bevy has hisotrically expected systems to return the empty type
()
. While this makes sense in the context of the ecs, it's at odds with how error handling is typically done in rust: returningResult::Error
to indicate failure, and using the short-circuiting?
operator to propagate that error up the call stack to where it can be properly handled. Users of functional languages will tell you this is called "monadic error handling".Not being able to return
Results
from systems left bevy users with a quandry. They could add custom error handling logic to every system, or manually pipe every system into an error handler, or perhaps sidestep the issue with some combination of fallible assignents, logging, macros, and early returns. Often, users would just litter their systems with unwraps and possible panics.While any one of these approaches might be fine for a particular user, each of them has their own drawbacks, and none makes good use of the language. Serious issues could also arrise when two different crates used by the same project made different choices about error handling.
Now, by returning results, systems can defer error handling to the application itself. It looks like this:
There are currently some limitations. Systems must either return
()
orResult<(), Box<dyn Error + Send + Sync + 'static>>
, with no in-between. Results are also ignored by default, and though implementing a custom handler is possible, it involves writing your own custom ecs executor (which is not recomended).Systems should return errors when they cannot perform their normal behavior. In turn, errors returned to the executor while running the schedule will (eventually) be treated as unexpected. Users and library authors should prefer to return errors for anything that disrupts the normal expected behavior of a system, and should only handle expected cases internally.
We have big plans for improving error handling further: