Skip to content

fix(core): avoid deleting idle bucket created during another bucket's… - #70959

Open
Sepandard wants to merge 1 commit into
angular:mainfrom
Sepandard:fix/idle-scheduler-stale-bucket-delete
Open

Sepandard wants to merge 1 commit into
angular:mainfrom
Sepandard:fix/idle-scheduler-stale-bucket-delete

Conversation

@Sepandard

Copy link
Copy Markdown

When a bucket was emptied and removed during its own drain, and a new bucket with the same key was created in the same drain, the end of the drain deleted the new bucket from the map instead of its own. The new bucket's idle request could then no longer be cancelled on removal or on destroy, and could run after the injector was destroyed (NG0205).

Only delete the bucket from the map if it is still the one stored under that key.

Fixes #70941

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.dev application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

While an idle bucket is draining, a trigger cleanup can empty it, and remove() then deletes it from buckets. If ApplicationRef._tick() in the same drain creates a new @defer (on idle) block, add() creates a new bucket under the same key and requests a new idle callback.

When the drain finishes, the old bucket's queue is empty, so scheduleBucket() calls this.buckets.delete(key). This removes the new bucket from the map, not the old one. After that:

  • remove() can't find the bucket, so destroying the view doesn't cancel the request.
  • ngOnDestroy() only cancels buckets that are still in the map, so destroying the injector doesn't cancel it either.

When the orphaned callback runs later, it can throw NG0205: Injector has already been destroyed, or load dependencies for a destroyed defer block.

Issue Number: #70941

What is the new behavior?

At the end of a drain, the bucket is removed from the map only if the map still holds that same bucket (this.buckets.get(key) === bucket). A bucket created during the drain stays in the map, so its idle request is cancelled correctly when its callback is removed or when the scheduler is destroyed.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

… drain

When a bucket was emptied and removed during its own drain, and a new
bucket with the same key was created in the same drain, the end of the
drain deleted the new bucket from the map instead of its own. The new
bucket's idle request could then no longer be cancelled on removal or on
destroy, and could run after the injector was destroyed (NG0205).

Only delete the bucket from the map if it is still the one stored under
that key.

Fixes angular#70941
@pullapprove
pullapprove Bot requested a review from atscott September 26, 2026 07:24
@angular-robot angular-robot Bot added the area: core Issues related to the framework runtime label Sep 26, 2026
@ngbot ngbot Bot added this to the Backlog milestone Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Issues related to the framework runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: IdleScheduler drops a bucket created during another bucket's drain, so its idle request can never be cancelled (NG0205 after teardown)

1 participant