millipede_core/retry_strategy.rs
1//! Attempt-level retry strategy hooks.
2
3use crate::{
4 errors::{AntiBotTech, CrawlError},
5 proxy::{ProxyInfo, ProxyKind},
6 request::Request,
7 session::SessionId,
8};
9use std::time::Duration;
10
11/// Overrides carried to the next attempt of the same request.
12///
13/// Overrides live in engine memory keyed by request unique key and do not survive restarts.
14///
15/// # Examples
16///
17/// ```
18/// use millipede_core::retry_strategy::AttemptOverrides;
19/// use std::time::Duration;
20///
21/// let mut overrides = AttemptOverrides::default();
22/// overrides.backoff = Some(Duration::from_millis(250));
23/// ```
24#[derive(Debug, Default, Clone)]
25#[non_exhaustive]
26pub struct AttemptOverrides {
27 /// Proxy bucket requested for the next attempt.
28 pub proxy_kind: Option<ProxyKind>,
29 /// User-agent profile requested for the next attempt.
30 pub user_agent_profile: Option<String>,
31 /// Delay before the next attempt begins.
32 pub backoff: Option<Duration>,
33}
34
35/// Borrowed metadata describing a failed request attempt.
36///
37/// `session_id`, `proxy_info`, and `response_bytes` are present when the failed attempt produced a
38/// handler context, and absent for fetch-level failures.
39///
40/// # Examples
41///
42/// ```
43/// use millipede_core::retry_strategy::AttemptOutcome;
44///
45/// fn is_first_attempt(outcome: &AttemptOutcome<'_>) -> bool {
46/// outcome.attempt == 0
47/// }
48/// ```
49#[non_exhaustive]
50pub struct AttemptOutcome<'a> {
51 /// Request after preparation for this attempt.
52 pub request: &'a Request,
53 /// The request's retry count, independent of session rotations.
54 pub attempt: u32,
55 /// Observed or error-carried HTTP status.
56 pub status: Option<http::StatusCode>,
57 /// Attempt error.
58 pub error: Option<&'a CrawlError>,
59 /// Detected anti-bot technology.
60 pub anti_bot: Option<AntiBotTech>,
61 /// Proxy used by the attempt, when known.
62 pub proxy_info: Option<&'a ProxyInfo>,
63 /// Session used by the attempt, when known.
64 pub session_id: Option<&'a SessionId>,
65 /// Number of response body bytes, when known.
66 pub response_bytes: Option<usize>,
67}
68
69/// Session disposition for a strategy-authorized retry.
70///
71/// # Examples
72///
73/// ```
74/// use millipede_core::retry_strategy::SessionRetryAction;
75///
76/// let action = SessionRetryAction::Rotate;
77/// assert_ne!(action, SessionRetryAction::Keep);
78/// ```
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80#[non_exhaustive]
81pub enum SessionRetryAction {
82 /// Keep the current session classification.
83 #[default]
84 Keep,
85 /// Rotate the session for the next attempt.
86 Rotate,
87 /// Retire the session. In Phase 3 the engine accounts this like [`Self::Rotate`]; kind-level
88 /// retirement relies on the kind's own classification and is revisited in Phase 4.
89 Retire,
90}
91
92/// Owned instructions returned by a [`RetryStrategy`].
93///
94/// # Examples
95///
96/// ```
97/// use millipede_core::retry_strategy::{RetryDirective, SessionRetryAction};
98/// use std::time::Duration;
99///
100/// let directive = RetryDirective::retry()
101/// .backoff(Duration::from_secs(1))
102/// .session_action(SessionRetryAction::Rotate);
103/// assert!(directive.should_retry);
104/// ```
105#[derive(Debug, Clone, Default)]
106#[non_exhaustive]
107pub struct RetryDirective {
108 /// Whether the attempt should be retried.
109 pub should_retry: bool,
110 /// Delay before the next attempt.
111 pub backoff: Option<Duration>,
112 /// Proxy bucket for the next attempt.
113 pub proxy_kind: Option<ProxyKind>,
114 /// User-agent profile for the next attempt.
115 pub user_agent_profile: Option<String>,
116 /// Session disposition for the retry.
117 pub session_action: SessionRetryAction,
118}
119
120impl RetryDirective {
121 /// Creates a directive authorizing a retry.
122 pub fn retry() -> Self {
123 Self {
124 should_retry: true,
125 ..Self::default()
126 }
127 }
128 /// Creates a directive stopping retries.
129 pub fn stop() -> Self {
130 Self::default()
131 }
132 /// Sets the retry delay.
133 pub fn backoff(mut self, backoff: Duration) -> Self {
134 self.backoff = Some(backoff);
135 self
136 }
137 /// Sets the proxy bucket for the next attempt.
138 pub fn proxy_kind(mut self, proxy_kind: ProxyKind) -> Self {
139 self.proxy_kind = Some(proxy_kind);
140 self
141 }
142 /// Sets the user-agent profile for the next attempt.
143 pub fn user_agent_profile(mut self, profile: impl Into<String>) -> Self {
144 self.user_agent_profile = Some(profile.into());
145 self
146 }
147 /// Sets the session disposition.
148 pub fn session_action(mut self, action: SessionRetryAction) -> Self {
149 self.session_action = action;
150 self
151 }
152}
153
154/// Controls retries and next-attempt overrides for non-critical failures.
155///
156/// A configured strategy has full authority over `should_retry` and may retry a non-retryable
157/// error. Critical errors and requests marked `no_retry` never reach it.
158///
159/// # Examples
160///
161/// ```
162/// use std::time::Duration;
163/// use millipede_core::retry_strategy::{AttemptOutcome, RetryDirective, RetryStrategy};
164/// struct Backoff;
165/// impl RetryStrategy for Backoff {
166/// fn max_retries(&self) -> u32 { 3 }
167/// fn on_retry(&self, outcome: &AttemptOutcome<'_>) -> RetryDirective {
168/// RetryDirective::retry().backoff(Duration::from_secs(1 << outcome.attempt))
169/// }
170/// }
171/// ```
172pub trait RetryStrategy: Send + Sync + 'static {
173 /// Maximum ordinary retries when a request does not override the cap.
174 fn max_retries(&self) -> u32;
175 /// Returns instructions after a failed attempt.
176 fn on_retry(&self, outcome: &AttemptOutcome<'_>) -> RetryDirective;
177}