feat(client): adopt an asynchronous Tokio runtime for the client frontend

This commit is contained in:
Nils Andreas Svee committed 2026-10-05 20:48:06 +07:00
1 parent 1f7df1ebc9
commit 901d8cfae5
8 files changed
+258 -325

No files matched your search

+22 -4
View File
@@ -4,7 +4,10 @@ use std::{
};
use thiserror::Error;
use tokio::runtime::{Builder, Runtime};
use tokio::{
runtime::{Builder, Runtime},
sync::oneshot,
};
use super::{
ManagedClient, ManagedClientConfig, ManagedClientHandle, ManagedClientTask, ManagedConfigError,
@@ -83,16 +86,19 @@ pub enum BlockingManagedJoinError {
WorkerPanicked,
}
/// Blocking completion owner for one unified managed Tokio task.
/// Dedicated-thread 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
/// with its own current-thread Tokio runtime. Synchronous callers use [`Self::join`];
/// async frontends use [`Self::join_async`] without sharing the measurement executor.
/// Dropping a live owner requests 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>>,
completion: oneshot::Receiver<()>,
}
impl BlockingManagedClient {
@@ -131,11 +137,13 @@ impl BlockingManagedClient {
.build()
.map_err(|source| BlockingManagedStartError::Runtime { source })?;
let runtime = WorkerRuntime::new(runtime);
let (completed, completion) = oneshot::channel();
let worker = thread::Builder::new()
.name("irtt-managed".to_owned())
.spawn(move || {
let outcome = runtime.block_on(task);
runtime.shutdown();
let _ = completed.send(());
outcome
})
.map_err(|source| BlockingManagedStartError::ThreadSpawn { source })?;
@@ -143,6 +151,7 @@ impl BlockingManagedClient {
Ok(Self {
handle,
worker: Some(worker),
completion,
})
}
@@ -156,6 +165,15 @@ impl BlockingManagedClient {
self.join_worker()
}
/// Await worker completion, then join its OS thread and obtain its outcome.
///
/// The task and runtime remain isolated from the caller's executor. Cancelling
/// this future drops the owner, requesting stop and joining as usual.
pub async fn join_async(mut self) -> Result<ManagedOutcome, BlockingManagedJoinError> {
let _ = (&mut self.completion).await;
self.join_worker()
}
fn join_worker(&mut self) -> Result<ManagedOutcome, BlockingManagedJoinError> {
self.worker
.take()
@@ -197,3 +197,25 @@ fn concurrent_submission_and_stop_or_seal_never_lose_an_accepted_receipt() {
.expect("every accepted concurrent submission must receive a terminal disposition");
});
}
// Joining an isolated worker must leave the caller's executor free to stop it.
#[test]
fn async_worker_join_allows_concurrent_stop_and_preserves_final_status() {
let owner = BlockingManagedClient::start(config(), vec![]).unwrap();
let handle = owner.handle();
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let joined = owner.join_async();
let stop = async { handle.stop().await };
let (outcome, ()) =
tokio::time::timeout(Duration::from_secs(2), async { tokio::join!(joined, stop) })
.await
.expect("async join must allow stop on the caller's executor");
let outcome = outcome.unwrap();
assert_eq!(outcome.end_reason, ManagedEndReason::StopRequested);
assert_eq!(handle.status().final_outcome.as_deref(), Some(&outcome));
});
}