Skip to content

Commit 0070514

Browse files
Fallible systems (#16589)
# 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 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]>
1 parent e763b71 commit 0070514

File tree

14 files changed

+389
-52
lines changed

14 files changed

+389
-52
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,6 +2031,17 @@ description = "Systems are skipped if their parameters cannot be acquired"
20312031
category = "ECS (Entity Component System)"
20322032
wasm = false
20332033

2034+
[[example]]
2035+
name = "fallible_systems"
2036+
path = "examples/ecs/fallible_systems.rs"
2037+
doc-scrape-examples = true
2038+
2039+
[package.metadata.example.fallible_systems]
2040+
name = "Fallible Systems"
2041+
description = "Systems that return results to handle errors"
2042+
category = "ECS (Entity Component System)"
2043+
wasm = false
2044+
20342045
[[example]]
20352046
name = "startup_system"
20362047
path = "examples/ecs/startup_system.rs"

crates/bevy_ecs/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// FIXME(11590): remove this once the lint is fixed
22
#![allow(unsafe_op_in_unsafe_fn)]
3+
// TODO: remove once Edition 2024 is released
4+
#![allow(dependency_on_unit_never_type_fallback)]
35
#![doc = include_str!("../README.md")]
46
// `rustdoc_internals` is needed for `#[doc(fake_variadics)]`
57
#![allow(internal_features)]
@@ -30,6 +32,7 @@ pub mod query;
3032
#[cfg(feature = "bevy_reflect")]
3133
pub mod reflect;
3234
pub mod removal_detection;
35+
pub mod result;
3336
pub mod schedule;
3437
pub mod storage;
3538
pub mod system;
@@ -53,6 +56,7 @@ pub mod prelude {
5356
observer::{CloneEntityWithObserversExt, Observer, Trigger},
5457
query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
5558
removal_detection::RemovedComponents,
59+
result::{Error, Result},
5660
schedule::{
5761
apply_deferred, common_conditions::*, ApplyDeferred, Condition, IntoSystemConfigs,
5862
IntoSystemSet, IntoSystemSetConfigs, Schedule, Schedules, SystemSet,

crates/bevy_ecs/src/result.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! Contains error and result helpers for use in fallible systems.
2+
3+
/// A dynamic error type for use in fallible systems.
4+
pub type Error = Box<dyn core::error::Error + Send + Sync + 'static>;
5+
6+
/// A result type for use in fallible systems.
7+
pub type Result<T = (), E = Error> = core::result::Result<T, E>;

crates/bevy_ecs/src/schedule/config.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use variadics_please::all_tuples;
22

33
use crate::{
4+
result::Result,
45
schedule::{
56
condition::{BoxedCondition, Condition},
67
graph::{Ambiguity, Dependency, DependencyKind, GraphInfo},
78
set::{InternedSystemSet, IntoSystemSet, SystemSet},
89
Chain,
910
},
10-
system::{BoxedSystem, IntoSystem, System},
11+
system::{BoxedSystem, IntoSystem, ScheduleSystem, System},
1112
};
1213

1314
fn new_condition<M>(condition: impl Condition<M>) -> BoxedCondition {
@@ -47,7 +48,7 @@ pub struct NodeConfig<T> {
4748
}
4849

4950
/// Stores configuration for a single system.
50-
pub type SystemConfig = NodeConfig<BoxedSystem>;
51+
pub type SystemConfig = NodeConfig<ScheduleSystem>;
5152

5253
/// A collections of generic [`NodeConfig`]s.
5354
pub enum NodeConfigs<T> {
@@ -65,10 +66,10 @@ pub enum NodeConfigs<T> {
6566
}
6667

6768
/// A collection of [`SystemConfig`].
68-
pub type SystemConfigs = NodeConfigs<BoxedSystem>;
69+
pub type SystemConfigs = NodeConfigs<ScheduleSystem>;
6970

7071
impl SystemConfigs {
71-
fn new_system(system: BoxedSystem) -> Self {
72+
fn new_system(system: ScheduleSystem) -> Self {
7273
// include system in its default sets
7374
let sets = system.default_system_sets().into_iter().collect();
7475
Self::NodeConfig(SystemConfig {
@@ -517,18 +518,41 @@ impl IntoSystemConfigs<()> for SystemConfigs {
517518
}
518519
}
519520

520-
impl<Marker, F> IntoSystemConfigs<Marker> for F
521+
#[doc(hidden)]
522+
pub struct Infallible;
523+
524+
impl<F, Marker> IntoSystemConfigs<(Infallible, Marker)> for F
521525
where
522526
F: IntoSystem<(), (), Marker>,
523527
{
524528
fn into_configs(self) -> SystemConfigs {
525-
SystemConfigs::new_system(Box::new(IntoSystem::into_system(self)))
529+
let boxed_system = Box::new(IntoSystem::into_system(self));
530+
SystemConfigs::new_system(ScheduleSystem::Infallible(boxed_system))
526531
}
527532
}
528533

529534
impl IntoSystemConfigs<()> for BoxedSystem<(), ()> {
530535
fn into_configs(self) -> SystemConfigs {
531-
SystemConfigs::new_system(self)
536+
SystemConfigs::new_system(ScheduleSystem::Infallible(self))
537+
}
538+
}
539+
540+
#[doc(hidden)]
541+
pub struct Fallible;
542+
543+
impl<F, Marker> IntoSystemConfigs<(Fallible, Marker)> for F
544+
where
545+
F: IntoSystem<(), Result, Marker>,
546+
{
547+
fn into_configs(self) -> SystemConfigs {
548+
let boxed_system = Box::new(IntoSystem::into_system(self));
549+
SystemConfigs::new_system(ScheduleSystem::Fallible(boxed_system))
550+
}
551+
}
552+
553+
impl IntoSystemConfigs<()> for BoxedSystem<(), Result> {
554+
fn into_configs(self) -> SystemConfigs {
555+
SystemConfigs::new_system(ScheduleSystem::Fallible(self))
532556
}
533557
}
534558

crates/bevy_ecs/src/schedule/executor/mod.rs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::{
1919
prelude::{IntoSystemSet, SystemSet},
2020
query::Access,
2121
schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet},
22-
system::{BoxedSystem, System, SystemIn},
22+
system::{ScheduleSystem, System, SystemIn},
2323
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World},
2424
};
2525

@@ -67,7 +67,7 @@ pub struct SystemSchedule {
6767
/// List of system node ids.
6868
pub(super) system_ids: Vec<NodeId>,
6969
/// Indexed by system node id.
70-
pub(super) systems: Vec<BoxedSystem>,
70+
pub(super) systems: Vec<ScheduleSystem>,
7171
/// Indexed by system node id.
7272
pub(super) system_conditions: Vec<Vec<BoxedCondition>>,
7373
/// Indexed by system node id.
@@ -140,9 +140,8 @@ pub const apply_deferred: ApplyDeferred = ApplyDeferred;
140140
pub struct ApplyDeferred;
141141

142142
/// Returns `true` if the [`System`] is an instance of [`ApplyDeferred`].
143-
pub(super) fn is_apply_deferred(system: &BoxedSystem) -> bool {
144-
// deref to use `System::type_id` instead of `Any::type_id`
145-
system.as_ref().type_id() == TypeId::of::<ApplyDeferred>()
143+
pub(super) fn is_apply_deferred(system: &ScheduleSystem) -> bool {
144+
system.type_id() == TypeId::of::<ApplyDeferred>()
146145
}
147146

148147
impl System for ApplyDeferred {
@@ -247,19 +246,18 @@ mod __rust_begin_short_backtrace {
247246
use core::hint::black_box;
248247

249248
use crate::{
250-
system::{ReadOnlySystem, System},
249+
result::Result,
250+
system::{ReadOnlySystem, ScheduleSystem, System},
251251
world::{unsafe_world_cell::UnsafeWorldCell, World},
252252
};
253253

254254
/// # Safety
255255
/// See `System::run_unsafe`.
256256
#[inline(never)]
257-
pub(super) unsafe fn run_unsafe(
258-
system: &mut dyn System<In = (), Out = ()>,
259-
world: UnsafeWorldCell,
260-
) {
261-
system.run_unsafe((), world);
257+
pub(super) unsafe fn run_unsafe(system: &mut ScheduleSystem, world: UnsafeWorldCell) -> Result {
258+
let result = system.run_unsafe((), world);
262259
black_box(());
260+
result
263261
}
264262

265263
/// # Safety
@@ -273,9 +271,10 @@ mod __rust_begin_short_backtrace {
273271
}
274272

275273
#[inline(never)]
276-
pub(super) fn run(system: &mut dyn System<In = (), Out = ()>, world: &mut World) {
277-
system.run((), world);
274+
pub(super) fn run(system: &mut ScheduleSystem, world: &mut World) -> Result {
275+
let result = system.run((), world);
278276
black_box(());
277+
result
279278
}
280279

281280
#[inline(never)]

crates/bevy_ecs/src/schedule/executor/multi_threaded.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
prelude::Resource,
1919
query::Access,
2020
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
21-
system::BoxedSystem,
21+
system::{ScheduleSystem, System},
2222
world::{unsafe_world_cell::UnsafeWorldCell, World},
2323
};
2424

@@ -29,7 +29,7 @@ use super::__rust_begin_short_backtrace;
2929
/// Borrowed data used by the [`MultiThreadedExecutor`].
3030
struct Environment<'env, 'sys> {
3131
executor: &'env MultiThreadedExecutor,
32-
systems: &'sys [SyncUnsafeCell<BoxedSystem>],
32+
systems: &'sys [SyncUnsafeCell<ScheduleSystem>],
3333
conditions: SyncUnsafeCell<Conditions<'sys>>,
3434
world_cell: UnsafeWorldCell<'env>,
3535
}
@@ -269,7 +269,7 @@ impl<'scope, 'env: 'scope, 'sys> Context<'scope, 'env, 'sys> {
269269
&self,
270270
system_index: usize,
271271
res: Result<(), Box<dyn Any + Send>>,
272-
system: &BoxedSystem,
272+
system: &ScheduleSystem,
273273
) {
274274
// tell the executor that the system finished
275275
self.environment
@@ -459,7 +459,7 @@ impl ExecutorState {
459459
fn can_run(
460460
&mut self,
461461
system_index: usize,
462-
system: &mut BoxedSystem,
462+
system: &mut ScheduleSystem,
463463
conditions: &mut Conditions,
464464
world: UnsafeWorldCell,
465465
) -> bool {
@@ -523,7 +523,7 @@ impl ExecutorState {
523523
unsafe fn should_run(
524524
&mut self,
525525
system_index: usize,
526-
system: &mut BoxedSystem,
526+
system: &mut ScheduleSystem,
527527
conditions: &mut Conditions,
528528
world: UnsafeWorldCell,
529529
) -> bool {
@@ -603,8 +603,9 @@ impl ExecutorState {
603603
// access the world data used by the system.
604604
// - `update_archetype_component_access` has been called.
605605
unsafe {
606-
__rust_begin_short_backtrace::run_unsafe(
607-
&mut **system,
606+
// TODO: implement an error-handling API instead of suppressing a possible failure.
607+
let _ = __rust_begin_short_backtrace::run_unsafe(
608+
system,
608609
context.environment.world_cell,
609610
);
610611
};
@@ -650,7 +651,8 @@ impl ExecutorState {
650651
// that no other systems currently have access to the world.
651652
let world = unsafe { context.environment.world_cell.world_mut() };
652653
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
653-
__rust_begin_short_backtrace::run(&mut **system, world);
654+
// TODO: implement an error-handling API instead of suppressing a possible failure.
655+
let _ = __rust_begin_short_backtrace::run(system, world);
654656
}));
655657
context.system_completed(system_index, res, system);
656658
};
@@ -710,7 +712,7 @@ impl ExecutorState {
710712

711713
fn apply_deferred(
712714
unapplied_systems: &FixedBitSet,
713-
systems: &[SyncUnsafeCell<BoxedSystem>],
715+
systems: &[SyncUnsafeCell<ScheduleSystem>],
714716
world: &mut World,
715717
) -> Result<(), Box<dyn Any + Send>> {
716718
for system_index in unapplied_systems.ones() {

crates/bevy_ecs/src/schedule/executor/simple.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
schedule::{
88
executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
99
},
10+
system::System,
1011
world::World,
1112
};
1213

@@ -100,7 +101,8 @@ impl SystemExecutor for SimpleExecutor {
100101
}
101102

102103
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
103-
__rust_begin_short_backtrace::run(&mut **system, world);
104+
// TODO: implement an error-handling API instead of suppressing a possible failure.
105+
let _ = __rust_begin_short_backtrace::run(system, world);
104106
}));
105107
if let Err(payload) = res {
106108
eprintln!("Encountered a panic in system `{}`!", &*system.name());
@@ -119,7 +121,7 @@ impl SystemExecutor for SimpleExecutor {
119121

120122
impl SimpleExecutor {
121123
/// Creates a new simple executor for use in a [`Schedule`](crate::schedule::Schedule).
122-
/// This calls each system in order and immediately calls [`System::apply_deferred`](crate::system::System::apply_deferred).
124+
/// This calls each system in order and immediately calls [`System::apply_deferred`].
123125
pub const fn new() -> Self {
124126
Self {
125127
evaluated_sets: FixedBitSet::new(),

crates/bevy_ecs/src/schedule/executor/single_threaded.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use fixedbitset::FixedBitSet;
55

66
use crate::{
77
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
8+
system::System,
89
world::World,
910
};
1011

@@ -108,14 +109,18 @@ impl SystemExecutor for SingleThreadedExecutor {
108109

109110
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
110111
if system.is_exclusive() {
111-
__rust_begin_short_backtrace::run(&mut **system, world);
112+
// TODO: implement an error-handling API instead of suppressing a possible failure.
113+
let _ = __rust_begin_short_backtrace::run(system, world);
112114
} else {
113115
// Use run_unsafe to avoid immediately applying deferred buffers
114116
let world = world.as_unsafe_world_cell();
115117
system.update_archetype_component_access(world);
116118
// SAFETY: We have exclusive, single-threaded access to the world and
117119
// update_archetype_component_access is being called immediately before this.
118-
unsafe { __rust_begin_short_backtrace::run_unsafe(&mut **system, world) };
120+
unsafe {
121+
// TODO: implement an error-handling API instead of suppressing a possible failure.
122+
let _ = __rust_begin_short_backtrace::run_unsafe(system, world);
123+
};
119124
}
120125
}));
121126
if let Err(payload) = res {

0 commit comments

Comments
 (0)