Skip to content

Stop leaking change token registrations - #7674

Open
ismailbennani wants to merge 1 commit into
dotnet:mainfrom
ismailbennani:issue-7673-service-discovery-change-token-leak
Open

Stop leaking change token registrations#7674
ismailbennani wants to merge 1 commit into
dotnet:mainfrom
ismailbennani:issue-7673-service-discovery-change-token-leak

Conversation

@ismailbennani

@ismailbennani ismailbennani commented Aug 3, 2026

Copy link
Copy Markdown

Fixes #7673

ServiceEndpointBuilder.Build wrapped the change tokens contributed by the endpoint providers in a CompositeChangeToken. That composite registers on its inner tokens on behalf of its consumers and releases those registrations only when it signals, so linking a token which never signals, such as the reload token of a configuration which is never reloaded, keeps the composite and everything it references alive for the lifetime of that source.

ServiceEndpointResolver evicts a watcher whose service name has not been resolved in the last ten seconds, and the next resolution creates a new one. Each of those lifecycles added a registration on the application's configuration reload token which was never released, even though the watcher disposes its own registration correctly both on refresh and in DisposeAsync.

Replace the composite with LinkedChangeToken, which registers the consumer's callback directly on each source and hands those source registrations to the consumer's own registration. Releasing them then needs nothing from the owner of the token: the consumer disposing its registration, which consumers already do, is enough.
A lone change token, which is what the Configuration and PassThrough providers produce together, is now returned as-is with no wrapper at all.

Microsoft Reviewers: Open in CodeFlow

ServiceEndpointBuilder.Build wrapped the change tokens contributed by
the endpoint providers in a CompositeChangeToken. That composite
registers on its inner tokens on behalf of its consumers and releases
those registrations only when it signals, so linking a token which
never signals, such as the reload token of a configuration which is
never reloaded, keeps the composite and everything it references alive
for the lifetime of that source.

ServiceEndpointResolver evicts a watcher whose service name has not
been resolved in the last ten seconds, and the next resolution creates
a new one. Each of those lifecycles added a registration on the
application's configuration reload token which was never released,
even though the watcher disposes its own registration correctly both
on refresh and in DisposeAsync.

Replace the composite with LinkedChangeToken, which registers the
consumer's callback directly on each source and hands those source
registrations to the consumer's own registration. Releasing them then
needs nothing from the owner of the token: the consumer disposing its
registration, which consumers already do, is enough. A lone change
token, which is what the Configuration and PassThrough providers
produce together, is now returned as-is with no wrapper at all.

Fixes dotnet#7673

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 3, 2026 00:36
@ismailbennani

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

Copilot AI left a comment

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.

Pull request overview

This PR fixes a memory leak in ServiceDiscovery endpoint resolution caused by wrapping provider change tokens in CompositeChangeToken, which can keep proxy registrations alive until the composite signals (problematic when a source token never signals, e.g., typical configuration reload tokens). It replaces that behavior with a new LinkedChangeToken that makes consumer registrations directly against the underlying sources so disposing the consumer registration releases the source registrations immediately.

Changes:

  • Replace CompositeChangeToken usage in ServiceEndpointBuilder.Build() with either the single original token (when only one exists) or a new LinkedChangeToken (when multiple exist).
  • Introduce LinkedChangeToken (internal) to link multiple change tokens without retaining proxy registrations beyond consumer lifetime.
  • Add comprehensive unit/regression tests validating callback/HasChanged behavior and that repeated watcher lifecycles do not accumulate registrations.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/LinkedChangeTokenTests.cs Adds behavioral + regression tests ensuring linked tokens and watcher lifecycles don’t accumulate registrations.
src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs Stops wrapping tokens in CompositeChangeToken; returns single token as-is or uses LinkedChangeToken for multiple tokens.
src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/LinkedChangeToken.cs Implements a linked token that ties source registrations to the consumer registration lifetime to avoid long-lived proxy registrations.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ServiceDiscovery: each ServiceEndpointWatcher lifecycle permanently leaks a change-token registration on IConfiguration

2 participants