You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Improve MRTR thread-safety, cancellation, and shutdown
Harden the MRTR (Multi Round-Trip Request) implementation to correctly
handle cancellation across retries, clean shutdown, and handler lifecycle
tracking.
Thread-safety:
- Replace mutable ExchangeTask property with immutable InitialExchangeTask
and return-value data flow from ResetForNextExchange
- Use Interlocked.CompareExchange in ResetForNextExchange to validate
expected state, ensuring concurrent calls reliably fail
- Use TrySetResult as the sole atomicity gate in RequestInputAsync, with
explicit failure on concurrent exchanges
- Store SourceTcs back-reference in MrtrExchange for CAS validation
Cancellation:
- Introduce a long-lived handler CTS (encapsulated in MrtrContinuation)
that survives across retries, keeping the handler cancellable after the
original request's combinedCts is disposed
- Bridge each retry's cancellation to the handler CTS via
CancellationTokenRegistration in AwaitMrtrHandlerAsync
- Check TrySetResult/TrySetException return values on retry to detect
already-cancelled exchanges
- CTS is never disposed (like Kestrel's HttpContext.RequestAborted) to
avoid deadlock risks from Cancel/Dispose inside synchronization
primitives. CancelHandler() is the sole operation and is thread-safe.
Shutdown:
- Dispose session handler before iterating _mrtrContinuations so no new
continuations can be created during the cleanup loop
- Track MRTR handler tasks with inFlightCount + TCS drain pattern
(matching McpSessionHandler.ProcessMessagesCoreAsync) so DisposeAsync
waits for all handlers to complete before returning
- Add ObserveHandlerCompletionAsync fire-and-forget observer that logs
unhandled handler exceptions at Error level
Logging:
- Exclude IncompleteResultException from Error-level ToolCallError logging
since it is normal MRTR control flow, not an error
Simplifications:
- Flow MrtrContext via JsonRpcMessageContext property instead of
_pendingMrtrContexts ConcurrentDictionary with synchronous-before-await
assumptions
- MrtrContinuation is a lifecycle object created upfront, eliminating
CTS disposal branching, orphanedCts tracking, and post-drain cleanup
Tests (8 new):
- ServerDisposal_CancelsHandlerCancellationToken_DuringMrtr
- CancellationNotification_DuringInFlightMrtrRetry_CancelsHandler
- CancellationNotification_ForExpiredRequestId_DoesNotAffectHandler
- DisposeAsync_WaitsForMrtrHandler_BeforeReturning
- HandlerException_DuringMrtr_IsLoggedAtErrorLevel
- IncompleteResultException_IsNotLoggedAtErrorLevel
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
// TrySetResult is the sole atomicity gate. If it returns false,
67
+
// the TCS was already completed by a prior call — concurrent exchanges
68
+
// are not supported.
69
+
if(!tcs.TrySetResult(exchange))
37
70
{
38
-
thrownewInvalidOperationException("Concurrent server-to-client requests are not supported. Await each ElicitAsync, SampleAsync, or RequestRootsAsync call before making another.");
71
+
thrownewInvalidOperationException(
72
+
"Concurrent server-to-client requests are not supported. "+
73
+
"Await each ElicitAsync, SampleAsync, or RequestRootsAsync call before making another.");
0 commit comments