@@ -539,6 +539,18 @@ def _get_service_data_info(service_id: str) -> dict | None:
539539_AGENT_LOG_TIMEOUT = 30 # seconds — log fetches should be fast
540540
541541
542+ def _fetch_agent_logs (url : str , headers : dict , data : bytes , timeout : int ) -> str :
543+ """Blocking POST to host agent that returns the response body as text.
544+
545+ Extracted so async handlers can offload the urllib call via
546+ ``asyncio.to_thread``. urllib.error.HTTPError / URLError raised inside
547+ propagate back to the caller and are handled there.
548+ """
549+ req = urllib .request .Request (url , data = data , headers = headers , method = "POST" )
550+ with urllib .request .urlopen (req , timeout = timeout ) as resp :
551+ return resp .read ().decode ()
552+
553+
542554def _call_agent (action : str , service_id : str ) -> bool :
543555 """Call host agent to start/stop a service. Returns True on success."""
544556 url = f"{ AGENT_URL } /v1/extension/{ action } "
@@ -551,8 +563,11 @@ def _call_agent(action: str, service_id: str) -> bool:
551563 try :
552564 with urllib .request .urlopen (req , timeout = _AGENT_TIMEOUT ) as resp :
553565 return resp .status == 200
554- except Exception :
555- logger .warning ("Host agent unreachable at %s — fallback to restart_required" , AGENT_URL )
566+ except (urllib .error .URLError , urllib .error .HTTPError , OSError , TimeoutError ) as exc :
567+ logger .warning (
568+ "Host agent unreachable at %s — fallback to restart_required: %s" ,
569+ AGENT_URL , exc ,
570+ )
556571 return False
557572
558573
@@ -567,9 +582,10 @@ def _call_agent_invalidate_compose_cache() -> None:
567582 logger .warning (
568583 "compose-flags cache invalidation returned HTTP %d" , resp .status ,
569584 )
570- except Exception :
585+ except ( urllib . error . URLError , urllib . error . HTTPError , OSError , TimeoutError ) as exc :
571586 logger .warning (
572- "Host agent unreachable for compose-flags invalidation at %s" , AGENT_URL ,
587+ "Host agent unreachable for compose-flags invalidation at %s: %s" ,
588+ AGENT_URL , exc ,
573589 )
574590
575591
@@ -647,8 +663,10 @@ def _call_agent_compose_rename(action: str, service_id: str) -> bool:
647663 try :
648664 with urllib .request .urlopen (req , timeout = _AGENT_LOG_TIMEOUT ) as resp :
649665 return resp .status == 200
650- except Exception :
651- logger .warning ("Host agent unreachable for compose rename at %s" , AGENT_URL )
666+ except (urllib .error .URLError , urllib .error .HTTPError , OSError , TimeoutError ) as exc :
667+ logger .warning (
668+ "Host agent unreachable for compose rename at %s: %s" , AGENT_URL , exc ,
669+ )
652670 return False
653671
654672
@@ -667,7 +685,7 @@ def _check_agent_health() -> bool:
667685 req = urllib .request .Request (f"{ AGENT_URL } /health" )
668686 with urllib .request .urlopen (req , timeout = 3 ) as resp :
669687 available = resp .status == 200
670- except Exception :
688+ except ( urllib . error . URLError , urllib . error . HTTPError , OSError , TimeoutError ) :
671689 available = False
672690 with _agent_cache_lock :
673691 _agent_cache .update (available = available , checked_at = time .monotonic ())
@@ -709,7 +727,16 @@ async def extensions_catalog(
709727 api_key : str = Depends (verify_api_key ),
710728):
711729 """Get the extensions catalog with computed status."""
712- asyncio .get_running_loop ().run_in_executor (None , _cleanup_stale_progress )
730+ _cleanup_future = asyncio .get_running_loop ().run_in_executor (
731+ None , _cleanup_stale_progress ,
732+ )
733+
734+ def _log_cleanup_error (f : asyncio .Future ) -> None :
735+ exc = f .exception ()
736+ if exc is not None :
737+ logger .error ("stale-progress cleanup failed: %s" , exc , exc_info = exc )
738+
739+ _cleanup_future .add_done_callback (_log_cleanup_error )
713740
714741 from helpers import get_cached_services , get_all_services
715742
@@ -816,12 +843,14 @@ async def extensions_catalog(
816843 except OSError :
817844 lib_available = False
818845
846+ agent_available = await asyncio .to_thread (_check_agent_health )
847+
819848 return {
820849 "extensions" : extensions ,
821850 "summary" : summary ,
822851 "gpu_backend" : GPU_BACKEND ,
823852 "library_available" : lib_available ,
824- "agent_available" : _check_agent_health () ,
853+ "agent_available" : agent_available ,
825854 }
826855
827856
@@ -927,10 +956,11 @@ async def extension_logs(
927956 "Authorization" : f"Bearer { DREAM_AGENT_KEY } " ,
928957 }
929958 data = json .dumps ({"service_id" : service_id , "tail" : 100 }).encode ()
930- req = urllib .request .Request (url , data = data , headers = headers , method = "POST" )
931959 try :
932- with urllib .request .urlopen (req , timeout = _AGENT_LOG_TIMEOUT ) as resp :
933- return json .loads (resp .read ().decode ())
960+ body = await asyncio .to_thread (
961+ _fetch_agent_logs , url , headers , data , _AGENT_LOG_TIMEOUT ,
962+ )
963+ return json .loads (body )
934964 except urllib .error .HTTPError as exc :
935965 try :
936966 err_body = json .loads (exc .read ().decode ())
0 commit comments