irtt-client 0.6.0

IRTT-compatible Rust client library
Documentation
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<Runtime>,
}

impl WorkerRuntime {
    pub(super) fn new(runtime: Runtime) -> Self {
        Self {
            runtime: Some(runtime),
        }
    }

    fn block_on<F: std::future::Future>(&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<JoinHandle<ManagedOutcome>>,
}

impl BlockingManagedClient {
    /// Validate configuration, create a dedicated runtime, and start its worker.
    pub fn start(
        config: ManagedClientConfig,
        targets: Vec<ManagedTargetConfig>,
    ) -> Result<Self, BlockingManagedStartError> {
        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<ManagedTargetConfig>,
    ) -> 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<Self, BlockingManagedStartError> {
        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<ManagedOutcome, BlockingManagedJoinError> {
        self.join_worker()
    }

    fn join_worker(&mut self) -> Result<ManagedOutcome, BlockingManagedJoinError> {
        self.worker
            .take()
            .expect("blocking managed worker is joined at most once")
            .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();
        }
    }
}