Skip to main content

millipede_core/storage/
queue.rs

1//! Lease-based request queue contracts.
2
3use super::{StorageError, StorageResult};
4use crate::request::{Request, RequestId};
5use std::{fmt, time::Duration};
6
7/// Identifier for one active request lease.
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub struct LeaseId(u64);
10
11impl LeaseId {
12    /// Creates a lease identifier from its raw value.
13    pub fn new(raw: u64) -> Self {
14        Self(raw)
15    }
16
17    /// Returns the raw identifier value.
18    pub fn as_u64(&self) -> u64 {
19        self.0
20    }
21}
22
23impl fmt::Display for LeaseId {
24    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25        self.0.fmt(formatter)
26    }
27}
28
29/// Temporary, linear ownership of a queued request.
30///
31/// This type deliberately does not implement `Clone`: exactly one of
32/// [`RequestQueue::mark_handled`], [`RequestQueue::reclaim`], or
33/// [`RequestQueue::abandon`] consumes it.
34#[derive(Debug)]
35pub struct Lease {
36    /// Leased request.
37    pub request: Request,
38    /// Identifier used to renew this lease.
39    pub lease_id: LeaseId,
40    /// Current lease deadline.
41    pub expires_at: std::time::Instant,
42}
43
44/// Options controlling insertion of requests.
45#[derive(Debug, Clone, Default)]
46#[non_exhaustive]
47#[must_use = "add options do nothing unless passed to RequestQueue::add"]
48pub struct AddOptions {
49    /// Whether to insert at the front of the queue.
50    pub forefront: bool,
51}
52
53/// Options controlling return of a leased request to the queue.
54#[derive(Debug, Clone)]
55#[non_exhaustive]
56#[must_use = "reclaim options do nothing unless passed to RequestQueue::reclaim"]
57pub struct ReclaimOptions {
58    /// Whether to reinsert at the front of the queue.
59    pub forefront: bool,
60    /// Whether to increment the request retry count.
61    pub increment_retry: bool,
62}
63
64impl Default for ReclaimOptions {
65    fn default() -> Self {
66        Self {
67            forefront: false,
68            increment_retry: true,
69        }
70    }
71}
72
73/// Result metadata for adding one request.
74#[derive(Debug, Clone)]
75#[must_use = "queue insertion results report deduplication state"]
76pub struct ProcessedRequest {
77    /// Stable request identifier.
78    pub request_id: RequestId,
79    /// Queue deduplication key.
80    pub unique_key: String,
81    /// Whether the request was already known to the queue.
82    pub was_already_present: bool,
83    /// Whether the known request had already been handled.
84    pub was_already_handled: bool,
85}
86
87/// Alternate interface spelling for [`ProcessedRequest`]; both names describe the same payload.
88pub type QueueOpInfo = ProcessedRequest;
89
90/// A source from which requests can be added.
91///
92/// Sitemap ingestion ships as [`crate::sitemap::RequestQueueWithSitemap`], a queue wrapper rather
93/// than a `RequestSource` variant, which keeps queue backends decoupled from fetching. This enum
94/// remains non-exhaustive for future request-source variants.
95#[derive(Debug, Clone)]
96#[non_exhaustive]
97pub enum RequestSource {
98    /// One fully built request.
99    Request(Request),
100}
101
102impl From<Request> for RequestSource {
103    fn from(request: Request) -> Self {
104        Self::Request(request)
105    }
106}
107
108/// Completion result for a batched request addition.
109#[derive(Debug, Clone)]
110#[non_exhaustive]
111#[must_use = "batched insertion results report processed requests"]
112pub struct AddRequestsBatchedResult {
113    /// All processed requests.
114    pub processed: Vec<ProcessedRequest>,
115}
116
117/// Handle for observing completion of a batched request addition.
118#[must_use = "batch handles must be awaited to observe completion"]
119pub struct BatchAddHandle {
120    /// Requests added synchronously.
121    pub added: Vec<ProcessedRequest>,
122    completion: Completion,
123}
124
125enum Completion {
126    Ready(AddRequestsBatchedResult),
127    Task(tokio::task::JoinHandle<StorageResult<AddRequestsBatchedResult>>),
128}
129
130impl fmt::Debug for BatchAddHandle {
131    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132        formatter
133            .debug_struct("BatchAddHandle")
134            .field("added", &self.added)
135            .field("completion", &"<completion>")
136            .finish()
137    }
138}
139
140impl BatchAddHandle {
141    /// Creates an already-completed batch handle.
142    pub fn ready(added: Vec<ProcessedRequest>) -> Self {
143        Self {
144            completion: Completion::Ready(AddRequestsBatchedResult {
145                processed: added.clone(),
146            }),
147            added,
148        }
149    }
150
151    /// Creates a batch handle backed by a spawned completion task.
152    pub fn deferred(
153        added: Vec<ProcessedRequest>,
154        task: tokio::task::JoinHandle<StorageResult<AddRequestsBatchedResult>>,
155    ) -> Self {
156        Self {
157            added,
158            completion: Completion::Task(task),
159        }
160    }
161
162    /// Runs `notify` after this batch completes, even if the public handle is never awaited.
163    pub(crate) fn notify_on_completion<F>(self, notify: F) -> Self
164    where
165        F: FnOnce() + Send + 'static,
166    {
167        let Self { added, completion } = self;
168        match completion {
169            Completion::Ready(result) => {
170                notify();
171                Self {
172                    added,
173                    completion: Completion::Ready(result),
174                }
175            }
176            Completion::Task(task) => Self {
177                added,
178                completion: Completion::Task(tokio::spawn(async move {
179                    let result = task.await.map_err(|error| {
180                        StorageError::Backend(anyhow::anyhow!("batch add task failed: {error}"))
181                    });
182                    notify();
183                    result?
184                })),
185            },
186        }
187    }
188
189    /// Waits until all requests in the batch have been processed.
190    pub async fn wait(self) -> StorageResult<AddRequestsBatchedResult> {
191        match self.completion {
192            Completion::Ready(result) => Ok(result),
193            Completion::Task(task) => task.await.map_err(|error| {
194                StorageError::Backend(anyhow::anyhow!("batch add task failed: {error}"))
195            })?,
196        }
197    }
198}
199
200/// Object-safe queue with temporary lease ownership.
201///
202/// Single-process backends may document lease expiry as a no-op, but must retain this complete
203/// lease API so callers and distributed backends share one contract.
204#[async_trait::async_trait]
205pub trait RequestQueue: Send + Sync {
206    /// Adds a request and returns its deduplication status.
207    async fn add(&self, req: Request, opts: AddOptions) -> StorageResult<QueueOpInfo>;
208    /// Adds multiple request sources and returns a handle for deferred completion.
209    async fn add_batch(
210        &self,
211        reqs: Vec<RequestSource>,
212        opts: AddOptions,
213    ) -> StorageResult<BatchAddHandle>;
214    /// Hands temporary ownership of the next request to the caller as a lease.
215    async fn fetch_next(&self) -> StorageResult<Option<Lease>>;
216    /// Marks a leased request as successful and consumes its lease.
217    async fn mark_handled(&self, lease: Lease) -> StorageResult<()>;
218    /// Re-queues a lease, incrementing retry count unless `opts` disables it.
219    ///
220    /// Backends must persist the request state carried in the lease (e.g. mutated `error_messages`
221    /// or `session_rotation_count`), not a previously stored copy.
222    async fn reclaim(&self, lease: Lease, opts: ReclaimOptions) -> StorageResult<()>;
223    /// Extends an active lease deadline, returning `LeaseNotFound` if it is unknown or completed.
224    async fn renew(&self, lease_id: &LeaseId, extend_by: Duration) -> StorageResult<()>;
225    /// Re-queues and consumes a lease without incrementing its request retry count.
226    ///
227    /// Backends must persist the request state carried in the lease (e.g. mutated `error_messages`
228    /// or `session_rotation_count`), not a previously stored copy.
229    async fn abandon(&self, lease: Lease) -> StorageResult<()>;
230    /// Returns whether no requests are currently pending.
231    async fn is_empty(&self) -> StorageResult<bool>;
232    /// Returns whether no requests are pending or leased.
233    async fn is_finished(&self) -> StorageResult<bool>;
234    /// Returns the number of successfully handled requests.
235    async fn handled_count(&self) -> StorageResult<u64>;
236    /// Returns the number of pending requests.
237    async fn pending_count(&self) -> StorageResult<u64>;
238}