Skip to content

Commit 4fc848c

Browse files
committed
test
1 parent 7b252d3 commit 4fc848c

7 files changed

Lines changed: 207 additions & 103 deletions

File tree

eval_protocol/mcp/client/connection.py

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from contextlib import AsyncExitStack
1313
from typing import Any, Dict, List, Optional, Tuple
1414

15+
import httpx
1516
from mcp.client.session import ClientSession
1617
from mcp.client.streamable_http import streamablehttp_client
1718

@@ -26,6 +27,9 @@ class MCPConnectionManager:
2627
def __init__(self):
2728
self._tools_cache: Dict[str, List[Dict]] = {}
2829
self._tools_cache_lock = asyncio.Lock()
30+
# Shared HTTP client for control plane requests with high connection limits
31+
self._shared_client: Optional[httpx.AsyncClient] = None
32+
self._client_lock = asyncio.Lock()
2933

3034
async def initialize_session(self, session: MCPSession) -> None:
3135
"""
@@ -114,6 +118,12 @@ async def _prewarm_tools_cache(self, session: MCPSession) -> None:
114118
"""
115119
cache_key = session.base_url
116120

121+
# Fast path: if cache already exists, return immediately (no lock)
122+
if cache_key in self._tools_cache:
123+
logger.debug(f"Tools cache already exists for {cache_key}")
124+
return
125+
126+
# Slow path: need to create cache (use lock only for creation)
117127
async with self._tools_cache_lock:
118128
# Only fetch tools if not already cached for this base_url
119129
if cache_key not in self._tools_cache:
@@ -244,21 +254,33 @@ async def get_initial_state(self, session: MCPSession) -> Any:
244254
# Use shorter timeout for playback mode, longer timeout for high-concurrency initialization
245255
# (50+ concurrent sessions need more time for initial state setup)
246256
timeout = 3.0 if hasattr(session, "_is_playback_mode") and session._is_playback_mode else 15.0
247-
async with httpx.AsyncClient(timeout=timeout) as client:
248-
initial_state_response = await client.get(
249-
f"{base_url}/control/initial_state",
250-
headers=headers,
251-
timeout=timeout,
257+
258+
# TIMING: Get shared client
259+
client_start = __import__("time").time()
260+
client = await self._get_shared_client(timeout)
261+
client_time = __import__("time").time() - client_start
262+
logger.info(
263+
f"DEBUG_CLIENT: Getting shared client took {client_time:.3f}s for {session.session_id}"
264+
)
265+
266+
# TIMING: HTTP request with shared client
267+
request_start = __import__("time").time()
268+
initial_state_response = await client.get(
269+
f"{base_url}/control/initial_state",
270+
headers=headers,
271+
timeout=timeout,
272+
)
273+
request_time = __import__("time").time() - request_start
274+
logger.info(f"DEBUG_REQUEST: HTTP request took {request_time:.3f}s for {session.session_id}")
275+
if initial_state_response.status_code == 200:
276+
initial_observation = initial_state_response.json()
277+
logger.info(
278+
f"Session {session.session_id}: ✅ Successfully fetched session-aware initial state from control plane endpoint"
279+
)
280+
else:
281+
logger.warning(
282+
f"Control plane initial state endpoint returned {initial_state_response.status_code}"
252283
)
253-
if initial_state_response.status_code == 200:
254-
initial_observation = initial_state_response.json()
255-
logger.info(
256-
f"Session {session.session_id}: ✅ Successfully fetched session-aware initial state from control plane endpoint"
257-
)
258-
else:
259-
logger.warning(
260-
f"Control plane initial state endpoint returned {initial_state_response.status_code}"
261-
)
262284
except httpx.TimeoutException:
263285
logger.warning(f"Control plane initial state endpoint timed out after {timeout}s")
264286
except Exception as e:
@@ -579,3 +601,47 @@ async def close_session(self, session: MCPSession) -> None:
579601
finally:
580602
session._exit_stack = None
581603
session._mcp_session = None
604+
605+
async def _get_shared_client(self, timeout: float) -> httpx.AsyncClient:
606+
"""
607+
Get or create a shared HTTP client with high connection limits for concurrent requests.
608+
609+
Args:
610+
timeout: Timeout for requests
611+
612+
Returns:
613+
Shared httpx.AsyncClient instance
614+
"""
615+
# Fast path: if client exists and is not closed, return it immediately
616+
if self._shared_client is not None and not self._shared_client.is_closed:
617+
return self._shared_client
618+
619+
# Slow path: need to create client (use lock only for creation)
620+
async with self._client_lock:
621+
# Double-check pattern: another task might have created it while we waited
622+
if self._shared_client is None or self._shared_client.is_closed:
623+
# Create HTTP client with high connection limits for concurrent initial state requests
624+
limits = httpx.Limits(
625+
max_keepalive_connections=None, # Unlimited keep-alive connections
626+
max_connections=None, # Unlimited total connection pool size
627+
keepalive_expiry=30.0, # Keep connections alive for 30s
628+
)
629+
630+
self._shared_client = httpx.AsyncClient(
631+
timeout=timeout,
632+
limits=limits,
633+
# Enable connection pooling and keep-alive
634+
http2=False, # Disable HTTP/2 for better connection pooling with many concurrent requests
635+
)
636+
logger.info(
637+
"Created shared HTTP client with unlimited connection limits for MCP control plane requests"
638+
)
639+
640+
return self._shared_client
641+
642+
async def close_shared_client(self):
643+
"""Close the shared HTTP client when shutting down."""
644+
async with self._client_lock:
645+
if self._shared_client and not self._shared_client.is_closed:
646+
await self._shared_client.aclose()
647+
self._shared_client = None

eval_protocol/mcp/execution/manager.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import time
1414
from concurrent.futures import ThreadPoolExecutor, as_completed
1515
from dataclasses import asdict, dataclass
16+
from datetime import datetime
1617
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
1718

1819
from openai.types import CompletionUsage
@@ -188,6 +189,11 @@ async def _execute_rollout(
188189
"""
189190
session = envs.sessions[rollout_idx]
190191
dataset_row = envs.dataset_rows[rollout_idx]
192+
rollout_start = time.time()
193+
elapsed_from_main_start = rollout_start - start_time
194+
logger.info(
195+
f"DEBUG4. Starting rollout {dataset_row.id} at {datetime.fromtimestamp(rollout_start).strftime('%H:%M:%S.%f')[:-3]} (+{elapsed_from_main_start:.3f}s from start)"
196+
)
191197

192198
# Initialize trajectory
193199
trajectory = Trajectory(
@@ -210,7 +216,11 @@ async def _execute_rollout(
210216
},
211217
)
212218

219+
temp_start = time.time()
213220
current_observation, tool_schema = await envs.reset(session)
221+
logger.info(
222+
f"DEBUG6: User simulator get_init_state took {time.time() - temp_start:.3f}s for {session.session_id}"
223+
)
214224
system_prompt = dataset_row.system_prompt
215225

216226
# Record initial observation
@@ -244,7 +254,9 @@ async def _execute_rollout(
244254

245255
usage_stats_list: List[CompletionUsage] = []
246256

247-
logger.info(f"🎯 Starting rollout {rollout_idx} in thread {threading.current_thread().name}")
257+
logger.info(
258+
f"DEBUG7: 🎯 Starting rollout {dataset_row.id} in thread {threading.current_thread().name}, {datetime.fromtimestamp(time.time()).strftime('%H:%M:%S.%f')[:-3]} (+{time.time() - rollout_start:.3f}s from start)"
259+
)
248260

249261
# Run rollout loop for this specific environment
250262
step = 0
@@ -264,9 +276,13 @@ async def _execute_rollout(
264276
# Last message was agent, simulated user response
265277
if user_simulator_messages and isinstance(user_simulator_messages[-1], AssistantMessage):
266278
# Generate user response using the simulator
279+
temp_start1 = time.time()
267280
user_message, user_simulator_state = user_simulator.generate_next_message(
268281
user_simulator_messages[-1], user_simulator_state
269282
)
283+
logger.info(
284+
f"DEBUG8: User simulator generate_next_message took {time.time() - temp_start1:.3f}s for {dataset_row.id}"
285+
)
270286
user_content = user_message.content if user_message.content else ""
271287

272288
user_prompt = envs.format_user_prompt(rollout_idx, user_content)
@@ -279,7 +295,9 @@ async def _execute_rollout(
279295

280296
# In each turn: keep looping until assistant is ready to provide final response
281297
while not turn_completed and not trajectory.terminated:
298+
temp_start2 = time.time()
282299
tool_calls, usage_stats = await policy(tool_schema, rollout_idx, conversation_history)
300+
logger.info(f"DEBUG9: Policy took {time.time() - temp_start2:.3f}s for {dataset_row.id}")
283301

284302
# If no tool call is generated, turn is finished
285303
if len(tool_calls) == 1:
@@ -296,7 +314,9 @@ async def _execute_rollout(
296314
for tool_call in tool_calls:
297315

298316
# Execute tool call for this environment
317+
temp_start3 = time.time()
299318
observation, reward, rollout_end, info = await envs.step(rollout_idx, tool_call)
319+
logger.info(f"DEBUG10: Env step took {time.time() - temp_start3:.3f}s for {dataset_row.id}")
300320

301321
tool_response = envs.format_tool_response(observation)
302322

@@ -444,6 +464,9 @@ async def _execute_rollout(
444464
logger.info(
445465
f"✅ Rollout {rollout_idx} completed: {trajectory.steps} steps, reward: {trajectory.total_reward:.2f}, termination: {trajectory.termination_reason}, in thread {threading.current_thread().name}"
446466
)
467+
logger.info(
468+
f"DEBUG11: Rollout {dataset_row.id} completed at {datetime.fromtimestamp(time.time()).strftime('%H:%M:%S.%f')[:-3]} (+{time.time() - rollout_start:.3f}s from start)"
469+
)
447470
return trajectory
448471

449472
async def _get_control_plane_status(self, session) -> Optional[Dict[str, Any]]:

eval_protocol/mcp/execution/policy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .base_policy import LLMBasePolicy
2020

2121
logger = logging.getLogger(__name__)
22+
litellm._turn_on_debug()
2223

2324

2425
class LiteLLMPolicy(LLMBasePolicy):

eval_protocol/pytest/default_mcp_gym_rollout_processor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,14 @@ async def default_mcp_gym_rollout_processor(
213213
)
214214

215215
# Create MCP environments directly from evaluation_rows
216+
print("DEBUG1", time.time())
216217
envs = await ep.make(
217218
"http://localhost:9700/mcp/",
218219
evaluation_rows=rows,
219220
model_id=policy.model_id,
220221
)
222+
print("DEBUG2", time.time())
223+
print("max_concurrent_rollouts", config.max_concurrent_rollouts)
221224

222225
# Run rollout with environments and policy
223226
evaluation_rows = await ep.rollout(

monitor_connections.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
3+
echo "Monitoring connections to port 9700..."
4+
echo "Press Ctrl+C to stop"
5+
6+
while true; do
7+
count=$(netstat -an | grep :9700 | grep ESTABLISHED | wc -l)
8+
timestamp=$(date '+%H:%M:%S')
9+
echo "$timestamp: $count connections to port 9700"
10+
sleep 1
11+
done

0 commit comments

Comments
 (0)