Use latest-wins MCP manager replacement (#27259)

## Summary

We originally addressed startup prewarming holding the read side of
`RwLock<McpConnectionManager>` by snapshotting tool-list state. Review
feedback identified the broader ownership problem: the outer
synchronization should only publish or retrieve the current manager,
while MCP operations rely on the manager's internal synchronization. A
follow-up preserved operation retirement with a separate gate, but
further review questioned whether that synchronization was actually
required and whether we could support latest-wins replacement instead.

This PR now stores the current MCP manager in `ArcSwap`. Each operation
uses `load_full()` to obtain an owned `Arc<McpConnectionManager>`, then
performs MCP I/O without retaining the publication mechanism. Refresh
cancels obsolete startup work, constructs a replacement, and atomically
publishes it. New operations see the latest manager, while operations
that already loaded the previous manager retain a valid handle. Refresh
happens at a turn boundary, so there should be no active user tool calls
to drain.

Git history supports dropping the outer `RwLock`. It was introduced in
`03ffe4d595` on November 17, 2025 for non-blocking MCP startup: the
session published an empty manager, startup initialized that same object
while holding the write lock, and readers waited for initialization.
`7cd2e84026` on February 19, 2026 removed that two-phase initialization
in favor of constructing a fresh manager and swapping it in, explicitly
noting that `Option` or `OnceCell` could replace the placeholder design.
Hot reload later reused the existing lock to publish a replacement, but
I found no indication that the lock was introduced to guarantee
in-flight tool calls finish before refresh or shutdown.

Terminal shutdown remains separate from refresh: it aborts startup
prewarming and active tasks before shutting down the current manager, so
tool calls may be interrupted and no model WebSocket work continues
after shutdown. Focused regression coverage exercises pending tool-list
cancellation, deferred refresh, and startup-prewarm shutdown.
This commit is contained in:
Charlie Marsh
2026-06-10 08:33:21 -07:00
committed by GitHub
Unverified
parent d2f6d23c6c
commit 41b4fabbb4
20 changed files with 156 additions and 192 deletions
+4 -36
View File
@@ -452,7 +452,7 @@ async fn delegated_mcp_guardian_abort_returns_synthetic_decline_answer() {
}
#[tokio::test]
async fn delegated_mcp_user_reviewer_waits_for_metadata_lookup() {
async fn delegated_mcp_user_reviewer_returns_none_without_metadata() {
let (parent_session, parent_ctx, _rx_events) =
crate::session::tests::make_session_and_context_with_rx().await;
let pending_mcp_invocations = Arc::new(Mutex::new(HashMap::from([(
@@ -464,21 +464,6 @@ async fn delegated_mcp_user_reviewer_waits_for_metadata_lookup() {
},
)])));
let cancel_token = CancellationToken::new();
let manager = Arc::clone(&parent_session.services.mcp_connection_manager);
let (manager_locked_tx, manager_locked_rx) = std::sync::mpsc::sync_channel(0);
let (release_manager_tx, release_manager_rx) = std::sync::mpsc::sync_channel(0);
let manager_lock = tokio::task::spawn_blocking(move || {
let _manager_guard = manager.blocking_write();
manager_locked_tx
.send(())
.expect("manager lock receiver should remain open");
release_manager_rx
.recv()
.expect("manager lock release sender should remain open");
});
manager_locked_rx
.recv_timeout(Duration::from_secs(1))
.expect("manager write lock should be acquired");
let event = RequestUserInputEvent {
call_id: "call-1".to_string(),
@@ -498,24 +483,7 @@ async fn delegated_mcp_user_reviewer_waits_for_metadata_lookup() {
&pending_mcp_invocations,
&event,
&cancel_token,
);
tokio::pin!(response);
assert!(
timeout(Duration::from_millis(100), &mut response)
.await
.is_err(),
"manual reviewer should wait for MCP metadata"
);
release_manager_tx
.send(())
.expect("manager lock holder should remain open");
manager_lock
.await
.expect("manager lock task should not panic");
assert_eq!(
timeout(Duration::from_secs(1), response)
.await
.expect("manual reviewer should finish after MCP metadata lookup"),
None
);
)
.await;
assert_eq!(response, None);
}