11import asyncio
2+ import atexit
23import logging
4+ import subprocess
35import time
46import uuid
7+ from contextlib import asynccontextmanager
58from enum import Enum
69from typing import Optional
710
811import aiohttp
12+ import requests
913import yaml
1014from fastapi import BackgroundTasks , FastAPI , HTTPException
1115from pydantic import BaseModel
1216
1317from auto_agent .constants import EVAL_SERVER_PORT
14- from auto_agent .server_utils .tpu_server import get_tpu_version
1518
1619logging .basicConfig (
1720 level = logging .INFO ,
1821 format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ,
1922 datefmt = "%Y-%m-%d %H:%M:%S" ,
2023)
2124
22- app = FastAPI (title = "Agent Evaluation Server" , version = "1.0.0" )
25+
26+ @asynccontextmanager
27+ async def lifespan (app : FastAPI ):
28+ # Start background task to clean up old completed tasks (TTL: 30 mins / 1800 seconds)
29+ cleanup_task = asyncio .create_task (
30+ clean_old_tasks_periodically (ttl_seconds = 1800 , check_interval_seconds = 60 )
31+ )
32+ yield
33+ # Clean up background task on shutdown
34+ cleanup_task .cancel ()
35+ try :
36+ await cleanup_task
37+ except asyncio .CancelledError :
38+ pass
39+ logging .info ("Shutting down eval_server, cleaning up tunnels..." )
40+ evaluator ._cleanup_tunnels ()
41+
42+
43+ async def clean_old_tasks_periodically (
44+ ttl_seconds : int = 1800 , check_interval_seconds : int = 60
45+ ):
46+ """Periodically removes completed tasks older than ttl_seconds."""
47+ while True :
48+ try :
49+ await asyncio .sleep (check_interval_seconds )
50+ now = time .time ()
51+ to_remove = []
52+ for task_id , task_info in list (_tasks .items ()):
53+ # Only clean up tasks that have finished (are not queued or running)
54+ if task_info ["status" ] not in [TaskStatus .QUEUED , TaskStatus .RUNNING ]:
55+ completed_at = task_info .get ("completed_at" )
56+ if completed_at and (now - completed_at ) > ttl_seconds :
57+ to_remove .append (task_id )
58+
59+ for task_id in to_remove :
60+ _tasks .pop (task_id , None )
61+ logging .info (f"Cleaned up expired task { task_id } from memory." )
62+ except asyncio .CancelledError :
63+ break
64+ except Exception as e :
65+ logging .error (f"Error in task cleanup loop: { e } " )
66+
67+
68+ app = FastAPI (
69+ title = "Agent Evaluation Server" , version = "1.0.0" , lifespan = lifespan
70+ )
2371
2472backend_semaphore = asyncio .Semaphore (1 )
2573
@@ -34,7 +82,20 @@ def __init__(self, name: str, ip: str, port: int, backend_type: str = "tpu"):
3482
3583 # Get version info for display purposes
3684 if backend_type == "tpu" :
37- self .version = get_tpu_version ()
85+ url = f"http://{ self .ip } :{ self .port } /get_tpu_version"
86+ try :
87+ resp = requests .post (url , timeout = 10 )
88+ if resp .status_code == 200 :
89+ data = resp .json ()
90+ if isinstance (data , dict ):
91+ self .version = data .get ("tpu_version" , "TPU version not found" )
92+ else :
93+ self .version = str (data )
94+ else :
95+ self .version = f"HTTP Error { resp .status_code } "
96+ except Exception as e :
97+ logging .warning (f"Failed to get TPU version from { url } : { e } " )
98+ self .version = "Unknown TPU"
3899 else :
39100 self .version = "CPU"
40101
@@ -100,22 +161,86 @@ def __init__(self, cfg_path="eval_config.yaml"):
100161
101162 self .backends = []
102163
164+ self .tunnels = []
165+ self ._load_backends ()
166+
167+ logging .info (f"Evaluator initialized with backends: { self .backends } " )
168+
169+ def _load_backends (self ):
103170 if "backends" in self .config and self .config ["backends" ]:
104171 logging .info ("Using 'backends' configuration format" )
105172 for backend_config in self .config ["backends" ]:
173+ # Check if gcloud TPU VM tunnel is requested
174+ self ._create_tunnel (backend_config )
175+
106176 backend_obj = Backend (
107177 name = backend_config ["name" ],
108178 ip = backend_config ["ip" ],
109179 port = backend_config ["port" ],
110180 backend_type = backend_config .get ("type" , "tpu" ),
111181 )
112182 self .backends .append (backend_obj )
183+
184+ # Register cleanup
185+ if self .tunnels :
186+ logging .info (f"Registering cleanup for { len (self .tunnels )} tunnels." )
187+ atexit .register (self ._cleanup_tunnels )
188+
113189 else :
114190 raise ValueError (
115191 "No backends configured in eval_config.yaml. Please use the 'backends' format."
116192 )
117193
118- logging .info (f"Evaluator initialized with backends: { self .backends } " )
194+ def _create_tunnel (self , backend_config ):
195+ if "tpu_vm" in backend_config :
196+ local_port = backend_config ["port" ]
197+ gt = backend_config ["tpu_vm" ]
198+ tpu_name = gt ["tpu_name" ]
199+ zone = gt ["zone" ]
200+ project = gt ["project" ]
201+ remote_port = gt ["port" ]
202+
203+ logging .info (f"Constructing gcloud SSH tunnel to { tpu_name } ..." )
204+
205+ cmd = [
206+ "gcloud" ,
207+ "compute" ,
208+ "tpus" ,
209+ "tpu-vm" ,
210+ "ssh" ,
211+ tpu_name ,
212+ f"--zone={ zone } " ,
213+ f"--project={ project } " ,
214+ "--" ,
215+ "-N" ,
216+ "-L" ,
217+ f"{ local_port } :localhost:{ remote_port } " ,
218+ ]
219+
220+ try :
221+ process = subprocess .Popen (cmd )
222+ self .tunnels .append (process )
223+ logging .info (
224+ f"Gcloud SSH tunnel started for { backend_config ['name' ]} (PID: { process .pid } )"
225+ )
226+ time .sleep (10 ) # Blocking sleep as requested
227+ except Exception as e :
228+ logging .error (f"Failed to start gcloud SSH tunnel: { e } " )
229+
230+ def _cleanup_tunnels (self ):
231+ if self .tunnels :
232+ logging .info ("Cleaning up SSH tunnels..." )
233+ for p in self .tunnels :
234+ try :
235+ p .terminate ()
236+ p .wait (timeout = 5 )
237+ logging .info (f"Tunnel PID { p .pid } terminated." )
238+ except Exception as e :
239+ logging .error (f"Failed to terminate tunnel PID { p .pid } : { e } " )
240+ try :
241+ p .kill ()
242+ except :
243+ pass
119244
120245 def get_available_backend (self , backend_type : Optional [str ] = None ):
121246 """
@@ -173,6 +298,8 @@ async def run_evaluation_task(task_id: str, request: EvalRequest):
173298 except Exception as e :
174299 _tasks [task_id ]["status" ] = TaskStatus .FAILED
175300 _tasks [task_id ]["error" ] = str (e )
301+ finally :
302+ _tasks [task_id ]["completed_at" ] = time .time ()
176303
177304
178305async def _perform_evaluation (request : EvalRequest ):
0 commit comments