-
-
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
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
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.
87b8879
to
dd985e1
Compare
I've opted to remove the Resolving some CI things and sorting out what |
I've made the I think this PR is now ready to merge. |
command: impl EntityCommand<M>, | ||
default_error_handler: fn(&mut World, CommandError), | ||
command: C, | ||
error_handler: fn(&mut World, 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.
We're using boxed errors here, but in my ideal world this "add an error handler to a specific command" would handle receive the non-erased error type here. That gives us much better recovery options, beyond just log / panic / ignore.
Can we change that?
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 do agree that this could probably handle non-erased error types instead in the "specific-command error handler" case. I spent a bit of time investigating and got this largely working. I added a type Error
associated type to HandleError
and CommandWithEntity
, and added an ErrorHandler<E>
type alias for a generic error handler function. This works for adding error handlers to specific commands, but there is a problem: The "default error handler" fundamentally requires the general-purpose bevy Error
type, making it (not directly) usable for a signature that expects a specific error type. We want queue
to be "default error conversion compatible" and queue_handled
to require the concrete type. I'm reasonably certain this requires a duplication of traits.
This is a big enough change that I think my preference is to defer it to a later 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.
Good cleanup! The "EntityCommands ~= EntityWorldMut` changes are great, and I like the new helper methods on EntityWorldMut.
Requests for better documentation (shocking), and a more serious question about getting the concrete error type in queue_handled. I think this is an improvement and I wouldn't be upset to see this merged, but I think that getting that last detail right is valuable.
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?