Skip to main content

spire_core/
context.rs

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