use std::ffi::CStr;
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
use std::pin::Pin;
use std::sync::{Arc, Mutex, PoisonError};
use std::task::{Context, Poll};
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::ffi::c_str;
use pyo3::intern;
use pyo3::prelude::*;
use pyo3::types::{PyCFunction, PyDict, PyString, PyTuple};
use tokio::sync::oneshot;
use crate::{dataset::AsyncDataset, error::Result, python::pool::attach};
#[pyclass(
frozen,
subclass,
name = "PyAsyncDatasetBase",
module = "dataloader_rs"
)]
pub struct PyAsyncDatasetBase;
#[pymethods]
impl PyAsyncDatasetBase {
#[new]
#[pyo3(signature = (*_args, **_kwargs))]
fn new(_args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>) -> Self {
Self
}
}
const EVENT_LOOP: &CStr = c_str!(include_str!("event_loop.py"));
struct EventLoop {
submit: Py<PyAny>,
stop: Py<PyAny>,
}
impl EventLoop {
fn stop(&self) {
Python::try_attach(|py| {
let _ = self.stop.call0(py);
});
}
}
impl Drop for EventLoop {
fn drop(&mut self) {
self.stop();
}
}
pub(crate) struct PyAsyncItems {
shared: Arc<Shared>,
batched: bool,
}
struct Shared {
dataset: Py<PyAny>,
event_loop: EventLoop,
}
impl PyAsyncItems {
pub(crate) fn new(
py: Python<'_>,
dataset: &Bound<'_, PyAny>,
batched: bool,
aclose: bool,
) -> PyResult<Self> {
let module = PyModule::from_code(py, EVENT_LOOP, c"event_loop.py", c"_dataloader_loop")?;
let aclose = match aclose {
true => Some(dataset.getattr(intern!(py, "aclose"))?),
false => None,
};
let (submit, stop) = module
.getattr("start")?
.call1((aclose,))?
.extract::<(Py<PyAny>, Py<PyAny>)>()?;
let shared = Shared {
dataset: dataset.clone().unbind(),
event_loop: EventLoop { submit, stop },
};
Ok(Self {
shared: Arc::new(shared),
batched,
})
}
pub(crate) fn close(&self) {
self.shared.event_loop.stop();
}
async fn submit<A>(&self, method: &'static str, arg: A) -> PyResult<Py<PyAny>>
where
A: for<'py> IntoPyObject<'py> + Send + 'static,
{
let shared = Arc::clone(&self.shared);
let (tx, rx) = oneshot::channel();
rayon::spawn(move || {
let submitted = catch_unwind(AssertUnwindSafe(|| {
attach(|py| shared.submit(py, method, arg))
}));
drop(shared);
let _ = tx.send(submitted);
});
let pending = match rx.await {
Ok(Ok(pending)) => pending?,
Ok(Err(panic)) => resume_unwind(panic),
Err(_) => return Err(PyRuntimeError::new_err("the coroutine was not submitted")),
};
pending.await
}
}
impl Shared {
fn submit<'py, A>(&self, py: Python<'py>, method: &str, arg: A) -> PyResult<Pending>
where
A: IntoPyObject<'py>,
{
let method = PyString::intern(py, method);
let coroutine = self.dataset.bind(py).call_method1(method, (arg,))?;
let future = self.event_loop.submit.bind(py).call1((coroutine,))?;
let (tx, rx) = oneshot::channel();
let tx = Mutex::new(Some(tx));
let done = PyCFunction::new_closure(
py,
None,
None,
move |args: &Bound<'_, PyTuple>, _: Option<&Bound<'_, PyDict>>| -> PyResult<()> {
let result = args.get_item(0)?.call_method0("result").map(Bound::unbind);
if let Some(tx) = tx.lock().unwrap_or_else(PoisonError::into_inner).take() {
let _ = tx.send(result);
}
Ok(())
},
)?;
future.call_method1(intern!(py, "add_done_callback"), (done,))?;
Ok(Pending {
rx,
future: Some(future.unbind()),
})
}
}
struct Pending {
rx: oneshot::Receiver<PyResult<Py<PyAny>>>,
future: Option<Py<PyAny>>,
}
impl Future for Pending {
type Output = PyResult<Py<PyAny>>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let result = match Pin::new(&mut self.rx).poll(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Ok(result)) => result,
Poll::Ready(Err(_)) => Err(PyRuntimeError::new_err(
"the event loop stopped before the coroutine finished",
)),
};
self.future = None;
Poll::Ready(result)
}
}
impl Drop for Pending {
fn drop(&mut self) {
if let Some(future) = self.future.take() {
Python::try_attach(|py| {
let _ = future.call_method0(py, intern!(py, "cancel"));
});
}
}
}
impl AsyncDataset for PyAsyncItems {
type Item = Py<PyAny>;
async fn get(&self, index: usize) -> Result<Py<PyAny>> {
Ok(self.submit("__getitem__", index).await?)
}
async fn get_batch(&self, indices: &[usize]) -> Result<Vec<Py<PyAny>>> {
if !self.batched {
let mut items = Vec::with_capacity(indices.len());
for &index in indices {
items.push(self.get(index).await?);
}
return Ok(items);
}
let samples = self.submit("__getitems__", indices.to_vec()).await?;
let samples = attach(|py| {
samples
.bind(py)
.try_iter()?
.map(|sample| sample.map(Bound::unbind))
.collect::<PyResult<Vec<_>>>()
})?;
if samples.len() != indices.len() {
return Err(PyValueError::new_err(format!(
"__getitems__ returned {} samples for {} indices",
samples.len(),
indices.len()
))
.into());
}
Ok(samples)
}
fn len(&self) -> usize {
Python::attach(|py| {
self.shared
.dataset
.bind(py)
.len()
.unwrap_or_else(|e| panic!("PyAsyncDataset.__len__ failed: {e}"))
})
}
}