Skip to content

Apollo Federation: _entities resolver throws ObjectDisposedException in ResolverTask.CompleteUnsafeAsync under concurrency (15.1.x) #9971

Closed

Description

@sebzz

Product

Hot Chocolate

Version

15.1.17

Link to minimal reproduction

https://github.com/sebzz/hc-issue-repro

Steps to reproduce

Clone the minimal repro: https://github.com/sebzz/hc-issue-repro
.NET 10 console app - references HotChocolate.AspNetCore 15.1.17, HotChocolate.ApolloFederation 15.1.17

Run dotnet run

It builds an in-process executor with one federated entity (a [Key("id")] type with a [ReferenceResolver] that awaits Task.Yield()), then fires the federation _entities query from 32 concurrent tasks × 10,000 iterations (320,000 total), forcing GC periodically so faulted tasks surface via the finalizer.

A TaskScheduler.UnobservedTaskException handler counts exceptions originating from ResolverTask.CompleteUnsafeAsync. Within ~320K requests on a multi-core machine, the handler captures the ObjectDisposedException. The race is statistical — re-run, or raise Iterations/Concurrency, if it does not fire on the first run on a slow or single-core machine.

What is expected?

The _entities query executes and returns normally with no unobserved task exceptions. Per-representation context clones created inside EntitiesResolver.ResolveAsync should complete their cleanup (clone.CompleteUnsafeAsync) without touching an OperationContext that has already been returned to the pool.

What is actually happening?

Under concurrency, the _entities resolver crashes during cleanup.

For each representation, EntitiesResolver clones the request context and queues the clone's cleanup to run when the parent resolver finishes. But the parent signals "done" to the scheduler before that cleanup runs. If _entities was the last work item, another thread sees "done" and recycles the OperationContext into the pool — so the cleanup then runs against a pooled context and throws ObjectDisposedException. No one awaits the faulted task, so it surfaces on the finalizer thread as an UnobservedTaskException.

Responses stay correct, but prod logs this ~8–11K times/day. Not reproducible on 16.1.4 (0 in 1M requests)

Relevant log output

System.ObjectDisposedException: Cannot access a disposed object.
  Object name: 'The specified object was not initialized and is no longer usable.'.
     at HotChocolate.Execution.Processing.WorkScheduler.Complete(IExecutionTask task)
     at HotChocolate.Execution.Processing.Tasks.ResolverTask.CompleteUnsafeAsync()
     at HotChocolate.Execution.Processing.MiddlewareContext.ExecuteCleanupTasksAsync()
     at HotChocolate.Execution.Processing.Tasks.ResolverTask.ExecuteAsync(CancellationToken cancellationToken)

Additional context

No response

Activity

  1. added theissue type on Jun 23, 2026
  2. self-assigned this
    on Jun 24, 2026
  3. erf1nd0r commented on Sep 7, 2026

    @erf1nd0r

    We are seeing this on 15.1.17 as well, through the Relay node(id:) field rather than _entities. NodeFieldResolvers clones the resolver context the same way EntitiesResolver does, so any Relay @refetchable fragment on a Node type hits the same race. The 15.1.18 tag and main-version-15 still have the old ordering in ResolverTask.Execute.cs. I opened #10364 with a cherry-pick of 6e5b640 onto main-version-15, and confirmed locally that the backported test fails on that branch without the fix and passes with it. Would you consider including it in the next 15.1.x patch release?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions