-
-
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
Improved Command Errors #17215
base: main
Are you sure you want to change the base?
Improved Command Errors #17215
Conversation
I don't have time for a full review, but I skimmed, and two quick comments. First, I think the output should be an associated type, not a parameter. Can't think of any good reason to allow a single type to implement the trait multiple times with different return types. Second, if I read correctly, there isn't a way to make built in commands like |
self(world) | ||
/// Takes a [`Command`] that returns a [`Result`] with an error that can be converted into the [`Error`] type | ||
/// and returns a [`Command`] that internally converts that error to [`Error`] (if it occurs). | ||
pub fn map_command_err<T, E: Into<Error>>( |
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.
Would it be worth combining this with the impl HandleError above with
impl<C: Command<Result<T, E>>, T, E: Into<Error>> HandleError for C {
...
}
Since it's essentially what we want in the end?
I see this function is only used once in queue_fallible to create a Command compatible with HandleError
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 really like the leaner traits, great work!
-
I feel like the
EntityCommands
change to only supportEntityWorldMut
should be part of a separate PR. It's kind of independent to error-handling. Having this change in the same PR makes it harder to discuss implications specific to theEntityWorldMut
change -
I think
queue
andqueue_fallible
should be unified if possible. I have tons of commands in my project, some of them are tiny and return()
, some of them are more complex and are fallible. I don't really want to have to remember if they return () or Result to see if I need to callqueue
orqueue_fallible
, I just want to queue them. If they returnResult
then I want theResult
to be handled.
And in the rare case where you have a command which returns Result
but you don't want it handled you can always apply the silent
error-handler
pub fn reset_error_handler(&mut self) { | ||
self.error_handler_override = None; | ||
/// This will use the default error handler. See [`Command`] for details on error handling. | ||
pub fn queue_fallible<E: Into<Error>>(&mut self, command: impl Command<Result<(), E>>) { |
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.
We also want to support commands that return Result<T, E> right? (even if the result just gets discarded)
I'm assuming this since the Command
trait is implemented for Result<T, E>
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.
This was largely just to leave doors open in case we find a use for "commands that produce outputs". My thought was that its probably preferable to outright not support outputs here than to silently ignore them, but I don't have strong feelings.
/// Use [`get_resource`](EntityWorldMut::get_resource) instead if you want to handle this case. | ||
#[inline] | ||
#[track_caller] | ||
pub fn resource<R: Resource>(&self) -> &R { |
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.
Should this part of a separate PR?
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 can see the argument, although porting one of the doc examples is what motivated this change. Switching EntityCommand
to accept EntityWorldMut
resulted in increased UX pressure to add these. I personally consider them to be reasonably uncontroversial. But if people feel differently I'm happy to break this out.
It looks like your PR is a breaking change, but you didn't provide a migration guide. Could you add some context on what users should update when this change get released in a new version of Bevy? |
An associated type would prevent default values, which would force impl Command for Foo {
type Out = ();
fn apply(self, world: &mut World) {
}
} Instead of this: impl Command for Foo {
fn apply(self, world: &mut World) {
}
} I think normal generics are the move for this one.
First: I'll note that most of the built in commands as phrased in #17043 do not (yet) return errors, and I think updating everything is out of scope for this PR. That being said, multiple paths have been laid for built-in commands to return handled errors. The first path: all built-in commands are now exported as functions in the entity.queue_fallible(insert(Team::Blue).handle_error_with(warn())); The more common case of using the built-in shorthand functions would use the default global error handler, which defaults to panicking but can be configured: entity.insert(Team::Blue);
As mentioned in the description, the simplification of EntityCommands hinges on there being only one blanket function impl. We'd need to remove one of the two existing implementations anyway.
Good points. I'm on "team unify" as well. I'll investigate. |
Normal generics would allow for both There'd probably need to be some sort of trait bound on the output type, I guess, to ensure that it can be reduced to a |
I feel like a
Users don't have any reason to even know With
|
Ok, starting on a review of this now. Having not yet looked at the code, I'm really pleased with the high-level goals. There is just one note, before I forget:
Ok, I like this, I want default handlers. But it's weird that this is not unified with the system result handler, which I placed in the schedule runner here in #16589. Personally I don't really care where the default handler lives, and if we want to do a global-install-style handler that's fine with me, but imo we should pick one or the other.
OnceLock is probably alright here. I had some idea that we could be returning these errors to the scheduler through the sync-point system, idk if that's a viable. I don't really think this will be a niche feature though. Don't we anticipate the editor using custom error handling to populate an error console? But that's nothing to do with the implementation. Oh and off-the-cuff I would favor unifying |
The code you provided works, but it also isn't necessary. This is already supported: commands.entity(entity).queue_fallible(entity_command::insert(bundle).handle_error_with(error_handler::warn())); Which is roughly as good as commands.entity(entity).queue_fallible_with(entity_command::insert(bundle), error_handler::warn()); I personally like keeping the variants down / having one way to do things. It gets even closer ergonomically if we unify queue and queue_fallible: commands.entity(entity).queue(entity_command::insert(bundle).handle_error_with(error_handler::warn())); |
Hmm yeah we should think about how to unify these. I think applying the global handler to the system result handler by default makes some sense. I'd prefer that we tackle unification in a separate PR though. |
Worth considering all of our options. Imo the direct handler approach is nice because it gives us both granularity and nice performance (no need to queue results up, look up which handler to use, etc). I think we should start from there with essentially optimal performance and then compare against the fancier options later.
Hmmm yeah thats true. Although thats an implementation we'd own ourselves, and editor integration would likely be behind a cargo feature (so we could abstract that out from a user perspective). I think the majority of users shouldn't need to explicitly think about custom global error handlers. |
All fair points. I'm pretty happy with this, there's obviously going to be more work to do here but it will be in the long tail of gradual improvement. The changes to commands are really nice. |
Sorry, I'm still not seeing it. Wouldn't that result in an
|
I just pushed a commit that merges
Hmmm yeah I see your point. You can't customize the error handler for an internal |
Fundamentally the problem @JaySpruce has called out is that the custom error handler resolves "too early" before we call As @JaySpruce said, adding
I think we have a few options:
(1) does feel slightly inelegant / it removes functionality. But it does also make the UX simple / consistent / footgun free. If anyone has other ideas let me know. |
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.
If anyone has other ideas let me know.
Not sure I like either of these ideas, but posting them since they have some merit.
- Pass a
Result<EntityWorldMut, EntityFetchError>
instead ofEntityWorldMut
to EntityCommands. Not great, but just need to?
to get the EntityWorldMut. - Restructure things so you can attach an error handler to
commands.entity(e).with_error_handler
and then pass the EntityWorldMut to each entity command without having to reconstruct it every time. This feels like a pretty complex change and probably would need a EntityCommandQueue or maybe a intermediate buffer of components to be stored somewhere. It would probably slow down the case where you have only one entity command.
/// This will not emit a warning if the entity does not exist, essentially performing | ||
/// the same function as [`Self::despawn`] without emitting warnings. | ||
#[track_caller] | ||
pub fn try_despawn(&mut self) { | ||
self.queue(despawn(false)); | ||
self.queue(entity_command::despawn()); |
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.
This doesn't seem like it's going to suppress the error messages anymore. Should we just deprecate it and tell users to add their own error handler?
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.
Probably a comment for the other (now merged) PR, but I think I agree.
pub trait HandleError<T = ()> { | ||
/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into | ||
/// a [`Command`] that internally handles an error if it occurs and returns `()`. | ||
fn handle_error_with(self, error_handler: fn(&mut World, Error)) -> impl Command; |
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.
should error_handlers have a way of returning values? i.e. Something like rethrowing errors after adding some more info to the error message.
This is not something that needs to be fixed for this pr.
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.
Worth considering!
87b8879
to
dd985e1
Compare
I've opted to remove the Resolving some CI things and sorting out what |
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
queue
andqueue_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 tobevy_ecs::result::Error
).queue_fallible
. This is a simple direct call to thepanic()
error handler by default. Users that want to override this can enable theconfigurable_error_handler
cargo feature, then initialize the GLOBAL_ERROR_HANDLER value on startup. This is behind a flag because there might be minor overhead withOnceLock
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.queue_fallible
for most things. In the event that a custom handler is required,handle_error_with
can be used.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.EntityWorldMut::resource
and the other variants for more ergonomic resource access onEntityWorldMut
(removes the need for entity.world_scope, which also incurs entity-lookup overhead).Open Questions
queue
andqueue_fallible
into a singlequeue
which accepts both fallible and infallible commands (via the introduction of aQueueCommand
trait). Is this desirable?