use std::panic;
use std::sync::{
Arc,
mpsc,
};
use std::thread::JoinHandle;
use std::time::{
Duration,
Instant,
};
use super::attempt_cancel_token::AttemptCancelToken;
use super::blocking_attempt::BlockingAttempt;
use super::blocking_attempt_outcome::BlockingAttemptOutcome;
use super::blocking_value_operation::BlockingValueOperation;
use super::retry::{
Retry,
sleep_blocking,
};
use super::retry_flow_action::RetryFlowAction;
use super::retry_flow_state::RetryFlowState;
use crate::{
AttemptExecutorError,
AttemptFailure,
AttemptPanic,
RetryError,
RetryErrorReason,
};
const WORKER_DISCONNECTED_MESSAGE: &str = "retry worker thread stopped without sending a result";
const WORKER_SPAWN_FAILED_MESSAGE: &str = "failed to spawn retry worker thread";
macro_rules! worker_disconnected_failure {
() => {
Err(AttemptFailure::Executor(AttemptExecutorError::new(
WORKER_DISCONNECTED_MESSAGE,
)))
};
}
macro_rules! worker_spawn_failure {
($error:expr) => {
BlockingAttemptOutcome::new(
Err(AttemptFailure::Executor(
AttemptExecutorError::with_context(
WORKER_SPAWN_FAILED_MESSAGE,
&$error.to_string(),
),
)),
0,
)
};
}
#[allow(clippy::result_large_err)]
impl<E> Retry<E> {
pub fn run_in_worker<T, F>(&self, operation: F) -> Result<T, RetryError<E>>
where
T: Send + 'static,
E: Send + 'static,
F: Fn(AttemptCancelToken) -> Result<T, E> + Send + Sync + 'static,
{
let operation = Arc::new(BlockingValueOperation::new(operation));
let worker_operation: Arc<dyn BlockingAttempt<E>> = operation.clone();
self.run_worker_operation(worker_operation)
.map(|()| operation.take_value())
}
fn run_worker_operation(
&self,
operation: Arc<dyn BlockingAttempt<E>>,
) -> Result<(), RetryError<E>>
where
E: Send + 'static,
{
let mut state = RetryFlowState::new();
loop {
let attempt_timeout =
self.effective_attempt_timeout(state.operation_elapsed, state.total_elapsed());
self.ensure_elapsed_budget_available(&mut state, attempt_timeout)?;
let attempt_timeout =
self.effective_attempt_timeout(state.operation_elapsed, state.total_elapsed());
self.emit_before_attempt_for_next_attempt(&mut state, attempt_timeout);
let attempt_timeout =
self.effective_attempt_timeout(state.operation_elapsed, state.total_elapsed());
self.ensure_elapsed_budget_available(&mut state, attempt_timeout)?;
let attempt_start = Instant::now();
let outcome = call_blocking_attempt(
Arc::clone(&operation),
attempt_timeout.duration,
self.options().worker_cancel_grace(),
);
let attempt_elapsed = attempt_start.elapsed();
state.add_operation_elapsed(attempt_elapsed);
let context = self
.context_from_state(&state, attempt_elapsed, attempt_timeout.duration)
.with_attempt_timeout_source(attempt_timeout.source)
.with_unreaped_worker_count(outcome.unreaped_worker_count);
match outcome.result {
Ok(()) => {
self.emit_attempt_success(&context);
return Ok(());
}
Err(failure) => {
if let Some(reason) = attempt_timeout.elapsed_timeout_reason(&failure) {
return Err(self.emit_error(RetryError::new(
reason,
Some(failure),
context,
)));
}
let retry_block_reason = (context.unreaped_worker_count() > 0)
.then_some(RetryErrorReason::WorkerStillRunning);
match self.handle_failure(
state.attempts,
failure,
context,
retry_block_reason,
state.started_at,
) {
RetryFlowAction::Retry { delay, failure } => {
sleep_blocking(delay);
state.record_last_failure(failure);
}
RetryFlowAction::Finished(error) => return Err(self.emit_error(error)),
}
}
}
}
}
#[inline]
pub fn run_blocking_with_timeout<T, F>(&self, operation: F) -> Result<T, RetryError<E>>
where
T: Send + 'static,
E: Send + 'static,
F: Fn(AttemptCancelToken) -> Result<T, E> + Send + Sync + 'static,
{
self.run_in_worker(operation)
}
}
fn call_blocking_attempt<E>(
operation: Arc<dyn BlockingAttempt<E>>,
attempt_timeout: Option<Duration>,
worker_cancel_grace: Duration,
) -> BlockingAttemptOutcome<(), E>
where
E: Send + 'static,
{
let token = AttemptCancelToken::new();
let (sender, receiver) = mpsc::sync_channel(1);
let worker_token = token.clone();
let worker = match std::thread::Builder::new()
.name("qubit-retry-worker".to_string())
.spawn(move || {
let result =
panic::catch_unwind(panic::AssertUnwindSafe(|| operation.call(worker_token)));
let attempt_result = match result {
Ok(result) => result,
Err(payload) => Err(AttemptFailure::Panic(AttemptPanic::from_payload(payload))),
};
let _ = sender.send(attempt_result);
}) {
Ok(worker) => worker,
Err(error) => return worker_spawn_failure!(error),
};
match attempt_timeout {
Some(attempt_timeout) => worker_timeout_result_to_attempt_outcome(
receiver.recv_timeout(attempt_timeout),
receiver,
worker,
&token,
worker_cancel_grace,
),
None => worker_recv_result_to_attempt_outcome(receiver.recv(), worker),
}
}
fn worker_recv_result_to_attempt_outcome<E>(
result: Result<Result<(), AttemptFailure<E>>, mpsc::RecvError>,
worker: JoinHandle<()>,
) -> BlockingAttemptOutcome<(), E> {
join_finished_worker(worker);
match result {
Ok(result) => BlockingAttemptOutcome::new(result, 0),
Err(_) => BlockingAttemptOutcome::new(worker_disconnected_failure!(), 0),
}
}
fn worker_timeout_result_to_attempt_outcome<E>(
result: Result<Result<(), AttemptFailure<E>>, mpsc::RecvTimeoutError>,
receiver: mpsc::Receiver<Result<(), AttemptFailure<E>>>,
worker: JoinHandle<()>,
token: &AttemptCancelToken,
worker_cancel_grace: Duration,
) -> BlockingAttemptOutcome<(), E>
where
E: Send + 'static,
{
match result {
Ok(result) => {
join_finished_worker(worker);
BlockingAttemptOutcome::new(result, 0)
}
Err(mpsc::RecvTimeoutError::Timeout) => {
token.cancel();
let worker_exited = wait_for_cancelled_worker(&receiver, worker, worker_cancel_grace);
let unreaped_worker_count = if worker_exited { 0 } else { 1 };
BlockingAttemptOutcome::new(Err(AttemptFailure::Timeout), unreaped_worker_count)
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
join_finished_worker(worker);
BlockingAttemptOutcome::new(worker_disconnected_failure!(), 0)
}
}
}
fn wait_for_cancelled_worker<E>(
receiver: &mpsc::Receiver<Result<(), AttemptFailure<E>>>,
worker: JoinHandle<()>,
grace: Duration,
) -> bool {
let exited = if grace.is_zero() {
match receiver.try_recv() {
Ok(_) | Err(mpsc::TryRecvError::Disconnected) => true,
Err(mpsc::TryRecvError::Empty) => false,
}
} else {
match receiver.recv_timeout(grace) {
Ok(_) | Err(mpsc::RecvTimeoutError::Disconnected) => true,
Err(mpsc::RecvTimeoutError::Timeout) => false,
}
};
if exited {
join_finished_worker(worker);
}
exited
}
fn join_finished_worker(worker: JoinHandle<()>) {
let _ = worker.join();
}