1use std::marker::PhantomData;
13use std::panic::{
14 AssertUnwindSafe,
15 catch_unwind,
16};
17use std::sync::{
18 Arc,
19 Mutex,
20};
21use std::time::Duration;
22
23use qubit_atomic::AtomicRef;
24use qubit_error::BoxError;
25use qubit_function::{
26 Consumer,
27 Function,
28};
29use qubit_retry::{
30 AttemptFailure,
31 AttemptFailureDecision,
32 AttemptTimeoutPolicy,
33 AttemptTimeoutSource,
34 Retry,
35 RetryContext,
36 RetryError,
37 RetryOptions,
38};
39
40use crate::cas_decision::CasDecision;
41use crate::cas_outcome::CasOutcome;
42use crate::cas_success::CasSuccess;
43use crate::error::{
44 CasAttemptFailure,
45 CasError,
46 CasErrorKind,
47};
48use crate::event::{
49 CasContext,
50 CasEvent,
51 CasHooks,
52};
53use crate::observability::{
54 CasAlert,
55 CasObservabilityConfig,
56 CasObservabilityMode,
57 ListenerPanicPolicy,
58};
59use crate::report::{
60 CasExecutionOutcome,
61 CasExecutionReport,
62 CasReportBuilder,
63};
64use crate::strategy::CasStrategy;
65
66use super::cas_builder::CasBuilder;
67
68#[derive(Debug, Clone)]
70pub struct CasExecutor<T, E = BoxError> {
71 options: RetryOptions,
73 observability: CasObservabilityConfig,
75 marker: PhantomData<fn() -> (T, E)>,
77}
78
79enum AttemptSuccess<T, R> {
81 Updated {
83 previous: Arc<T>,
84 current: Arc<T>,
85 output: R,
86 },
87 Finished { current: Arc<T>, output: R },
89}
90
91struct CasReportFinishContext {
93 attempts_total: u32,
94 max_attempts: u32,
95 max_operation_elapsed: Option<Duration>,
96 max_total_elapsed: Option<Duration>,
97 outcome: CasExecutionOutcome,
98}
99
100impl CasReportFinishContext {
101 #[inline]
102 fn new(
103 attempts_total: u32,
104 max_attempts: u32,
105 max_operation_elapsed: Option<Duration>,
106 max_total_elapsed: Option<Duration>,
107 outcome: CasExecutionOutcome,
108 ) -> Self {
109 Self {
110 attempts_total,
111 max_attempts,
112 max_operation_elapsed,
113 max_total_elapsed,
114 outcome,
115 }
116 }
117}
118
119impl<T, E> CasExecutor<T, E> {
120 #[inline]
125 pub fn builder() -> CasBuilder<T, E> {
126 CasBuilder::new()
127 }
128
129 pub fn from_options(options: RetryOptions) -> Result<Self, qubit_retry::RetryConfigError> {
140 Self::builder().options(options).build()
141 }
142
143 pub fn latency_first() -> Self {
148 Self::builder()
149 .build_latency_first()
150 .expect("latency-first CAS strategy must be valid")
151 }
152
153 pub fn contention_adaptive() -> Self {
158 Self::builder()
159 .build_contention_adaptive()
160 .expect("contention-adaptive CAS strategy must be valid")
161 }
162
163 pub fn reliability_first() -> Self {
168 Self::builder()
169 .build_reliability_first()
170 .expect("reliability-first CAS strategy must be valid")
171 }
172
173 pub fn with_strategy(strategy: CasStrategy) -> Self {
181 Self::builder()
182 .strategy(strategy)
183 .build()
184 .expect("built-in CAS strategy must be valid")
185 }
186
187 #[inline]
196 pub(crate) fn new(options: RetryOptions, observability: CasObservabilityConfig) -> Self {
197 Self {
198 options,
199 observability,
200 marker: PhantomData,
201 }
202 }
203
204 #[inline]
209 pub fn options(&self) -> &RetryOptions {
210 &self.options
211 }
212
213 #[inline]
218 pub fn observability(&self) -> &CasObservabilityConfig {
219 &self.observability
220 }
221
222 pub fn execute<R, O>(&self, state: &AtomicRef<T>, operation: O) -> CasOutcome<T, R, E>
232 where
233 T: 'static,
234 E: 'static,
235 O: Function<T, CasDecision<T, R, E>>,
236 {
237 self.execute_with_hooks(state, operation, CasHooks::new())
238 }
239
240 pub fn execute_with_hooks<R, O>(
251 &self,
252 state: &AtomicRef<T>,
253 operation: O,
254 hooks: CasHooks,
255 ) -> CasOutcome<T, R, E>
256 where
257 T: 'static,
258 E: 'static,
259 O: Function<T, CasDecision<T, R, E>>,
260 {
261 let success_context = Arc::new(Mutex::new(None));
262 let attempt_snapshot = Arc::new(Mutex::new(None));
263 let report_builder = Arc::new(Mutex::new(CasReportBuilder::start()));
264 self.emit_started(&hooks, &report_builder);
265 let retry = self.build_retry(
266 &hooks,
267 Arc::clone(&success_context),
268 Arc::clone(&report_builder),
269 );
270 let attempt = retry.run(|| self.run_sync_attempt(state, &operation));
271 self.finish_execution(
272 attempt,
273 hooks,
274 success_context,
275 attempt_snapshot,
276 report_builder,
277 )
278 }
279
280 #[cfg(feature = "tokio")]
289 pub async fn execute_async<R, O, Fut>(
290 &self,
291 state: &AtomicRef<T>,
292 operation: O,
293 ) -> CasOutcome<T, R, E>
294 where
295 T: 'static,
296 E: 'static,
297 O: Fn(Arc<T>) -> Fut,
298 Fut: std::future::Future<Output = CasDecision<T, R, E>>,
299 {
300 self.execute_async_with_hooks(state, operation, CasHooks::new())
301 .await
302 }
303
304 #[cfg(feature = "tokio")]
314 pub async fn execute_async_with_hooks<R, O, Fut>(
315 &self,
316 state: &AtomicRef<T>,
317 operation: O,
318 hooks: CasHooks,
319 ) -> CasOutcome<T, R, E>
320 where
321 T: 'static,
322 E: 'static,
323 O: Fn(Arc<T>) -> Fut,
324 Fut: std::future::Future<Output = CasDecision<T, R, E>>,
325 {
326 let success_context = Arc::new(Mutex::new(None));
327 let attempt_snapshot = Arc::new(Mutex::new(None));
328 let report_builder = Arc::new(Mutex::new(CasReportBuilder::start()));
329 self.emit_started(&hooks, &report_builder);
330 let retry = self.build_retry(
331 &hooks,
332 Arc::clone(&success_context),
333 Arc::clone(&report_builder),
334 );
335 let attempt_snapshot_for_attempt = Arc::clone(&attempt_snapshot);
336 let attempt = retry
337 .run_async(|| {
338 self.run_async_attempt(state, &operation, Arc::clone(&attempt_snapshot_for_attempt))
339 })
340 .await;
341 self.finish_execution(
342 attempt,
343 hooks,
344 success_context,
345 attempt_snapshot,
346 report_builder,
347 )
348 }
349
350 fn build_retry(
359 &self,
360 hooks: &CasHooks,
361 success_context: Arc<Mutex<Option<RetryContext>>>,
362 report_builder: Arc<Mutex<CasReportBuilder>>,
363 ) -> Retry<CasAttemptFailure<T, E>>
364 where
365 T: 'static,
366 E: 'static,
367 {
368 let event_hook = hooks.event_hook();
369 let retry_timeout_policy = self
370 .options
371 .attempt_timeout()
372 .map(|attempt_timeout| attempt_timeout.policy());
373 let observability = self.observability.clone();
374
375 let mut builder = Retry::<CasAttemptFailure<T, E>>::builder()
376 .options(self.options.clone())
377 .on_success(move |context: &RetryContext| {
378 *success_context
379 .lock()
380 .expect("CAS success context slot should be lockable") = Some(*context);
381 })
382 .on_failure(
383 move |failure: &AttemptFailure<CasAttemptFailure<T, E>>, context: &RetryContext| {
384 let failure = match failure {
385 AttemptFailure::Panic(_) | AttemptFailure::Executor(_) => {
386 return AttemptFailureDecision::UseDefault;
387 }
388 AttemptFailure::Error(failure) => failure,
389 AttemptFailure::Timeout => {
390 let cas_context = CasContext::new(context);
391 report_builder
392 .lock()
393 .expect("CAS report builder should be lockable")
394 .record_timeout();
395 if Self::should_emit_events(&observability, &event_hook) {
396 let hook = event_hook
397 .as_ref()
398 .expect("event hook should exist when events are emitted");
399 Self::dispatch_event(
400 &observability,
401 hook,
402 CasEvent::AttemptFailed {
403 context: cas_context,
404 kind: crate::error::CasAttemptFailureKind::Timeout,
405 },
406 );
407 if context.attempt_timeout_source()
408 == Some(AttemptTimeoutSource::Configured)
409 && retry_timeout_policy == Some(AttemptTimeoutPolicy::Retry)
410 {
411 Self::dispatch_event(
412 &observability,
413 hook,
414 CasEvent::RetryRequested {
415 context: cas_context,
416 },
417 );
418 }
419 }
420 return AttemptFailureDecision::UseDefault;
421 }
422 };
423 let cas_context = CasContext::new(context);
424 {
425 let mut report = report_builder
426 .lock()
427 .expect("CAS report builder should be lockable");
428 match failure {
429 CasAttemptFailure::Conflict { .. } => report.record_conflict(),
430 CasAttemptFailure::Retry { .. } => report.record_retry_error(),
431 CasAttemptFailure::Abort { .. } => report.record_abort(),
432 CasAttemptFailure::Timeout { .. } => report.record_timeout(),
433 }
434 }
435 if Self::should_emit_events(&observability, &event_hook) {
436 Self::dispatch_event(
437 &observability,
438 event_hook
439 .as_ref()
440 .expect("event hook should exist when events are emitted"),
441 CasEvent::AttemptFailed {
442 context: cas_context,
443 kind: Self::failure_kind(failure),
444 },
445 );
446 }
447 match failure {
448 CasAttemptFailure::Conflict { .. } | CasAttemptFailure::Retry { .. } => {
449 if Self::should_emit_events(&observability, &event_hook) {
450 Self::dispatch_event(
451 &observability,
452 event_hook
453 .as_ref()
454 .expect("event hook should exist when events are emitted"),
455 CasEvent::RetryRequested {
456 context: cas_context,
457 },
458 );
459 }
460 AttemptFailureDecision::Retry
461 }
462 CasAttemptFailure::Abort { .. } => AttemptFailureDecision::Abort,
463 CasAttemptFailure::Timeout { .. } => AttemptFailureDecision::UseDefault,
464 }
465 },
466 );
467
468 if self.observability.listener_panic_policy() == ListenerPanicPolicy::Isolate {
469 builder = builder.isolate_listener_panics();
470 }
471 builder
472 .build()
473 .expect("validated CAS executor configuration must build retry policy")
474 }
475
476 fn run_sync_attempt<R, O>(
485 &self,
486 state: &AtomicRef<T>,
487 operation: &O,
488 ) -> Result<AttemptSuccess<T, R>, CasAttemptFailure<T, E>>
489 where
490 O: Function<T, CasDecision<T, R, E>>,
491 {
492 let current = state.load();
493 match operation.apply(current.as_ref()) {
494 CasDecision::Update { next, output } => {
495 match state.compare_set(¤t, Arc::clone(&next)) {
496 Ok(()) => Ok(AttemptSuccess::Updated {
497 previous: current,
498 current: next,
499 output,
500 }),
501 Err(actual) => Err(CasAttemptFailure::conflict(actual)),
502 }
503 }
504 CasDecision::Finish { output } => Ok(AttemptSuccess::Finished { current, output }),
505 CasDecision::Retry(error) => Err(CasAttemptFailure::retry(current, error)),
506 CasDecision::Abort(error) => Err(CasAttemptFailure::abort(current, error)),
507 }
508 }
509
510 #[cfg(feature = "tokio")]
519 async fn run_async_attempt<R, O, Fut>(
520 &self,
521 state: &AtomicRef<T>,
522 operation: &O,
523 attempt_snapshot: Arc<Mutex<Option<Arc<T>>>>,
524 ) -> Result<AttemptSuccess<T, R>, CasAttemptFailure<T, E>>
525 where
526 O: Fn(Arc<T>) -> Fut,
527 Fut: std::future::Future<Output = CasDecision<T, R, E>>,
528 {
529 let current = state.load();
530 *attempt_snapshot
531 .lock()
532 .expect("CAS attempt snapshot slot should be lockable") = Some(Arc::clone(¤t));
533 let decision = operation(Arc::clone(¤t)).await;
534
535 match decision {
536 CasDecision::Update { next, output } => {
537 match state.compare_set(¤t, Arc::clone(&next)) {
538 Ok(()) => Ok(AttemptSuccess::Updated {
539 previous: current,
540 current: next,
541 output,
542 }),
543 Err(actual) => Err(CasAttemptFailure::conflict(actual)),
544 }
545 }
546 CasDecision::Finish { output } => Ok(AttemptSuccess::Finished { current, output }),
547 CasDecision::Retry(error) => Err(CasAttemptFailure::retry(current, error)),
548 CasDecision::Abort(error) => Err(CasAttemptFailure::abort(current, error)),
549 }
550 }
551
552 fn finish_execution<R>(
562 &self,
563 attempt: Result<AttemptSuccess<T, R>, RetryError<CasAttemptFailure<T, E>>>,
564 hooks: CasHooks,
565 success_context: Arc<Mutex<Option<RetryContext>>>,
566 attempt_snapshot: Arc<Mutex<Option<Arc<T>>>>,
567 report_builder: Arc<Mutex<CasReportBuilder>>,
568 ) -> CasOutcome<T, R, E>
569 where
570 T: 'static,
571 E: 'static,
572 {
573 match attempt {
574 Ok(success) => {
575 let context = success_context
576 .lock()
577 .expect("CAS success context slot should be lockable")
578 .take()
579 .expect("retry success hook must capture CAS success context");
580 let attempts_total = context.attempt();
581 let max_attempts = context.max_attempts();
582 let max_operation_elapsed = context.max_operation_elapsed();
583 let max_total_elapsed = context.max_total_elapsed();
584 let outcome = match success {
585 AttemptSuccess::Updated { .. } => CasExecutionOutcome::SuccessUpdated,
586 AttemptSuccess::Finished { .. } => CasExecutionOutcome::SuccessFinished,
587 };
588 let success = self.enrich_success(success, context);
589 let report = self.finish_report(
590 &hooks,
591 report_builder,
592 CasReportFinishContext::new(
593 attempts_total,
594 max_attempts,
595 max_operation_elapsed,
596 max_total_elapsed,
597 outcome,
598 ),
599 );
600 CasOutcome::new(Ok(success), report)
601 }
602 Err(error) => {
603 let timeout_current = attempt_snapshot
604 .lock()
605 .expect("CAS attempt snapshot slot should be lockable")
606 .clone();
607 let error = CasError::new(error, timeout_current);
608 let context = error.context();
609 let outcome = Self::error_outcome(error.kind());
610 let report = self.finish_report(
611 &hooks,
612 report_builder,
613 CasReportFinishContext::new(
614 context.attempt(),
615 context.max_attempts(),
616 context.max_operation_elapsed(),
617 context.max_total_elapsed(),
618 outcome,
619 ),
620 );
621 CasOutcome::new(Err(error), report)
622 }
623 }
624 }
625
626 fn enrich_success<R>(
635 &self,
636 success: AttemptSuccess<T, R>,
637 context: RetryContext,
638 ) -> CasSuccess<T, R> {
639 let context = CasContext::new(&context);
640 match success {
641 AttemptSuccess::Updated {
642 previous,
643 current,
644 output,
645 } => CasSuccess::updated(previous, current, output, context),
646 AttemptSuccess::Finished { current, output } => {
647 CasSuccess::finished(current, output, context)
648 }
649 }
650 }
651
652 fn emit_started(&self, hooks: &CasHooks, report_builder: &Arc<Mutex<CasReportBuilder>>)
658 where
659 T: 'static,
660 E: 'static,
661 {
662 if hooks.event_hook().is_none()
663 || self.observability.mode() == CasObservabilityMode::ReportOnly
664 {
665 return;
666 }
667 let started_at = report_builder
668 .lock()
669 .expect("CAS report builder should be lockable")
670 .started_at();
671 let event_hook = hooks.event_hook();
672 Self::dispatch_event(
673 &self.observability,
674 event_hook
675 .as_ref()
676 .expect("event hook should exist when events are emitted"),
677 CasEvent::ExecutionStarted { started_at },
678 );
679 }
680
681 fn finish_report(
695 &self,
696 hooks: &CasHooks,
697 report_builder: Arc<Mutex<CasReportBuilder>>,
698 ctx: CasReportFinishContext,
699 ) -> CasExecutionReport
700 where
701 T: 'static,
702 E: 'static,
703 {
704 let report = report_builder
705 .lock()
706 .expect("CAS report builder should be lockable")
707 .finish(
708 ctx.attempts_total,
709 ctx.max_attempts,
710 ctx.max_operation_elapsed,
711 ctx.max_total_elapsed,
712 ctx.outcome,
713 );
714 let event_hook = hooks.event_hook();
715 if Self::should_emit_events(&self.observability, &event_hook) {
716 Self::dispatch_event(
717 &self.observability,
718 event_hook
719 .as_ref()
720 .expect("event hook should exist when events are emitted"),
721 CasEvent::ExecutionFinished {
722 report: report.clone(),
723 },
724 );
725 }
726 if self.observability.mode() == CasObservabilityMode::EventStreamWithAlert
727 && let Some(thresholds) = self.observability.contention_thresholds()
728 && report.is_contention_hot(&thresholds)
729 {
730 Self::dispatch_alert(
731 &self.observability,
732 &hooks.alert_hook(),
733 CasAlert::contention(report.clone(), thresholds),
734 );
735 }
736 report
737 }
738
739 #[inline]
747 fn error_outcome(kind: CasErrorKind) -> CasExecutionOutcome {
748 match kind {
749 CasErrorKind::Abort => CasExecutionOutcome::ErrorAbort,
750 CasErrorKind::Conflict => CasExecutionOutcome::ErrorConflictExhausted,
751 CasErrorKind::RetryExhausted => CasExecutionOutcome::ErrorRetryExhausted,
752 CasErrorKind::AttemptTimeout => CasExecutionOutcome::ErrorAttemptTimeout,
753 CasErrorKind::MaxOperationElapsedExceeded => {
754 CasExecutionOutcome::ErrorMaxOperationElapsedExceeded
755 }
756 CasErrorKind::MaxTotalElapsedExceeded => {
757 CasExecutionOutcome::ErrorMaxTotalElapsedExceeded
758 }
759 }
760 }
761
762 #[inline]
770 fn failure_kind(failure: &CasAttemptFailure<T, E>) -> crate::error::CasAttemptFailureKind {
771 failure.kind()
772 }
773
774 fn dispatch_event(
776 observability: &CasObservabilityConfig,
777 hook: &crate::event::CasEventHook,
778 event: CasEvent,
779 ) where
780 T: 'static,
781 E: 'static,
782 {
783 match observability.listener_panic_policy() {
784 ListenerPanicPolicy::Propagate => hook.accept(&event),
785 ListenerPanicPolicy::Isolate => {
786 let _ = catch_unwind(AssertUnwindSafe(|| hook.accept(&event)));
787 }
788 }
789 }
790
791 #[inline]
793 fn should_emit_events(
794 observability: &CasObservabilityConfig,
795 hook: &Option<crate::event::CasEventHook>,
796 ) -> bool {
797 observability.mode() != CasObservabilityMode::ReportOnly && hook.is_some()
798 }
799
800 fn dispatch_alert(
802 observability: &CasObservabilityConfig,
803 hook: &Option<crate::event::CasAlertHook>,
804 alert: CasAlert,
805 ) {
806 if let Some(hook) = hook {
807 match observability.listener_panic_policy() {
808 ListenerPanicPolicy::Propagate => hook.accept(&alert),
809 ListenerPanicPolicy::Isolate => {
810 let _ = catch_unwind(AssertUnwindSafe(|| hook.accept(&alert)));
811 }
812 }
813 }
814 }
815}