use std::{ thread::{self, JoinHandle}, time::Duration, }; use thiserror::Error; use tokio::runtime::{Builder, Runtime}; use super::{ ManagedClient, ManagedClientConfig, ManagedClientHandle, ManagedClientTask, ManagedConfigError, ManagedEventSubscription, ManagedOutcome, ManagedSubscribeError, ManagedTargetConfig, }; const BLOCKING_RUNTIME_SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(100); /// Owns the runtime used exclusively by the blocking managed worker. pub(super) struct WorkerRuntime { runtime: Option, } impl WorkerRuntime { pub(super) fn new(runtime: Runtime) -> Self { Self { runtime: Some(runtime), } } fn block_on(&self, task: F) -> F::Output { self.runtime .as_ref() .expect("worker runtime is available until shutdown") .block_on(task) } /// Shut down residual runtime work without delaying managed completion indefinitely. pub(super) fn shutdown(mut self) { self.runtime .take() .expect("worker runtime is available until shutdown") .shutdown_timeout(BLOCKING_RUNTIME_SHUTDOWN_TIMEOUT); } } impl Drop for WorkerRuntime { fn drop(&mut self) { if let Some(runtime) = self.runtime.take() { runtime.shutdown_background(); } } } /// Failure to start a blocking managed client owner. #[derive(Debug, Error)] pub enum BlockingManagedStartError { /// The managed task configuration was invalid. #[error(transparent)] Config(#[from] ManagedConfigError), /// The managed event subscription could not be created before startup. #[error(transparent)] Subscribe(#[from] ManagedSubscribeError), /// The dedicated Tokio runtime could not be created. #[error("failed to create blocking managed Tokio runtime: {source}")] Runtime { #[source] source: std::io::Error, }, /// The dedicated worker thread could not be spawned. #[error("failed to spawn blocking managed worker thread: {source}")] ThreadSpawn { #[source] source: std::io::Error, }, } /// Failure to obtain completion from a blocking managed client owner. #[derive(Debug, Error, Eq, PartialEq)] pub enum BlockingManagedJoinError { /// The dedicated managed worker thread panicked. #[error("blocking managed worker thread panicked")] WorkerPanicked, } /// Blocking completion owner for one unified managed Tokio task. /// /// This owner runs exactly one [`ManagedClient`] task on one dedicated OS thread /// with its own current-thread Tokio runtime. Dropping a live owner requests a /// graceful stop and synchronously joins that worker, so drop may block while /// bounded managed cleanup completes. #[must_use = "dropping the blocking managed owner requests stop and joins its worker"] pub struct BlockingManagedClient { handle: ManagedClientHandle, worker: Option>, } impl BlockingManagedClient { /// Validate configuration, create a dedicated runtime, and start its worker. pub fn start( config: ManagedClientConfig, targets: Vec, ) -> Result { let (task, handle) = ManagedClient::task(config, targets)?; Self::start_task(task, handle) } /// Validate configuration, subscribe to presentation events, and start a /// dedicated managed worker. /// /// The returned subscription exists before the managed task receives its /// first poll, so it cannot miss startup events because of subscription /// ordering. pub fn start_with_subscription( config: ManagedClientConfig, targets: Vec, ) -> Result<(Self, ManagedEventSubscription), BlockingManagedStartError> { let (task, handle) = ManagedClient::task(config, targets)?; let subscription = handle.subscribe()?; let owner = Self::start_task(task, handle)?; Ok((owner, subscription)) } fn start_task( task: ManagedClientTask, handle: ManagedClientHandle, ) -> Result { let runtime = Builder::new_current_thread() .enable_io() .enable_time() .build() .map_err(|source| BlockingManagedStartError::Runtime { source })?; let runtime = WorkerRuntime::new(runtime); let worker = thread::Builder::new() .name("irtt-managed".to_owned()) .spawn(move || { let outcome = runtime.block_on(task); runtime.shutdown(); outcome }) .map_err(|source| BlockingManagedStartError::ThreadSpawn { source })?; Ok(Self { handle, worker: Some(worker), }) } /// Return a cloned managed control and observation capability. pub fn handle(&self) -> ManagedClientHandle { self.handle.clone() } /// Wait synchronously for the managed task's authoritative outcome. pub fn join(mut self) -> Result { self.join_worker() } fn join_worker(&mut self) -> Result { join_worker( self.worker .take() .expect("blocking managed worker is joined at most once"), ) } } pub(super) fn join_worker( worker: JoinHandle, ) -> Result { worker .join() .map_err(|_| BlockingManagedJoinError::WorkerPanicked) } impl Drop for BlockingManagedClient { fn drop(&mut self) { if self.worker.is_some() { let _receipt = self.handle.stop(); let _ = self.join_worker(); } } }