use std::sync::Arc;
use tokio::sync::{Notify, RwLock};
use super::{JoinCompletion, PendingJoins, RemovalReport};
use crate::{
core::{
actor::ActorExitReason,
deferred_drop::DropBundle,
outcome::TaskOutcome,
registry::{
Registry,
completion::{OutcomeTx, RemovalCompletion},
scheduler::{ActorJoinError, AttemptReaper},
state::{EntryState, Inner},
},
},
events::{Bus, Event, EventKind},
identity::TaskId,
};
struct OutcomeDropGuard<'a> {
outcome: Option<TaskOutcome>,
cleanup: &'a mut DropBundle,
}
impl<'a> OutcomeDropGuard<'a> {
fn new(outcome: TaskOutcome, cleanup: &'a mut DropBundle) -> Self {
Self {
outcome: Some(outcome),
cleanup,
}
}
fn get(&self) -> &TaskOutcome {
self.outcome
.as_ref()
.expect("the terminal outcome remains guarded until delivery")
}
fn take(&mut self) -> TaskOutcome {
self.outcome
.take()
.expect("the terminal outcome is delivered at most once")
}
}
impl Drop for OutcomeDropGuard<'_> {
fn drop(&mut self) {
if let Some(outcome) = self.outcome.take() {
self.cleanup.attach_outcome(outcome);
}
}
}
struct PendingTerminalReport {
state: Arc<RwLock<Inner>>,
empty_notify: Arc<Notify>,
pending_joins: Arc<PendingJoins>,
bus: Bus,
id: TaskId,
outcome: Option<TaskOutcome>,
done: Option<OutcomeTx>,
cleanup: Option<DropBundle>,
reaper: AttemptReaper,
completion: RemovalCompletion,
detach_on_drop: bool,
}
impl PendingTerminalReport {
fn take(&mut self) -> (TaskOutcome, Option<OutcomeTx>, DropBundle) {
(
self.outcome
.take()
.expect("one terminal outcome is classified"),
self.done.take(),
self.cleanup
.take()
.expect("one charged terminal bundle is retained"),
)
}
async fn commit(&mut self) {
let removed = {
let mut st = self.state.write().await;
let is_removing = st
.tasks
.get(&self.id)
.is_some_and(|entry| matches!(&entry.state, EntryState::Removing { .. }));
if !is_removing {
None
} else {
let entry = st
.tasks
.remove(&self.id)
.expect("the removing entry was checked above");
let EntryState::Removing {
completion: state_completion,
} = entry.state
else {
unreachable!("the removing entry was checked above")
};
if st.by_label.get(entry.label.as_ref()) == Some(&self.id) {
st.by_label.remove(entry.label.as_ref());
}
let is_empty = st.tasks.is_empty();
Some((entry.label, state_completion, is_empty))
}
};
let Some((label, state_completion, is_empty)) = removed else {
self.finish_without_membership();
return;
};
let (terminal_outcome, outcome, cleanup) = self.take();
let mut finalizer = TerminalFinalizer {
id: self.id,
empty_notify: &self.empty_notify,
pending_joins: &self.pending_joins,
state_completion: Some(state_completion),
report_completion: self.completion.clone(),
is_empty,
terminal: Some((self.reaper.clone(), cleanup)),
};
let cleanup = &mut finalizer
.terminal
.as_mut()
.expect("terminal ownership is installed")
.1;
Registry::report_outcome(
&self.bus,
self.id,
&label,
terminal_outcome,
outcome,
cleanup,
);
drop(finalizer);
}
fn finish_without_membership(&mut self) {
if self.retain_terminal() {
self.pending_joins.dec(self.id);
self.completion.complete_logical();
}
}
fn retain_terminal(&mut self) -> bool {
let Some(mut cleanup) = self.cleanup.take() else {
return false;
};
if let Some(outcome) = self.outcome.take() {
match self.done.take() {
Some(done) => {
if let Err(undelivered) = done.send(outcome) {
cleanup.attach_outcome(undelivered);
}
}
None => cleanup.attach_outcome(outcome),
}
}
self.reaper
.attach_terminal(self.id, cleanup, None, self.completion.clone());
true
}
fn take_continuation(&mut self) -> Self {
Self {
state: Arc::clone(&self.state),
empty_notify: Arc::clone(&self.empty_notify),
pending_joins: Arc::clone(&self.pending_joins),
bus: self.bus.clone(),
id: self.id,
outcome: self.outcome.take(),
done: self.done.take(),
cleanup: self.cleanup.take(),
reaper: self.reaper.clone(),
completion: self.completion.clone(),
detach_on_drop: false,
}
}
fn retain_without_logical_completion(&mut self) {
let _ = self.retain_terminal();
}
}
impl Drop for PendingTerminalReport {
fn drop(&mut self) {
if self.cleanup.is_none() {
return;
}
if self.detach_on_drop
&& let Ok(runtime) = tokio::runtime::Handle::try_current()
{
let mut continuation = self.take_continuation();
drop(runtime.spawn(async move {
continuation.commit().await;
}));
return;
}
self.retain_without_logical_completion();
}
}
pub(in crate::core::registry) struct TerminalFinalizer<'a> {
pub(in crate::core::registry) id: TaskId,
pub(in crate::core::registry) empty_notify: &'a Notify,
pub(in crate::core::registry) pending_joins: &'a PendingJoins,
pub(in crate::core::registry) state_completion: Option<RemovalCompletion>,
pub(in crate::core::registry) report_completion: RemovalCompletion,
pub(in crate::core::registry) is_empty: bool,
pub(in crate::core::registry) terminal: Option<(AttemptReaper, DropBundle)>,
}
impl Drop for TerminalFinalizer<'_> {
fn drop(&mut self) {
if let Some((reaper, bundle)) = self.terminal.take() {
reaper.attach_terminal(
self.id,
bundle,
self.state_completion.clone(),
self.report_completion.clone(),
);
}
self.pending_joins.dec(self.id);
if let Some(completion) = &self.state_completion {
completion.complete_logical();
}
self.report_completion.complete_logical();
if self.is_empty {
self.empty_notify.notify_waiters();
}
}
}
impl Registry {
pub(in crate::core::registry) async fn finish_removal(
state: &Arc<RwLock<Inner>>,
empty_notify: &Arc<Notify>,
pending_joins: &Arc<PendingJoins>,
bus: &Bus,
reaper: &AttemptReaper,
report: RemovalReport,
) {
let RemovalReport {
id,
outcome,
join,
completion: removal_completion,
mut cleanup,
} = report;
let terminal_outcome = match join {
JoinCompletion::Joined(result) => Self::outcome_of(result, &mut cleanup),
JoinCompletion::ForceAborted => TaskOutcome::ForceAborted,
};
let mut pending_report = PendingTerminalReport {
state: Arc::clone(state),
empty_notify: Arc::clone(empty_notify),
pending_joins: Arc::clone(pending_joins),
bus: bus.clone(),
id,
outcome: Some(terminal_outcome),
done: outcome,
cleanup: Some(cleanup),
reaper: reaper.clone(),
completion: removal_completion,
detach_on_drop: true,
};
pending_report.commit().await;
}
fn report_outcome(
bus: &Bus,
id: TaskId,
label: &str,
outcome: TaskOutcome,
done: Option<OutcomeTx>,
cleanup: &mut DropBundle,
) {
let mut outcome = OutcomeDropGuard::new(outcome, cleanup);
bus.publish_lazy(|| {
let mut finished = Event::new(EventKind::TaskFinished)
.with_task(label)
.with_id(id)
.with_outcome_kind(outcome.get().kind());
match outcome.get() {
TaskOutcome::Failed {
reason, exit_code, ..
}
| TaskOutcome::Fatal {
reason, exit_code, ..
} => {
finished = finished.with_reason(Arc::clone(reason));
if let Some(code) = exit_code {
finished = finished.with_exit_code(*code);
}
}
TaskOutcome::ForceAborted => {
finished =
finished.with_reason("task did not stop within grace; force-aborted");
}
TaskOutcome::Panicked => {
finished = finished.with_reason("internal task runner panicked");
}
TaskOutcome::Completed | TaskOutcome::Canceled | TaskOutcome::Rejected { .. } => {}
}
finished
});
if let Some(done) = done
&& let Err(undelivered) = done.send(outcome.take())
{
outcome.cleanup.attach_outcome(undelivered);
}
bus.publish_lazy(|| {
Event::new(EventKind::TaskRemoved)
.with_task(label)
.with_id(id)
});
}
fn outcome_of(
res: Result<ActorExitReason, ActorJoinError>,
cleanup: &mut DropBundle,
) -> TaskOutcome {
match res {
Ok(ActorExitReason::Completed) => TaskOutcome::Completed,
Ok(ActorExitReason::Canceled) => TaskOutcome::Canceled,
Ok(ActorExitReason::Panicked { cleanup_poisoned }) => {
if cleanup_poisoned {
cleanup.poison();
}
TaskOutcome::Panicked
}
Ok(ActorExitReason::Exhausted {
reason,
exit_code,
source,
}) => TaskOutcome::Failed {
reason,
exit_code,
source,
},
Ok(ActorExitReason::Fatal {
reason,
exit_code,
source,
}) => TaskOutcome::Fatal {
reason,
exit_code,
source,
},
Err(e) if e.is_panic() => {
if e.cleanup_poisoned() {
cleanup.poison();
}
TaskOutcome::Panicked
}
Err(_aborted) => TaskOutcome::ForceAborted,
}
}
}