Skip to content

fix: close reentrancy gap on the contended placement-lease path - #57

Merged
NeverMorewd merged 1 commit into
mainfrom
fix/placement-lease-reentrancy
Sep 30, 2026
Merged

NeverMorewd merged 1 commit into
mainfrom
fix/placement-lease-reentrancy

Conversation

@NeverMorewd

Copy link
Copy Markdown
Owner

Summary

Replaces #56, which had to be closed because its branch (codex/region-floating) predates PR #55's squash-merge and compared against main as 15 conflicting commits. This PR carries the exact same single commit, cherry-picked onto a fresh branch off current main - clean, no conflicts, 1 commit.

PR #55 was merged before this commit finished pushing, so the fix below never landed on main.

  • ViewPlacementCoordinator: the contended-acquisition path (EnterSlowAsync) set its reentrancy marker via an AsyncLocal<int> write made inside an async method — which never becomes visible to that method's own caller once it returns (the compiler-generated state machine restores the caller's captured ExecutionContext on resume, even when the method never truly suspends). This reproduced both the original self-deadlock the reentrant-lease fix was meant to prevent, and a spurious InvalidOperationException from RestorePlacementAsync's IsHeld() guard, whenever the first lease acquisition for a region happened to be genuinely contended. Replaced the AsyncLocal<int> depth counter with an AsyncLocal<Guid> flow id established once per logical operation via a new EnsureFlow(), called synchronously at every entry point before EnterAsync — since AsyncLocal always propagates correctly parent→child, only the old child→caller-after-suspension direction was broken. The "who holds this region" marker itself is a plain field on the shared per-region gate object, not an AsyncLocal, so writing it from inside the async slow path is fine.
  • WeakUnloadObserver: synchronized the ConditionalWeakTable read-modify-write sequence (Add throws on a concurrent duplicate-key race).
  • RegionNavigationService: set up the indicator host in RequestNavigateAsync before job scheduling and lease acquisition — not just inside CreateNavigateTask — so an error indicator can still be shown if either of those throw before reaching it.

Added ViewPlacementCoordinatorTests covering both the fast-path reentrant case and the previously-broken contended-then-reentrant case.

Test plan

  • dotnet build tests/AsyncNavigation.Tests (Release) — 0 errors
  • dotnet test tests/AsyncNavigation.Tests — 157 passed
  • dotnet test tests/AsyncNavigation.E2E.Tests — 9 passed
  • New ViewPlacementCoordinatorTests (2 tests) reproduce and verify the fix for the contended-lease self-deadlock/IsHeld gap
  • This exact commit already verified green on all 3 OS via manual CI dispatch (see fix: close reentrancy gap on the contended placement-lease path #56)

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

ViewPlacementCoordinator.EnterAsync's fast/reentrant paths write the
holder marker in a plain (non-async) method body so it stays visible to
the caller, but the genuinely-contended path delegated to an async
EnterSlowAsync helper - and since AsyncLocal writes made inside a called
async method never become visible to that method's own caller once it
returns, a lease acquired via that slow path was never recognized as
held by IsHeld() or by a nested reentrant EnterAsync call on the same
region, reproducing both the spurious InvalidOperationException from
RestorePlacementAsync's lease check and the original self-deadlock the
reentrant fast path was built to fix.

Replace the AsyncLocal<int> depth counter with an AsyncLocal<Guid> flow
id established once per logical operation via a new EnsureFlow(),
called synchronously at every entry point before EnterAsync (parent to
child AsyncLocal flow always works; only child-to-caller propagation
after a suspension does not). The "who currently holds this region"
marker itself is a plain field on the shared per-region gate, not an
AsyncLocal, so setting it from inside the async slow path is fine -
only the flow id needs the caller-established treatment. Added
ViewPlacementCoordinatorTests covering both the fast-path reentrant case
and the previously-broken contended-then-reentrant case.

Also: synchronize WeakUnloadObserver's ConditionalWeakTable
read-modify-write (Add throws on a concurrent duplicate-key race), and
set up the indicator host in RequestNavigateAsync before job scheduling
and lease acquisition - not just inside CreateNavigateTask - so an error
indicator can still be shown if either of those throw before reaching it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@NeverMorewd
NeverMorewd merged commit a77edc8 into main Sep 30, 2026
16 checks passed
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.

1 participant