Skip to content

feat(time): allow cancelling delayed commands#24179

Closed
Daedalus-Icarus wants to merge 1 commit into
bevyengine:mainfrom
Daedalus-Icarus:cancel-delayed-commands
Closed

feat(time): allow cancelling delayed commands#24179
Daedalus-Icarus wants to merge 1 commit into
bevyengine:mainfrom
Daedalus-Icarus:cancel-delayed-commands

Conversation

@Daedalus-Icarus

@Daedalus-Icarus Daedalus-Icarus commented May 7, 2026

Copy link
Copy Markdown

Objective

Solution

  • Added DelayedCommandHandle, which wraps the entity tracking a delayed command queue and can cancel it via try_despawn.
  • Added duration_with_handle and secs_with_handle while keeping the existing duration and secs APIs unchanged.
  • Preallocate the delayed queue entity when the delayed queue is created, then insert DelayedCommandQueue into that entity when delayed commands are submitted.
  • Updated the delayed commands release note with the cancellable API.

Testing

  • Added delayed_queues_can_be_cancelled, which queues a delayed spawn, cancels the handle, advances time, and confirms the spawn never runs.
  • Ran git diff --check.
  • Could not run cargo test -p bevy_time delayed_queues_can_be_cancelled locally because this environment does not have cargo or rustfmt installed.

Showcase

fn queue_spawn(mut commands: Commands) {
    let handle = {
        let mut delayed = commands.delayed();
        let (mut after_one_second, handle) = delayed.secs_with_handle(1.0);
        after_one_second.spawn(DummyComponent);
        handle
    };

    handle.cancel(&mut commands);
}

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events A-Time Involves time keeping and reporting X-Contentious There are nontrivial implications that should be thought through D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 7, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS May 7, 2026
@alice-i-cecile

Copy link
Copy Markdown
Member

@Runi-c, I would value your review here.

delayed.secs(1.5).entity(entity).insert(DummyComponent);
}

fn cancellable_delayed_spawn(mut commands: Commands) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is not realistic enough to teach the pattern properly: we want to gesture at some sort of "conditionally cancel" pattern.

@@ -18,6 +18,17 @@ fn delayed_spawn_then_insert(mut commands: Commands) {
let entity = delayed.secs(0.5).spawn_empty().id();
delayed.secs(1.5).entity(entity).insert(DummyComponent);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I'd be happy to land this in 0.19, but won't block on it. You may need to rewrite or cut the release notes later if it misses the window.

We can add this in a later release candidate though: it's additive and minor.

#[doc(hidden)]
pub use crate::{DelayedCommandsExt, Fixed, Real, Time, Timer, TimerMode, Virtual};
pub use crate::{
DelayedCommandHandle, DelayedCommandsExt, Fixed, Real, Time, Timer, TimerMode, Virtual,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DelayedCommandHandle is definitely not common enough that it should be in the prelude :)

queue: CommandQueue,
}

/// A handle that can be used to cancel delayed commands.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want to steer clear of the term "handle" specifically here. It's common in async contexts, but it means something totally different in the context of Bevy because of asset handles. Is there a synonym we can use?

/// Return a [`Commands`] whose commands will be delayed by `duration`, and
/// a handle that can be used to cancel them.
///
/// Commands queued for the same `duration` share a queue and will return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is extremely surprising, and IMO a bug. It would be good to avoid this, but if not, we should at least document how to avoid working around it (I think by generating two instances of DelayedCommands?).

/// Commands queued for the same `duration` share a queue and will return
/// the same handle.
#[must_use = "The returned Commands must be used to submit commands with this delay."]
pub fn duration_with_handle(

@alice-i-cecile alice-i-cecile May 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplication is a code smell that will lead to a combinatorial explosion of methods. We should instead have one with_handle method or the like that gets chained on.

The pattern we want to copy is EntityCommands::id()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above for notes on naming. Handle is particularly confusing as a term here.


/// A handle that can be used to cancel delayed commands.
///
/// This handle represents a [`DelayedCommandQueue`] entity. Despawning the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docs would benefit from some grounded advice on why you might want to cancel delayed commands in the context of game dev, and a dedicated doc test showing the end-to-end pattern.

@alice-i-cecile alice-i-cecile left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I quite like this, but there are some things to clean up before we merge.

Easier than I expected overall; good work :)

}

#[test]
fn delayed_queues_can_be_cancelled() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to test the idempotency of repeated cancellations, and the cancellation of commands that have already fired. Those should already pass, but they're tricky race condition bugs that we should protect against defensively.

let handle = {
let mut delayed_cmds = commands.delayed();
let (mut delayed, handle) = delayed_cmds.secs_with_handle(0.1);
delayed.spawn(DummyComponent);

@Runi-c Runi-c May 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the entity for this spawn command is allocated immediately. If the delayed command is then canceled, the allocated entity is never spawned nor freed for reuse. This was what stopped me from adding cancelling to the original PR and I don't see a solution here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you track this in the handle, and then free it? You'd need a Vec, since there could be multiple entities queued up.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite complex: see #18670 and the code added there for more context on exactly how Bevy's entity lifecycle works :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key API you'll need to clean up the memory leak of user entities is Entities::free_many :)

@Runi-c Runi-c May 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking it in the handle might be possible but there needs to be a proxy or facade for Commands::spawn and Commands::spawn_empty in order to "catch" allocations and somehow send/store them in the handle. As far as I can tell no other commands have this pre-allocation behavior (right now anyway! this would need to be carefully documented so future additions don't leak entities)

@alice-i-cecile alice-i-cecile added S-Adopt-Me The original PR author has no intent to complete this work. Pick me up! and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 7, 2026
@alice-i-cecile

Copy link
Copy Markdown
Member

This is the Github profile of NewN on Discord, who was previously warned for submitting AI generated code in opposition to our AI Policy. They were banned from Discord due to hostility and pushiness when confronted on this. They have been blocked from this repo and reported to Github as a result.

The discussion here is still useful here: I would welcome attempts to do it properly, but you will need to manually reconstruct this work from scratch to be compliant with our policy.

@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in ECS May 7, 2026
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 A-Time Involves time keeping and reporting C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Adopt-Me The original PR author has no intent to complete this work. Pick me up! X-Contentious There are nontrivial implications that should be thought through

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Add the ability to cancel delayed commands

3 participants