Skip to main content

spire_core/
context.rs

1//! The [`Context`] a worker handles.
2
3use std::fmt;
4use std::sync::{Arc, Mutex};
5use std::time::Duration;
6
7use bytes::Bytes;
8use tokio::sync::OnceCell;
9use tokio::time::Instant;
10
11use crate::Result;
12use crate::backend::Client;
13use crate::dataset::{Data, Dataset, Datasets};
14use crate::frontier::memory::InMemFrontier;
15use crate::frontier::queue::Queue;
16use crate::request::{Request, Response, ResponseExt};
17use crate::sync::lock;
18
19/// A request being crawled, with everything needed to handle it.
20///
21/// It holds the [`Request`], a client to fetch it with, a [`Queue`] to add
22/// the links it leads to, and the crawl's datasets. Handlers borrow it, so
23/// several of them can handle one request: the request is fetched once, by
24/// the first [`response`](Self::response) or [`body`](Self::body), and the
25/// response's head is kept. The body is always read from the client, so a
26/// browser page reads as currently rendered.
27pub struct Context<C> {
28    request: Request,
29    client: C,
30    queue: Queue,
31    datasets: Arc<Datasets>,
32    response: OnceCell<Response<()>>,
33    /// Where to record how long the fetch took, for adaptive host delays.
34    fetch_time: Option<FetchTime>,
35}
36
37/// How long a request's fetch took, shared with the crawler.
38pub(crate) type FetchTime = Arc<Mutex<Option<Duration>>>;
39
40impl<C> Context<C> {
41    /// Creates a standalone context, e.g. to test a handler.
42    ///
43    /// It gets an empty queue of its own and no datasets; see
44    /// [`with_queue`](Self::with_queue) and
45    /// [`with_dataset`](Self::with_dataset).
46    #[must_use]
47    pub fn new(request: Request, client: C) -> Self {
48        let queue = Queue::new(InMemFrontier::fifo()).child_of(&request);
49        Self::from_parts(request, client, queue, Arc::default())
50    }
51
52    pub(crate) fn from_parts(
53        request: Request,
54        client: C,
55        queue: Queue,
56        datasets: Arc<Datasets>,
57    ) -> Self {
58        Self {
59            request,
60            client,
61            queue,
62            datasets,
63            response: OnceCell::new(),
64            fetch_time: None,
65        }
66    }
67
68    /// Records how long the fetch takes in `fetch_time`.
69    pub(crate) fn with_fetch_time(mut self, fetch_time: FetchTime) -> Self {
70        self.fetch_time = Some(fetch_time);
71        self
72    }
73
74    /// Replaces the queue, binding it to the request.
75    #[must_use]
76    pub fn with_queue(mut self, queue: &Queue) -> Self {
77        self.queue = queue.child_of(&self.request);
78        self
79    }
80
81    /// Registers `dataset` for items of type `T`.
82    #[must_use]
83    pub fn with_dataset<T, D>(mut self, dataset: D) -> Self
84    where
85        T: Send + 'static,
86        D: Dataset<T>,
87    {
88        Arc::make_mut(&mut self.datasets).insert(dataset);
89        self
90    }
91
92    /// Returns the request.
93    #[must_use]
94    pub const fn request(&self) -> &Request {
95        &self.request
96    }
97
98    /// Returns the request mutably, e.g. to add headers before fetching.
99    pub const fn request_mut(&mut self) -> &mut Request {
100        &mut self.request
101    }
102
103    /// Returns the client.
104    #[must_use]
105    pub const fn client(&self) -> &C {
106        &self.client
107    }
108
109    /// Returns the queue for the links this request leads to.
110    #[must_use]
111    pub const fn queue(&self) -> &Queue {
112        &self.queue
113    }
114
115    /// Returns the dataset for items of type `T`.
116    ///
117    /// # Errors
118    ///
119    /// Returns a [`Dataset`](crate::ErrorKind::Dataset) error if no dataset
120    /// is registered for `T`.
121    pub fn dataset<T: Send + 'static>(&self) -> Result<Data<T>> {
122        self.datasets.get()
123    }
124
125    /// Returns the response's head, fetching the request on the first call.
126    ///
127    /// Later calls return the same head without fetching again. A failed
128    /// fetch is not kept: the next call tries again. Once fetched, links
129    /// followed through the queue, including clones taken earlier, resolve
130    /// against the URI the response was served from.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if the client fails to fetch the request.
135    pub async fn response(&self) -> Result<&Response<()>>
136    where
137        C: Client,
138    {
139        self.response
140            .get_or_try_init(|| async {
141                let started = Instant::now();
142                let response = self.client.fetch(&self.request).await;
143                if let Some(fetch_time) = &self.fetch_time {
144                    *lock(fetch_time) = Some(started.elapsed());
145                }
146                let response = response?;
147                if let Some(uri) = response.final_uri() {
148                    self.queue.rebase(uri.clone());
149                }
150                Ok(response)
151            })
152            .await
153    }
154
155    /// Returns the response's body, fetching the request first if needed:
156    /// the bytes received, or a browser page as currently rendered.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if the request cannot be fetched or its body read.
161    pub async fn body(&self) -> Result<Bytes>
162    where
163        C: Client,
164    {
165        self.response().await?;
166        self.client.body().await
167    }
168}
169
170impl<C> fmt::Debug for Context<C> {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        f.debug_struct("Context")
173            .field("method", self.request.method())
174            .field("uri", self.request.uri())
175            .field("queue", &self.queue)
176            .field("fetched", &self.response.initialized())
177            .finish_non_exhaustive()
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use std::future::Future;
184    use std::sync::atomic::{AtomicUsize, Ordering};
185
186    use super::*;
187    use crate::backend::Noop;
188    use crate::dataset::InMemDataset;
189    use crate::frontier::seed::Seed;
190    use crate::request::{Depth, RequestExt};
191
192    #[tokio::test]
193    async fn standalone_context() {
194        let request = Seed::get("https://example.com/").unwrap().into_request();
195        let cx = Context::new(request, Noop).with_dataset(InMemDataset::<u32>::new());
196
197        cx.dataset::<u32>().unwrap().write(1).await.unwrap();
198        assert!(cx.dataset::<String>().is_err());
199
200        cx.queue().follow("https://example.com/next").await.unwrap();
201        assert_eq!(cx.queue().size_hint(), Some(1));
202        assert_eq!(cx.request().depth(), Depth::ROOT);
203
204        let response = cx.response().await.unwrap();
205        assert!(response.status().is_success());
206    }
207
208    /// Counts its fetches, failing the first `fail` of them, and serves a
209    /// body that changes after the fetch, as a browser page can.
210    struct Counting {
211        fetches: AtomicUsize,
212        fail: usize,
213        body: std::sync::Mutex<&'static str>,
214    }
215
216    impl Client for Counting {
217        fn fetch(&self, _request: &Request) -> impl Future<Output = Result<Response<()>>> + Send {
218            let fetch = self.fetches.fetch_add(1, Ordering::SeqCst);
219            std::future::ready(if fetch < self.fail {
220                Err(crate::Error::new(crate::ErrorKind::Http))
221            } else {
222                Ok(Response::new(()))
223            })
224        }
225
226        fn body(&self) -> impl Future<Output = Result<Bytes>> + Send {
227            let body = *self.body.lock().unwrap();
228            std::future::ready(Ok(Bytes::from_static(body.as_bytes())))
229        }
230    }
231
232    #[tokio::test]
233    async fn fetches_once_and_reads_the_current_body() {
234        let request = Seed::get("https://example.com/").unwrap().into_request();
235        let client = Counting {
236            fetches: AtomicUsize::new(0),
237            fail: 1,
238            body: std::sync::Mutex::new("loaded"),
239        };
240        let cx = Context::new(request, client);
241
242        // A failed fetch is tried again; a successful one is kept.
243        assert!(cx.response().await.is_err());
244        assert_eq!(cx.body().await.unwrap(), "loaded");
245        *cx.client().body.lock().unwrap() = "clicked";
246        assert_eq!(cx.body().await.unwrap(), "clicked");
247        assert!(cx.response().await.unwrap().status().is_success());
248        assert_eq!(cx.client().fetches.load(Ordering::SeqCst), 2);
249    }
250}