use futures_util::task::AtomicWaker;
use std::{
borrow::Borrow,
fmt,
hash::{Hash, Hasher},
pin::Pin,
sync::{
Arc, Weak,
atomic::{AtomicUsize, Ordering},
},
task::{Context, Poll},
time::{Duration, Instant},
};
use crate::{
task::{ExecutionContext, Task, from_request::FromRequest},
worker::context::WorkerContext,
};
const RUNNING: usize = 0;
const CANCELLED: usize = 1; const COMPLETED: usize = 2;
#[derive(Clone, Debug)]
pub struct TaskContext {
task_id: Arc<str>,
state: Arc<AtomicUsize>,
waker: Arc<AtomicWaker>,
instant: Instant,
inner: Weak<ExecutionContext>,
sub_tasks_count: Arc<AtomicUsize>,
}
impl TaskContext {
#[must_use]
pub(crate) fn new(ctx: &Arc<ExecutionContext>) -> Self {
Self {
task_id: ctx
.task_id
.as_ref()
.expect("A task id must be included")
.to_string()
.into(),
state: Arc::new(AtomicUsize::new(RUNNING)),
waker: Arc::new(AtomicWaker::new()),
instant: Instant::now(),
inner: Arc::downgrade(ctx),
sub_tasks_count: Arc::new(AtomicUsize::new(0)),
}
}
pub fn cancel(&self) -> Result<(), TaskStateError> {
self.state
.compare_exchange(RUNNING, CANCELLED, Ordering::AcqRel, Ordering::Acquire)
.map_err(|prev| {
TaskStateError::AlreadyExecuted(match prev {
CANCELLED => TerminalState::Cancelled,
COMPLETED => TerminalState::Completed,
other => unreachable!("unexpected task state: {other}"),
})
})?;
self.waker.wake();
Ok(())
}
pub(crate) fn complete(&self) -> Result<(), TaskStateError> {
self.state
.compare_exchange(RUNNING, COMPLETED, Ordering::AcqRel, Ordering::Acquire)
.map_err(|prev| {
TaskStateError::AlreadyTerminated(match prev {
CANCELLED => TerminalState::Cancelled,
COMPLETED => TerminalState::Completed,
other => unreachable!("unexpected task state: {other}"),
})
})?;
self.waker.wake();
Ok(())
}
#[must_use]
pub fn is_completed(&self) -> bool {
self.state.load(Ordering::Acquire) == COMPLETED
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.state.load(Ordering::Acquire) == CANCELLED
}
#[must_use]
pub fn is_executed(&self) -> bool {
self.state.load(Ordering::Acquire) != RUNNING
}
pub fn executed(&self) -> WaitForExecutionFuture {
WaitForExecutionFuture {
inner: self.clone(),
}
}
#[must_use]
pub fn elapsed(&self) -> Duration {
self.instant.elapsed()
}
#[must_use]
pub fn task_id(&self) -> &str {
&self.task_id
}
#[must_use]
pub fn execution_context(&self) -> Option<Arc<ExecutionContext>> {
self.inner.upgrade()
}
fn start_task(&self) {
self.sub_tasks_count.fetch_add(1, Ordering::Relaxed);
}
fn end_task(&self) {
self.sub_tasks_count.fetch_sub(1, Ordering::Relaxed);
}
#[must_use]
pub fn len(&self) -> usize {
self.sub_tasks_count.load(Ordering::Relaxed)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn run_until_executed<F>(&self, fut: F) -> SubTaskFuture<F>
where
F: Future,
{
self.start_task();
SubTaskFuture {
parent: self.clone(),
future: fut,
}
}
}
#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TaskStateError {
#[error("task cannot be cancelled: already {0}")]
AlreadyExecuted(TerminalState),
#[error("task cannot be completed: already {0}")]
AlreadyTerminated(TerminalState),
#[error("task cannot be found")]
TaskNotFound,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TerminalState {
Cancelled,
Completed,
}
impl fmt::Display for TerminalState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cancelled => write!(f, "cancelled"),
Self::Completed => write!(f, "completed"),
}
}
}
#[must_use = "futures do nothing unless you `.await` or poll them"]
#[derive(Debug)]
pub struct WaitForExecutionFuture {
inner: TaskContext,
}
impl Future for WaitForExecutionFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.inner.is_executed() {
return Poll::Ready(());
}
self.inner.waker.register(cx.waker());
if self.inner.is_executed() {
Poll::Ready(())
} else {
Poll::Pending
}
}
}
#[must_use = "futures do nothing unless polled"]
#[pin_project::pin_project(PinnedDrop)]
#[derive(Debug)]
pub struct SubTaskFuture<F: Future> {
parent: TaskContext,
#[pin]
future: F,
}
#[pin_project::pinned_drop]
impl<F: Future> PinnedDrop for SubTaskFuture<F> {
fn drop(self: Pin<&mut Self>) {
self.parent.end_task();
}
}
impl<F: Future> Future for SubTaskFuture<F> {
type Output = Option<F::Output>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
if let Poll::Ready(res) = this.future.poll(cx) {
Poll::Ready(Some(res))
} else if this.parent.is_executed() {
Poll::Ready(None)
} else {
Poll::Pending
}
}
}
impl PartialEq for TaskContext {
fn eq(&self, other: &Self) -> bool {
self.task_id() == other.task_id()
}
}
impl Eq for TaskContext {}
impl Hash for TaskContext {
fn hash<H: Hasher>(&self, state: &mut H) {
self.task_id().hash(state);
}
}
impl Borrow<str> for TaskContext {
fn borrow(&self) -> &str {
&self.task_id
}
}
impl<Args: Sync> FromRequest<Task<Args>> for TaskContext {
type Error = TaskStateError;
async fn from_request(task: &Task<Args>) -> Result<Self, Self::Error> {
let worker: &WorkerContext = task
.data()
.get_checked()
.map_err(|_| TaskStateError::TaskNotFound)?;
let token = worker.get_task_context(&task.ctx)?;
Ok(token)
}
}