fix(studio): harden runtime integration and dependency defaults
Stabilize studio publish/visualization flow and tighten export behavior while aligning project dependencies with the monorepo runtime expectations.
This commit is contained in:
@@ -9,6 +9,7 @@ Provides pluggable result publishing:
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from concurrent.futures import CancelledError, Future
|
||||
import json
|
||||
import logging
|
||||
import nats
|
||||
@@ -181,10 +182,38 @@ class NatsPublisher:
|
||||
def _stop_background_loop(self) -> None:
|
||||
"""Stop the background event loop and thread."""
|
||||
with self._lock:
|
||||
if self._loop is not None and self._loop.is_running():
|
||||
_ = self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
if self._thread is not None and self._thread.is_alive():
|
||||
self._thread.join(timeout=2.0)
|
||||
loop = self._loop
|
||||
thread = self._thread
|
||||
|
||||
if loop is not None and loop.is_running():
|
||||
try:
|
||||
|
||||
async def _cancel_pending_tasks() -> None:
|
||||
current = asyncio.current_task()
|
||||
pending = [
|
||||
task
|
||||
for task in asyncio.all_tasks()
|
||||
if task is not current and not task.done()
|
||||
]
|
||||
for task in pending:
|
||||
_ = task.cancel()
|
||||
if pending:
|
||||
_ = await asyncio.gather(*pending, return_exceptions=True)
|
||||
|
||||
cancel_future = asyncio.run_coroutine_threadsafe(
|
||||
_cancel_pending_tasks(),
|
||||
loop,
|
||||
)
|
||||
cancel_future.result(timeout=2.0)
|
||||
except (RuntimeError, OSError, TimeoutError, CancelledError):
|
||||
pass
|
||||
finally:
|
||||
_ = loop.call_soon_threadsafe(loop.stop)
|
||||
|
||||
if thread is not None and thread.is_alive():
|
||||
thread.join(timeout=2.0)
|
||||
|
||||
with self._lock:
|
||||
self._loop = None
|
||||
self._thread = None
|
||||
|
||||
@@ -204,7 +233,12 @@ class NatsPublisher:
|
||||
if not self._start_background_loop():
|
||||
return False
|
||||
|
||||
future: Future[_NatsClient] | None = None
|
||||
try:
|
||||
loop = self._loop
|
||||
if loop is None:
|
||||
logger.warning("Background event loop unavailable for NATS connection")
|
||||
return False
|
||||
|
||||
async def _connect() -> _NatsClient:
|
||||
nc = await nats.connect(self._nats_url) # pyright: ignore[reportUnknownMemberType]
|
||||
@@ -213,12 +247,21 @@ class NatsPublisher:
|
||||
# Run connection in background loop
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
_connect(),
|
||||
self._loop, # pyright: ignore[reportArgumentType]
|
||||
loop,
|
||||
)
|
||||
self._nc = future.result(timeout=10.0)
|
||||
self._connected = True
|
||||
logger.info(f"Connected to NATS at {self._nats_url}")
|
||||
return True
|
||||
except TimeoutError as e:
|
||||
if future is not None:
|
||||
_ = future.cancel()
|
||||
try:
|
||||
_ = future.result(timeout=1.0)
|
||||
except (TimeoutError, CancelledError, RuntimeError, OSError):
|
||||
pass
|
||||
logger.warning("Timed out connecting to NATS at %s: %s", self._nats_url, e)
|
||||
return False
|
||||
except (RuntimeError, OSError, TimeoutError) as e:
|
||||
logger.warning(f"Failed to connect to NATS at {self._nats_url}: {e}")
|
||||
return False
|
||||
@@ -260,16 +303,19 @@ class NatsPublisher:
|
||||
exc = fut.exception()
|
||||
except (RuntimeError, OSError) as callback_error:
|
||||
logger.warning(f"NATS publish callback failed: {callback_error}")
|
||||
self._connected = False
|
||||
with self._lock:
|
||||
self._connected = False
|
||||
return
|
||||
if exc is not None:
|
||||
logger.warning(f"Failed to publish to NATS: {exc}")
|
||||
self._connected = False
|
||||
with self._lock:
|
||||
self._connected = False
|
||||
|
||||
future.add_done_callback(_on_done)
|
||||
except (RuntimeError, OSError, ValueError, TypeError) as e:
|
||||
logger.warning(f"Failed to schedule NATS publish: {e}")
|
||||
self._connected = False
|
||||
with self._lock:
|
||||
self._connected = False
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close NATS connection."""
|
||||
|
||||
Reference in New Issue
Block a user