use crate::device::{DeviceId, DeviceService, ServerUtilitiesHandle};
use alloc::boxed::Box;
use alloc::string::String;
use core::any::Any;
#[cfg(feature = "std")]
use cubecl_environment::future::channel::oneshot;
pub struct CallError {
failure: Failure,
}
enum Failure {
Panicked(Box<dyn Any + Send>),
Closed,
Disconnected,
}
impl CallError {
#[cfg_attr(not(multi_threading), allow(dead_code))]
pub(crate) fn from_panic(payload: Box<dyn Any + Send>) -> Self {
Self {
failure: Failure::Panicked(payload),
}
}
#[cfg_attr(not(multi_threading), allow(dead_code))]
pub(crate) fn disconnected() -> Self {
Self {
failure: Failure::Disconnected,
}
}
#[cfg_attr(not(multi_threading), allow(dead_code))]
pub(crate) fn closed() -> Self {
Self {
failure: Failure::Closed,
}
}
pub fn is_closed(&self) -> bool {
matches!(self.failure, Failure::Closed)
}
pub fn message(&self) -> Option<&str> {
let Failure::Panicked(payload) = &self.failure else {
return None;
};
if let Some(s) = payload.downcast_ref::<&'static str>() {
Some(*s)
} else if let Some(s) = payload.downcast_ref::<String>() {
Some(s.as_str())
} else {
None
}
}
pub fn into_panic(self) -> Option<Box<dyn Any + Send>> {
match self.failure {
Failure::Panicked(payload) => Some(payload),
Failure::Closed | Failure::Disconnected => None,
}
}
#[track_caller]
pub fn resume(self) -> ! {
match self.failure {
#[cfg(feature = "std")]
Failure::Panicked(payload) => std::panic::resume_unwind(payload),
#[cfg(not(feature = "std"))]
Failure::Panicked(_payload) => {
panic!("a device task panicked but its payload cannot be re-raised without `std`")
}
Failure::Closed => panic!("the device service was closed, as the process exits"),
Failure::Disconnected => {
panic!("device runner channel disconnected before producing a result")
}
}
}
}
pub trait CallResultExt<R> {
fn unwrap_or_resume(self) -> R;
}
impl<R> CallResultExt<R> for Result<R, CallError> {
#[track_caller]
fn unwrap_or_resume(self) -> R {
match self {
Ok(value) => value,
Err(err) => err.resume(),
}
}
}
#[must_use = "the call's result is lost unless it is joined"]
pub struct SpawnedCall<R> {
state: SpawnedState<R>,
}
enum SpawnedState<R> {
Done(Result<R, CallError>),
#[cfg(feature = "std")]
Running(oneshot::Receiver<Result<R, CallError>>),
}
impl<R> SpawnedCall<R> {
pub(crate) fn done(result: Result<R, CallError>) -> Self {
Self {
state: SpawnedState::Done(result),
}
}
#[cfg(feature = "std")]
pub(crate) fn running(receiver: oneshot::Receiver<Result<R, CallError>>) -> Self {
Self {
state: SpawnedState::Running(receiver),
}
}
pub fn join(self) -> Result<R, CallError> {
match self.state {
SpawnedState::Done(result) => result,
#[cfg(feature = "std")]
SpawnedState::Running(receiver) => receiver
.recv()
.unwrap_or_else(|_| Err(CallError::disconnected())),
}
}
}
impl core::fmt::Debug for CallError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match (&self.failure, self.message()) {
(_, Some(message)) => write!(
f,
"CallError(task panicked on device runner thread: {message})"
),
(Failure::Panicked(_), None) => f.write_str(
"CallError(task panicked on device runner thread with a non-string payload)",
),
(Failure::Closed, None) => {
f.write_str("CallError(the device service was closed, as the process exits)")
}
(Failure::Disconnected, None) => f.write_str(
"CallError(device runner channel disconnected before producing a result)",
),
}
}
}
#[derive(new, Clone, Debug)]
pub struct ServiceCreationError {
#[allow(dead_code)] reason: alloc::string::String,
}
#[derive(new, Clone, Debug)]
pub struct ServiceCloseError {
#[allow(dead_code)] reason: alloc::string::String,
}
pub trait DeviceHandleSpec: Sized + Clone {
const BLOCKING: bool;
fn insert<S: DeviceService>(
device_id: DeviceId,
service: S,
) -> Result<Self, ServiceCreationError>;
fn new<S: DeviceService>(device_id: DeviceId) -> Self;
fn device_id(&self) -> DeviceId;
fn utilities(&self) -> ServerUtilitiesHandle;
fn flush_queue(&self);
fn submit_blocking<'a, R: Send, T: FnOnce(&mut dyn Any) -> R + Send + 'a>(
&self,
task: T,
) -> Result<R, CallError>;
fn submit<T: FnOnce(&mut dyn Any) + Send + 'static>(&self, task: T);
fn spawn<R: Send + 'static, T: FnOnce(&mut dyn Any) -> R + Send + 'static>(
&self,
task: T,
) -> SpawnedCall<R>;
fn exclusive<R: Send, T: FnOnce() -> R + Send>(&self, task: T) -> Result<R, CallError>;
fn shutdown(device_id: DeviceId) {
let _ = device_id;
}
fn close<S: DeviceService>(device_id: DeviceId) -> Result<(), ServiceCloseError> {
let _ = device_id;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::CallError;
use alloc::boxed::Box;
use core::any::Any;
#[test]
fn test_disconnected_has_no_message_or_payload() {
let err = CallError::disconnected();
assert_eq!(err.message(), None);
assert!(err.into_panic().is_none());
}
#[test]
fn test_closed_is_neither_a_panic_nor_a_disconnect() {
let err = CallError::closed();
assert!(err.is_closed());
assert!(!CallError::disconnected().is_closed());
assert!(alloc::format!("{err:?}").contains("closed"));
assert!(err.into_panic().is_none());
}
#[test]
fn test_debug_string_payload_includes_message() {
let err = CallError::from_panic(Box::new("oops") as Box<dyn Any + Send>);
let debug = alloc::format!("{err:?}");
assert!(debug.contains("task panicked on device runner thread"));
assert!(debug.contains("oops"));
}
#[test]
fn test_debug_non_string_payload() {
let err = CallError::from_panic(Box::new(123u8) as Box<dyn Any + Send>);
let debug = alloc::format!("{err:?}");
assert!(debug.contains("non-string payload"), "got: {debug}");
}
#[test]
fn test_debug_disconnected() {
let err = CallError::disconnected();
let debug = alloc::format!("{err:?}");
assert!(debug.contains("disconnected"), "got: {debug}");
}
}