Skip to content

[C#] Remove unnecessary SemaphoreSlim locks for handler fields#625

Merged
stephentoub merged 2 commits intomainfrom
copilot/remove-unnecessary-semaphore-locks
Mar 2, 2026
Merged

[C#] Remove unnecessary SemaphoreSlim locks for handler fields#625
stephentoub merged 2 commits intomainfrom
copilot/remove-unnecessary-semaphore-locks

Conversation

Copy link
Contributor

Copilot AI commented Mar 2, 2026

  • Analyze _permissionHandlerLock and _userInputHandlerLock usage in dotnet/src/Session.cs
  • Remove _permissionHandlerLock SemaphoreSlim field declaration
  • Remove _userInputHandlerLock SemaphoreSlim field declaration
  • Mark _permissionHandler as volatile
  • Mark _userInputHandler as volatile
  • Simplify RegisterPermissionHandler to direct assignment
  • Simplify HandlePermissionRequestAsync to direct read
  • Simplify RegisterUserInputHandler to direct assignment
  • Simplify HandleUserInputRequestAsync to direct read
  • Simplify dispose cleanup of _permissionHandler to direct assignment
  • Build and test to verify no regressions
  • Run code review and security checks

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…rInputHandlerLock

Replace SemaphoreSlim-based locking with volatile fields for
_permissionHandler and _userInputHandler. These fields are single
reference types where reads and writes are atomic in .NET, so
locking is unnecessary. The volatile keyword ensures proper
visibility across threads.

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
@stephentoub stephentoub changed the title [WIP] Remove unnecessary SemaphoreSlim locks for handler fields [C#] Remove unnecessary SemaphoreSlim locks for handler fields Mar 2, 2026
@stephentoub stephentoub marked this pull request as ready for review March 2, 2026 13:57
@stephentoub stephentoub requested a review from a team as a code owner March 2, 2026 13:57
Copilot AI review requested due to automatic review settings March 2, 2026 13:57
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

Cross-SDK Consistency Review

I've reviewed this PR for consistency across all four SDK implementations (Node.js/TypeScript, Python, Go, and .NET).

Summary

No cross-SDK consistency issues found. This is a .NET-specific optimization that correctly addresses thread-safety in a way appropriate for C#.

Analysis

This PR removes unnecessary SemaphoreSlim locks around _permissionHandler and _userInputHandler fields, replacing them with the volatile keyword. This is a valid C#-specific optimization because:

  1. The fields are simple reference assignments (write once, read many times pattern)
  2. The volatile keyword provides sufficient memory barrier guarantees for reference type assignments in .NET
  3. This matches C# best practices for simple reference updates without complex state

Comparison with Other SDKs

I reviewed how the other SDKs handle these same handlers:

Node.js/TypeScript:

  • Uses simple field assignment without locks (lines 355, 368 in session.ts)
  • JavaScript's single-threaded execution model means no explicit synchronization needed
  • ✅ Conceptually equivalent to this PR's approach

Python:

  • Uses threading.Lock() for both handlers (lines 81, 83, 309-310, 327-328 in session.py)
  • Locks protect read and write access
  • ⚠️ Potential optimization opportunity - Python could potentially simplify like .NET, but threading semantics differ (GIL considerations)

Go:

  • Uses sync.RWMutex with separate read/write locks (lines 62, 64, 294-296, 301-303 in session.go)
  • Has dedicated getter methods (getPermissionHandler, getUserInputHandler)
  • Uses read locks for reads, write locks for writes
  • ✅ More sophisticated than .NET but appropriate for Go's concurrency model

Why This Doesn't Create Inconsistency

Each language handles this pattern according to its concurrency model:

  • .NET: volatile keyword ensures memory visibility for simple reference assignments
  • Node.js: Single-threaded, no synchronization needed
  • Python: Uses locks (potentially over-synchronized, but safe given GIL)
  • Go: Uses read/write mutexes (idiomatic for Go's goroutine model)

Recommendation

Approve - This change is correct and maintains appropriate consistency. Each SDK uses the synchronization pattern that's idiomatic and correct for its platform.

Optional follow-up: The Python SDK could potentially be simplified similarly (using simple assignment instead of locks), but that would need careful analysis of Python's threading semantics and is outside the scope of this PR.

AI generated by SDK Consistency Review Agent

Copy link
Contributor

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 simplifies the .NET CopilotSession handler registration/invocation logic by removing per-handler SemaphoreSlim locks and switching to volatile fields for permission and user-input handlers.

Changes:

  • Removed _permissionHandlerLock / _userInputHandlerLock and their lock-protected read/write paths.
  • Marked _permissionHandler and _userInputHandler as volatile and switched to direct assignment / direct read.
  • Simplified dispose cleanup for _permissionHandler (now direct null assignment).
Comments suppressed due to low confidence (3)

dotnet/src/Session.cs:314

  • Now that the SemaphoreSlim around _permissionHandler is removed, DisposeAsync can race with HandlePermissionRequestAsync: a request can read a non-null handler and invoke it while disposal is in progress, even though DisposeAsync sets the handler to null to disable callbacks. If the intended behavior is to stop invoking handlers once disposal begins, add an _isDisposed check (or similar) in HandlePermissionRequestAsync and/or use an atomic exchange + disposed check to prevent invocation after disposal starts.
    internal async Task<PermissionRequestResult> HandlePermissionRequestAsync(JsonElement permissionRequestData)
    {
        var handler = _permissionHandler;

        if (handler == null)
        {

dotnet/src/Session.cs:353

  • HandleUserInputRequestAsync reads _userInputHandler without any disposed-state check. Combined with DisposeAsync not clearing _userInputHandler, an in-flight or late userInput.request can still call back into user code after the session is disposed. Consider treating disposed sessions as unavailable here (e.g., throw ObjectDisposedException or return a deterministic failure) and ensure DisposeAsync disables this handler.

This issue also appears on line 553 of the same file.

    internal async Task<UserInputResponse> HandleUserInputRequestAsync(UserInputRequest request)
    {
        var handler = _userInputHandler;

        if (handler == null)
        {
            throw new InvalidOperationException("No user input handler registered");
        }

dotnet/src/Session.cs:556

  • DisposeAsync clears _permissionHandler but leaves _userInputHandler set. That keeps user callbacks (and any captured resources) alive after disposal and can allow user-input requests to still invoke the handler even though the session is disposed. Clear _userInputHandler (and any other handler fields) during disposal for consistent cleanup behavior.
        _eventHandlers.Clear();
        _toolHandlers.Clear();

        _permissionHandler = null;

@stephentoub stephentoub enabled auto-merge March 2, 2026 14:10
@stephentoub stephentoub added this pull request to the merge queue Mar 2, 2026
Merged via the queue into main with commit 6415c40 Mar 2, 2026
30 checks passed
@stephentoub stephentoub deleted the copilot/remove-unnecessary-semaphore-locks branch March 2, 2026 14:38
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.

4 participants