pub(crate) struct TerminationTally(pub(crate) std::sync::Arc<std::sync::atomic::AtomicUsize>);
impl TerminationTally {
pub(crate) fn track_spawn<T, E>(
terminated: &std::sync::Arc<std::sync::atomic::AtomicUsize>,
spawn: impl FnOnce(Self) -> Result<T, E>,
) -> Result<T, E> {
let result = spawn(Self(std::sync::Arc::clone(terminated)));
if result.is_err() {
terminated.fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
}
result
}
}
impl Drop for TerminationTally {
fn drop(&mut self) {
self.0.fetch_add(1, std::sync::atomic::Ordering::Release);
}
}
pub mod adaptive_hedge;
#[cfg(test)]
pub mod adaptive_hedge_metamorphic;
pub mod bracket;
#[cfg(test)]
pub mod bracket_metamorphic;
pub mod bulkhead;
#[cfg(test)]
pub mod bulkhead_metamorphic;
pub mod circuit_breaker;
pub mod first_ok;
pub mod hedge;
pub mod join;
pub mod join_set;
pub mod laws;
pub mod map_reduce;
pub mod pipeline;
pub mod quorum;
pub mod race;
#[cfg(test)]
pub mod race_join_dist_metamorphic;
#[cfg(test)]
pub mod race_metamorphic;
pub mod rate_limit;
pub mod retry;
pub mod select;
pub mod timeout;
#[cfg(test)]
pub mod timeout_metamorphic;
pub use adaptive_hedge::PeakEwmaHedgeController;
pub use bracket::{BracketError, bracket, bracket_move, commit_section, try_commit_section};
pub use bulkhead::{
Bulkhead, BulkheadError, BulkheadMetrics, BulkheadPermit, BulkheadPolicy,
BulkheadPolicyBuilder, BulkheadRegistry, FullCallback,
};
pub use circuit_breaker::{
CircuitBreaker, CircuitBreakerError, CircuitBreakerMetrics, CircuitBreakerPolicy,
CircuitBreakerPolicyBuilder, FailurePredicate, Permit, SlidingWindowConfig, State,
StateChangeCallback,
};
pub use first_ok::{
FirstOk, FirstOkError, FirstOkFailure, FirstOkResult, FirstOkSuccess, first_ok_outcomes,
first_ok_to_result,
};
pub use hedge::{
AdaptiveHedgePolicy, Hedge, HedgeConfig, HedgeError, HedgeFuture, HedgeResult, HedgeWinner,
hedge, hedge_outcomes, hedge_to_result,
};
pub use join::{
Join, Join2Result, JoinAll, JoinAllError, JoinAllResult, JoinError, aggregate_outcomes,
join_all_outcomes, join_all_to_result, join2_outcomes, join2_to_result, make_join_all_result,
};
pub use join_set::{JoinSet, JoinSummary};
pub use map_reduce::{
MapReduce, MapReduceError, MapReduceExecution, MapReduceExecutionError, MapReduceLimits,
MapReduceResult, MapReduceStopCause, execute_map_reduce, make_map_reduce_result,
map_reduce_outcomes, map_reduce_to_result, reduce_successes,
};
pub use pipeline::{
FailedStage, Pipeline, PipelineConfig, PipelineError, PipelineExecution,
PipelineExecutionConfig, PipelineExecutionError, PipelineExecutionReport,
PipelineExecutionSummary, PipelineResult, pipeline_n_outcomes, pipeline_to_result,
pipeline_with_final, pipeline2_outcomes, pipeline3_outcomes, stage_outcome_to_result,
};
pub use quorum::{
Quorum, QuorumError, QuorumFailure, QuorumResult, quorum_achieved, quorum_outcomes,
quorum_still_possible, quorum_to_result,
};
pub use race::{
Cancel, PollingOrder, Race, Race2, Race2Result, Race3, Race4, RaceAll, RaceAllError,
RaceAllResult, RaceError, RaceResult, RaceWinner, make_race_all_result, race_all_outcomes,
race_all_to_result, race2_outcomes, race2_to_result,
};
pub use rate_limit::{
RateLimitAlgorithm, RateLimitError, RateLimitMetrics, RateLimitPolicy, RateLimitPolicyBuilder,
RateLimiter, RateLimiterRegistry, SlidingWindowRateLimiter, WaitStrategy,
};
pub use retry::{
AlwaysRetry, NeverRetry, Retry, RetryError, RetryFailure, RetryIf, RetryPolicy, RetryPredicate,
RetryResult, RetryState, calculate_deadline as retry_deadline, calculate_delay,
make_retry_result, retry, total_delay_budget,
};
pub use select::{
Either, Select, SelectAll, SelectAllDrain, SelectAllDrainError, SelectAllDrainResult,
SelectAllError, SelectError,
};
pub use timeout::{
TimedError, TimedResult, Timeout, TimeoutConfig, TimeoutError, effective_deadline,
make_timed_result,
};
#[cfg(test)]
mod termination_tally_tests {
use super::TerminationTally;
use crate::Cx;
use crate::runtime::{JoinError, RuntimeBuilder, SpawnError};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn termination_tally_rejected_spawn_preserves_native_child_accounting() {
let runtime = RuntimeBuilder::current_thread().build().unwrap();
runtime.block_on(runtime.handle().spawn(async {
let cx = Cx::current().expect("native parent context");
let terminated = Arc::new(AtomicUsize::new(0));
let mut completed = TerminationTally::track_spawn(&terminated, |tally| {
cx.spawn(move |_| async move {
let _tally = tally;
17
})
})
.unwrap();
assert_eq!(completed.join(&cx).await, Ok(17));
assert_eq!(terminated.load(Ordering::Acquire), 1);
let entered = Arc::new(AtomicUsize::new(0));
let child_entered = Arc::clone(&entered);
let pending = cx.pending_spawn_counter_handle().unwrap();
let before = pending.count();
let mut unpolled = TerminationTally::track_spawn(&terminated, |tally| {
cx.spawn(move |child| async move {
let _tally = tally;
child_entered.fetch_add(1, Ordering::Release);
assert!(
child.checkpoint().is_err(),
"pre-poll abort must be visible in the child's cleanup poll"
);
let (_sender, mut receiver) = crate::channel::mpsc::channel::<()>(1);
assert_eq!(
receiver.recv(&child).await,
Err(crate::channel::mpsc::RecvError::Cancelled)
);
})
})
.unwrap();
assert_eq!(pending.count(), before + 1, "child is still queued");
assert_eq!(entered.load(Ordering::Acquire), 0);
let unavailable = Cx::for_testing();
let scope = cx.scope();
for scoped in [false, true] {
let uncompensated = Arc::new(AtomicUsize::new(0));
let tally = TerminationTally(Arc::clone(&uncompensated));
let factory = move |_| async move {
let _tally = tally;
};
let old_boundary = if scoped {
unavailable.spawn_in_cancellation_dominant(&scope, factory)
} else {
unavailable.spawn(factory)
};
assert!(matches!(old_boundary, Err(SpawnError::RuntimeUnavailable)));
assert_eq!(uncompensated.load(Ordering::Acquire), 1);
let rejected_entered = Arc::clone(&entered);
let rejected = TerminationTally::track_spawn(&terminated, |tally| {
let factory = move |_| async move {
let _tally = tally;
rejected_entered.fetch_add(1, Ordering::Release);
};
if scoped {
unavailable.spawn_in_cancellation_dominant(&scope, factory)
} else {
unavailable.spawn(factory)
}
});
assert!(matches!(rejected, Err(SpawnError::RuntimeUnavailable)));
assert_eq!(
terminated.load(Ordering::Acquire),
1,
"rejection must not invent a termination or erase the completed child"
);
assert_eq!(pending.count(), before + 1);
assert_eq!(entered.load(Ordering::Acquire), 0);
eprintln!(
"old-boundary control (scoped={scoped}): rejected spawn counted 1 nonexistent child; corrected accounting retains only the 1 completed native child"
);
}
unpolled.abort();
assert!(matches!(
unpolled.join(&cx).await,
Err(JoinError::Cancelled(ref reason))
if reason.kind == crate::types::CancelKind::User
));
assert_eq!(
entered.load(Ordering::Acquire),
1,
"pre-poll abort delivers one cleanup poll while retaining task-level cancellation"
);
assert_eq!(terminated.load(Ordering::Acquire), 2);
assert_eq!(pending.count(), before);
}));
}
#[test]
fn termination_tally_accepted_drop_before_spawn_returns_still_counts() {
let terminated = Arc::new(AtomicUsize::new(0));
let result: Result<(), ()> = TerminationTally::track_spawn(&terminated, |tally| {
drop(tally);
Ok(())
});
assert_eq!(result, Ok(()));
assert_eq!(terminated.load(Ordering::Acquire), 1);
}
}