Skip to content

Add fork detection for upgradeable read lock context#1594

Open
lifengl wants to merge 1 commit into
mainfrom
dev/lifengl/detect-ur-lock-fork
Open

Add fork detection for upgradeable read lock context#1594
lifengl wants to merge 1 commit into
mainfrom
dev/lifengl/detect-ur-lock-fork

Conversation

@lifengl
Copy link
Copy Markdown
Member

@lifengl lifengl commented Jun 5, 2026

Summary

Adds a virtual method OnLockForkDetected() on AsyncReaderWriterLock that is called when a nested read lock request is detected from a forked execution context of an upgradeable read lock holder. This enables consumers like CPS to override the method and send telemetry to identify fork scenarios.

Background

This addresses Watson crash bug #1761459 in CPS (ProjectLockService.OnCompleted), where IsLockSupportingContext can flip between IsCompleted and OnCompleted when a nesting UR lock is released by another thread. The root cause is a race condition that occurs when code holding an upgradeable read lock uses ConfigureAwait(false) and then requests a nested read lock from the forked context.

Design

The existing write lock already has a fork check in TryIssueLock (lines 1110-1113) that throws InvalidOperationException. For the upgradeable read case, we use a non-breaking approach: a virtual method hook instead of throwing, since throwing would be a behavior change affecting many consumers.

Fork detection criteria (all must be true):

  • A read lock is being requested (!previouslyQueued — first request only, to avoid duplicate telemetry from PendAwaiter retries)
  • The nesting chain contains an active upgradeable read lock (hasUpgradeableRead)
  • No write lock in the nesting chain (!hasWrite — the write fork path already handles that)
  • The calling thread is MTA (CanCurrentThreadHoldActiveLock — excludes STA/UI thread)
  • The calling thread lacks NonConcurrentSynchronizationContext

The virtual method is called outside syncObject to avoid reentrancy concerns.

Changes

  • AsyncReaderWriterLock.cs: Added OnLockForkDetected() virtual method and fork detection logic in TryIssueLock
  • AsyncReaderWriterLockTests.cs: Added 3 tests and a LockWithForkDetection helper class:
    • UpgradeableReadLockForksAndAsksForReadLock — verifies detection fires on forked UR context
    • UpgradeableReadLockDoesNotForkOnCorrectContext — verifies no false positive on correct context
    • ReadLockForkFromReadOnlyContextDoesNotTriggerDetection — verifies no detection for read-only forks

Test Results

All 153 AsyncReaderWriterLockTests pass (149 succeeded, 4 pre-existing skips).

Add a virtual method OnLockForkDetected() that is called when a nested
read lock request is detected from a forked execution context of an
upgradeable read lock holder. This parallels the existing write lock
fork check in TryIssueLock (line 1110-1113) but is non-breaking:
instead of throwing, it calls a virtual method that consumers can
override for telemetry/diagnostics.

The fork is detected when:
- A read lock is requested (!previouslyQueued, first request only)
- The nesting chain contains an active upgradeable read lock
- The calling thread is MTA (CanCurrentThreadHoldActiveLock)
- The calling thread lacks NonConcurrentSynchronizationContext

The virtual method is called outside syncObject to avoid reentrancy
concerns.

This addresses the root cause of Watson crash bug #1761459 in CPS,
where IsLockSupportingContext can flip between IsCompleted and
OnCompleted when a nesting UR lock is released by another thread.
The detection enables CPS (and other consumers) to identify and
diagnose fork scenarios via telemetry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lifengl lifengl marked this pull request as ready for review June 5, 2026 22:02
Copilot AI review requested due to automatic review settings June 5, 2026 22:02
Copy link
Copy Markdown

Copilot AI left a comment

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 adds a diagnostic hook to AsyncReaderWriterLock to detect a specific execution-context fork pattern: requesting a nested read lock while an upgradeable read lock is active in the nesting chain, but the current thread lacks the required NonConcurrentSynchronizationContext. This enables downstream consumers (e.g., CPS) to log telemetry for these fork scenarios without introducing a breaking behavioral change (like throwing).

Changes:

  • Added a new protected virtual void OnLockForkDetected() hook on AsyncReaderWriterLock.
  • Implemented fork detection for nested read lock requests under an active upgradeable read lock in TryIssueLock, invoking the hook outside syncObject.
  • Added tests validating detection triggers for upgradeable-read forks, and does not trigger for correct-context nesting or read-only forks.

Reviewed changes

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

File Description
src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs Adds OnLockForkDetected() and detection logic in TryIssueLock for upgradeable-read forked nested read requests.
test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterLockTests.cs Adds coverage validating detection behavior (trigger + two non-trigger scenarios) via a derived lock that counts callbacks.

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.

2 participants