Skip to main content

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}