Skip to content

Use async ChangeToken.OnChange overload in FileConfigurationProvider - #130492

Merged
svick merged 2 commits into
dotnet:mainfrom
svick:use-async-changetoken-onchange
Jul 17, 2026
Merged

svick merged 2 commits into
dotnet:mainfrom
svick:use-async-changetoken-onchange

Conversation

@svick

@svick svick commented Jul 10, 2026 •

Copy link
Copy Markdown
Member

Uses the new async (Func<Task>) overload of ChangeToken.OnChange (see #69099) in FileConfigurationProvider.

The reload callback previously blocked a thread for the whole reload delay via Thread.Sleep(Source.ReloadDelay). It now awaits Task.Delay(...) instead, so no thread is blocked while waiting out the debounce delay before reloading:

_changeTokenRegistration = ChangeToken.OnChange(
    () => Source.FileProvider.Watch(Source.Path!),
    async () =>
    {
        await Task.Delay(Source.ReloadDelay).ConfigureAwait(false);
        Load(reload: true);
    });

Why only this call site

The other in-repo callers of ChangeToken.OnChange (ConfigurationRoot, ConfigurationManager, OptionsMonitor) invoke short, synchronous, non-blocking consumers (RaiseChanged / InvokeChanged) and gain nothing from the async overload, so they are intentionally left unchanged.

A closely related change is at dotnet/aspnetcore#67732.

Note

This PR description was generated by GitHub Copilot.

Replace the blocking Thread.Sleep in the reload callback with an
awaited Task.Delay, using the new async Func<Task> overload of
ChangeToken.OnChange (dotnet#69099). This avoids blocking a thread for the
duration of the reload delay.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 059bf1b8-31e2-4608-a038-210880f90d06
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-extensions-configuration
See info in area-owners.md if you want to be subscribed.

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 updates FileConfigurationProvider’s file-change reload callback to use the async ChangeToken.OnChange overload and replace a blocking Thread.Sleep debounce with an awaited Task.Delay, aiming to avoid tying up a thread during the reload delay.

Changes:

  • Replace Thread.Sleep(Source.ReloadDelay) with await Task.Delay(Source.ReloadDelay).ConfigureAwait(false) inside the reload callback.
  • Switch the callback passed to ChangeToken.OnChange to an async lambda (binding to the Func<Task> overload).
  • Update using directives accordingly (System.Threading → System.Threading.Tasks).

Copilot AI review requested due to automatic review settings July 13, 2026 12:35

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

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

@tarekgh tarekgh 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.

LGTM, Thanks!

@tarekgh tarekgh added this to the 11.0.0 milestone Jul 13, 2026
@svick
svick requested review from mrek-msft and rosebyte July 16, 2026 13:52
@svick

svick commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

/ba-g failure is #130678 and is unrelated

@svick
svick merged commit 8bce13d into dotnet:main Jul 17, 2026
83 of 88 checks passed
@svick
svick deleted the use-async-changetoken-onchange branch July 17, 2026 13:53
@dotnet-milestone-bot dotnet-milestone-bot Bot modified the milestones: 11.0.0, 11.0-preview7 Jul 18, 2026
svick added a commit that referenced this pull request Jul 21, 2026
…tems (#130627)

Fixes #121475.

## Problem

When a Generic Host app lives on a file system that can't be watched —
e.g. a WSL path (`\\wsl.localhost\...`) accessed via `dotnet` from
Windows, or other mounted/network drives — it crashes with a
`StackOverflowException` after a while.

On such a file system, `FileSystemWatcher` keeps failing to start and
raises the same `Error` every time it's enabled. In
`PhysicalFilesWatcher.OnError` this cancels the change tokens, which
fires the `ChangeToken.OnChange` registration set up by
`FileConfigurationProvider` (and similar consumers). That registration's
producer re-creates a token via `CreateFileChangeToken`, which
re-enables the watcher, which raises the same `Error` again — an
unbounded cancel -> re-register -> re-enable -> error cycle that
recurses until the stack overflows.

## Fix

`PhysicalFilesWatcher.OnError` now distinguishes persistent failures
from recoverable ones:

- The **first** occurrence of an error is reported as before.
- An **identical recurrence** — same exception type and OS error code
(`Win32Exception.NativeErrorCode`, otherwise `HResult`) — with **no
change delivered in between** is **suppressed** (the tokens aren't
cancelled), which breaks the loop.
- `InternalBufferOverflowException` (the watcher is alive but dropped
events, so consumers must rescan) and `DirectoryNotFoundException` (the
watched directory was deleted/moved) are **always** reported and reset
the detection.
- Any **delivered change** resets the remembered error, so a transient
error that later recovers isn't mistaken for a persistent one.

This is detection-only: on a genuinely unwatchable file system the app
no longer crashes, but file-change-based reload is simply inactive there
(matching the documented behavior that `FileSystemWatcher` "is
ineffective in some scenarios such as mounted drives", whose remedy is
`DOTNET_USE_POLLING_FILE_WATCHER`). Automatically falling back to
polling is probably undesirable.

## Relationship to #130492

#130492 (switching `FileConfigurationProvider` to the async
`ChangeToken.OnChange` overload) already works around the reported
crash: awaiting `Task.Delay(ReloadDelay)` unwinds the stack between
iterations, so the recursion becomes a (slow) async loop instead of
growing the stack. That mitigates the `StackOverflowException` for the
default `ReloadDelay` (250 ms), but it doesn't address the underlying
churn (the watcher is still re-enabled and re-errors on a loop), and
it's defeated by `ReloadDelay == 0` (`Task.Delay(0)` completes
synchronously, restoring the synchronous recursion). This PR fixes the
root cause in `PhysicalFilesWatcher`, independent of the consumer's
overload or reload delay.

## Known limitation

macOS raises buffer overflow as a plain
`IOException(SR.FSW_BufferOverflow)` (with `HResult` set to the FSEvents
flags), not `InternalBufferOverflowException` like Windows/Linux. So the
"always report" carve-out doesn't recognize it, and a sustained macOS
overflow with no delivered change in between could be suppressed. A
clean follow-up is to make the macOS `FileSystemWatcher` raise
`InternalBufferOverflowException` for consistency:
#130637.

## Tests

Added tests in `PhysicalFilesWatcherTests` that drive `OnError` via
`MockFileSystemWatcher.CallOnError`:
- an identical error recurrence is suppressed (covering both
`Win32Exception`/`NativeErrorCode` and `IOException`/`HResult`);
- a different error code is still reported;
- `InternalBufferOverflowException` and `DirectoryNotFoundException` are
always reported;
- a delivered change resets detection so the same error is reported
again;
- an `Error` with no exception is always reported.

> [!NOTE]
> This pull request was created by GitHub Copilot.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants