use std::{borrow::Cow, time::Duration}; use tokio_util::sync::CancellationToken; use tracing::{Instrument, Level}; use super::{IntoTransport, Transport}; use crate::service::{RxJsonRpcMessage, ServiceRole, TxJsonRpcMessage}; #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum WorkerQuitReason { #[error("Join error {0}")] Join(#[from] tokio::task::JoinError), #[error("Transport fatal {error}, when {context}")] Fatal { error: E, context: Cow<'static, str>, }, #[error("Transport cancelled")] Cancelled, #[error("Transport closed")] TransportClosed, #[error("Handler terminated")] HandlerTerminated, #[error("Worker idle timeout after {}ms", _0.as_millis())] IdleTimeout(Duration), } impl WorkerQuitReason { pub fn fatal(error: E, context: impl Into>) -> Self { Self::Fatal { error, context: context.into(), } } pub fn fatal_context(context: impl Into>) -> impl FnOnce(E) -> Self { |e| Self::Fatal { error: e, context: context.into(), } } } pub trait Worker: Sized + Send + 'static { type Error: std::error::Error + Send + Sync + 'static; type Role: ServiceRole; fn err_closed() -> Self::Error; fn err_join(e: tokio::task::JoinError) -> Self::Error; fn run( self, context: WorkerContext, ) -> impl Future>> + Send; fn config(&self) -> WorkerConfig { WorkerConfig::default() } } #[non_exhaustive] pub struct WorkerSendRequest { pub message: TxJsonRpcMessage, pub responder: tokio::sync::oneshot::Sender>, } pub struct WorkerTransport { rx: tokio::sync::mpsc::Receiver>, send_service: tokio::sync::mpsc::Sender>, join_handle: Option>>>, _drop_guard: tokio_util::sync::DropGuard, ct: CancellationToken, } #[non_exhaustive] pub struct WorkerConfig { pub name: Option, pub channel_buffer_capacity: usize, } impl Default for WorkerConfig { fn default() -> Self { Self { name: None, channel_buffer_capacity: 16, } } } #[non_exhaustive] pub enum WorkerAdapter {} impl IntoTransport for W { fn into_transport(self) -> impl Transport + 'static { WorkerTransport::spawn(self) } } impl WorkerTransport { pub fn cancel_token(&self) -> CancellationToken { self.ct.clone() } pub fn spawn(worker: W) -> Self { Self::spawn_with_ct(worker, CancellationToken::new()) } pub fn spawn_with_ct(worker: W, transport_task_ct: CancellationToken) -> Self { let config = worker.config(); let worker_name = config.name; let (to_transport_tx, from_handler_rx) = tokio::sync::mpsc::channel::>(config.channel_buffer_capacity); let (to_handler_tx, from_transport_rx) = tokio::sync::mpsc::channel::>(config.channel_buffer_capacity); let context = WorkerContext { to_handler_tx, from_handler_rx, cancellation_token: transport_task_ct.clone(), }; let join_handle = tokio::spawn(async move { worker .run(context) .instrument(tracing::span!( Level::TRACE, "transport_worker", name = worker_name, )) .await .inspect_err(|e| match e { WorkerQuitReason::Cancelled | WorkerQuitReason::TransportClosed | WorkerQuitReason::HandlerTerminated | WorkerQuitReason::IdleTimeout(_) => { tracing::debug!("worker quit with reason: {:?}", e); } WorkerQuitReason::Join(e) => { tracing::error!("worker quit with join error: {:?}", e); } WorkerQuitReason::Fatal { error, context } => { tracing::error!("worker quit with fatal: {error}, when {context}"); } }) .inspect(|_| { tracing::debug!("worker quit"); }) }); Self { rx: from_transport_rx, send_service: to_transport_tx, join_handle: Some(join_handle), ct: transport_task_ct.clone(), _drop_guard: transport_task_ct.drop_guard(), } } } #[non_exhaustive] pub struct SendRequest { pub message: TxJsonRpcMessage, pub responder: tokio::sync::oneshot::Sender>, } #[non_exhaustive] pub struct WorkerContext { pub to_handler_tx: tokio::sync::mpsc::Sender>, pub from_handler_rx: tokio::sync::mpsc::Receiver>, pub cancellation_token: CancellationToken, } impl WorkerContext { pub async fn send_to_handler( &mut self, item: RxJsonRpcMessage, ) -> Result<(), WorkerQuitReason> { self.to_handler_tx .send(item) .await .map_err(|_| WorkerQuitReason::HandlerTerminated) } pub async fn recv_from_handler( &mut self, ) -> Result, WorkerQuitReason> { self.from_handler_rx .recv() .await .ok_or(WorkerQuitReason::HandlerTerminated) } } impl Transport for WorkerTransport { type Error = W::Error; fn send( &mut self, item: TxJsonRpcMessage, ) -> impl Future> + Send + 'static { let tx = self.send_service.clone(); let (responder, receiver) = tokio::sync::oneshot::channel(); let request = WorkerSendRequest { message: item, responder, }; async move { tx.send(request).await.map_err(|_| W::err_closed())?; receiver.await.map_err(|_| W::err_closed())??; Ok(()) } } async fn receive(&mut self) -> Option> { self.rx.recv().await } async fn close(&mut self) -> Result<(), Self::Error> { if let Some(handle) = self.join_handle.take() { self.ct.cancel(); let _quit_reason = handle.await.map_err(W::err_join)?; Ok(()) } else { Ok(()) } } }