execution_policy/
builder.rs1use std::sync::Arc;
4use std::time::Duration;
5
6use crate::breaker::CircuitBreaker;
7use crate::concurrency::ConcurrencyLimit;
8use crate::core::Core;
9#[cfg(feature = "tokio")]
10use crate::core::DefaultCore;
11use crate::event::{Event, EventHook};
12use crate::plan::{CompiledBreaker, Plan};
13use crate::policy::ExecutionPolicy;
14use crate::retry::Retry;
15
16#[derive(Debug, Clone)]
18pub struct BuildError(pub(crate) String);
19
20impl std::fmt::Display for BuildError {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(f, "invalid execution policy: {}", self.0)
23 }
24}
25impl std::error::Error for BuildError {}
26
27pub struct ExecutionPolicyBuilder<T, E> {
29 retry: Option<Retry<T, E>>,
30 attempt_timeout: Option<Duration>,
31 total_timeout: Option<Duration>,
32 breaker: Option<CircuitBreaker<E>>,
33 concurrency: Option<ConcurrencyLimit>,
34 on_event: Option<EventHook>,
35}
36
37impl<T, E> std::fmt::Debug for ExecutionPolicyBuilder<T, E> {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("ExecutionPolicyBuilder")
40 .field("retry", &self.retry)
41 .field("attempt_timeout", &self.attempt_timeout)
42 .field("total_timeout", &self.total_timeout)
43 .field("breaker", &self.breaker)
44 .field("concurrency", &self.concurrency)
45 .field("on_event", &self.on_event.as_ref().map(|_| "<fn>"))
46 .finish()
47 }
48}
49
50impl<T, E> Default for ExecutionPolicyBuilder<T, E> {
51 fn default() -> Self {
52 Self {
53 retry: None,
54 attempt_timeout: None,
55 total_timeout: None,
56 breaker: None,
57 concurrency: None,
58 on_event: None,
59 }
60 }
61}
62
63impl<T, E> ExecutionPolicyBuilder<T, E> {
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn retry(mut self, retry: Retry<T, E>) -> Self {
69 debug_assert!(self.retry.is_none(), "retry configured twice (last-wins)");
70 self.retry = Some(retry);
71 self
72 }
73 pub fn attempt_timeout(mut self, d: Duration) -> Self {
74 self.attempt_timeout = Some(d);
75 self
76 }
77 pub fn total_timeout(mut self, d: Duration) -> Self {
78 self.total_timeout = Some(d);
79 self
80 }
81 pub fn circuit_breaker(mut self, breaker: CircuitBreaker<E>) -> Self {
82 debug_assert!(
83 self.breaker.is_none(),
84 "circuit_breaker configured twice (last-wins)"
85 );
86 self.breaker = Some(breaker);
87 self
88 }
89 pub fn concurrency_limit(mut self, limit: impl Into<ConcurrencyLimit>) -> Self {
90 debug_assert!(
91 self.concurrency.is_none(),
92 "concurrency_limit configured twice (last-wins)"
93 );
94 self.concurrency = Some(limit.into());
95 self
96 }
97 pub fn on_event(mut self, hook: impl Fn(&Event) + Send + Sync + 'static) -> Self {
100 self.on_event = Some(Arc::new(hook));
101 self
102 }
103 #[cfg(feature = "tracing")]
105 pub fn with_tracing(self) -> Self {
106 self.on_event(|e: &Event| tracing::debug!(event = ?e, "execution-policy"))
107 }
108
109 fn validate(&self) -> Result<(), BuildError> {
110 if let Some(r) = &self.retry {
111 if r.max_attempts_value() == 0 {
112 return Err(BuildError("max_attempts must be >= 1".into()));
113 }
114 }
115 if let (Some(a), Some(t)) = (self.attempt_timeout, self.total_timeout) {
116 if a > t {
117 return Err(BuildError(format!(
118 "attempt_timeout ({a:?}) must be <= total_timeout ({t:?})"
119 )));
120 }
121 }
122 Ok(())
123 }
124
125 fn compile(self) -> Plan<T, E> {
126 let breaker = self.breaker.map(|b| {
127 let (runtime, record_when) = b.compile();
128 CompiledBreaker {
129 runtime: std::sync::Arc::new(runtime),
130 record_when,
131 }
132 });
133 let concurrency = self.concurrency.as_ref().map(|c| c.compile());
134 Plan {
135 retry: self.retry.unwrap_or_else(Retry::none),
136 attempt_timeout: self.attempt_timeout,
137 total_timeout: self.total_timeout,
138 breaker,
139 concurrency,
140 on_event: self.on_event,
141 }
142 }
143
144 #[cfg(feature = "tokio")]
146 pub fn build(self) -> ExecutionPolicy<DefaultCore, T, E> {
147 self.build_with(DefaultCore::new())
148 }
149
150 #[cfg(feature = "tokio")]
152 pub fn try_build(self) -> Result<ExecutionPolicy<DefaultCore, T, E>, BuildError> {
153 self.validate()?;
154 Ok(ExecutionPolicy::from_parts(
155 DefaultCore::new(),
156 Arc::new(self.compile()),
157 ))
158 }
159
160 pub fn build_with<C: Core>(self, core: C) -> ExecutionPolicy<C, T, E> {
162 if let Err(e) = self.validate() {
163 panic!("{e}");
164 }
165 ExecutionPolicy::from_parts(core, Arc::new(self.compile()))
166 }
167}
168
169#[cfg(all(test, feature = "test-util"))]
170mod tests {
171 use super::*;
172 use crate::core::{ManualClock, TestCore};
173
174 #[test]
175 #[should_panic(expected = "max_attempts must be >= 1")]
176 fn build_panics_on_zero_attempts() {
177 let _ = ExecutionPolicyBuilder::<u32, &str>::new()
178 .retry(Retry::exponential().max_attempts(0))
179 .build_with(TestCore::new(ManualClock::new()));
180 }
181
182 #[test]
183 fn try_build_reports_timeout_inversion() {
184 let err = ExecutionPolicyBuilder::<u32, &str>::new()
185 .attempt_timeout(Duration::from_secs(5))
186 .total_timeout(Duration::from_secs(2))
187 .validate();
188 assert!(err.is_err());
189 }
190}