1use 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::backend::Session;
14use crate::crawler::event::Notify;
15use crate::dataset::{Data, Dataset, Datasets};
16use crate::frontier::InMemFrontier;
17use crate::frontier::queue::Queue;
18use crate::http::{Request, Response, ResponseExt};
19use crate::{Error, Result};
20
21pub struct Context<C> {
35 request: Request,
36 session: C,
37 queue: Queue,
38 datasets: Arc<Datasets>,
39 response: OnceCell<Response>,
40}
41
42#[derive(Debug, Default, Clone, Copy)]
50pub(crate) struct Fetch {
51 pub(crate) latency: Option<Duration>,
52 pub(crate) status: Option<StatusCode>,
54}
55
56#[derive(Default)]
58pub(crate) struct FetchScope {
59 pub(crate) fetch: Cell<Fetch>,
60 pub(crate) notify: Option<Notify>,
62}
63
64impl FetchScope {
65 fn record(&self, fetch: Fetch) {
66 self.fetch.set(fetch);
67 if let Some(notify) = &self.notify {
68 notify.fetched(fetch);
69 }
70 }
71}
72
73tokio::task_local! {
74 pub(crate) static FETCH: FetchScope;
76}
77
78impl<C> Context<C> {
79 #[must_use]
85 pub fn new(request: Request, session: C) -> Self {
86 let queue = Queue::new(InMemFrontier::fifo()).child_of(&request);
87 Self::from_parts(request, session, queue, Arc::default())
88 }
89
90 pub(crate) fn from_parts(
91 request: Request,
92 session: C,
93 queue: Queue,
94 datasets: Arc<Datasets>,
95 ) -> Self {
96 Self {
97 request,
98 session,
99 queue,
100 datasets,
101 response: OnceCell::new(),
102 }
103 }
104
105 #[must_use]
107 pub fn with_queue(mut self, queue: &Queue) -> Self {
108 self.queue = queue.child_of(&self.request);
109 self
110 }
111
112 #[must_use]
114 pub fn with_dataset<T, D>(mut self, dataset: D) -> Self
115 where
116 T: Send + 'static,
117 D: Dataset<T>,
118 {
119 Arc::make_mut(&mut self.datasets).insert(dataset);
120 self
121 }
122
123 #[must_use]
125 pub fn with_extension<T: Clone + Send + Sync + 'static>(mut self, value: T) -> Self {
126 self.request.extensions_mut().insert(value);
127 self
128 }
129
130 #[must_use]
132 pub const fn request(&self) -> &Request {
133 &self.request
134 }
135
136 pub const fn request_mut(&mut self) -> &mut Request {
138 &mut self.request
139 }
140
141 #[must_use]
143 pub const fn session(&self) -> &C {
144 &self.session
145 }
146
147 #[must_use]
149 pub const fn queue(&self) -> &Queue {
150 &self.queue
151 }
152
153 pub fn dataset<T: Send + 'static>(&self) -> Result<Data<T>> {
160 self.datasets.get()
161 }
162
163 pub async fn response(&self) -> Result<&Response>
174 where
175 C: Session,
176 {
177 self.response
178 .get_or_try_init(|| async {
179 let started = Instant::now();
180 let response = self.session.fetch(&self.request).await;
181 let fetch = Fetch {
184 latency: Some(started.elapsed()),
185 status: response
186 .as_ref()
187 .map_or_else(Error::status, |response| Some(response.status())),
188 };
189 let _ = FETCH.try_with(|scope| scope.record(fetch));
190 let response = response?;
191 if let Some(uri) = response.final_uri() {
192 self.queue.rebase(uri.clone());
193 }
194 Ok(response)
195 })
196 .await
197 }
198
199 pub async fn body(&self) -> Result<Bytes>
207 where
208 C: Session,
209 {
210 Ok(self.response().await?.body().clone())
211 }
212}
213
214impl<C> fmt::Debug for Context<C> {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 f.debug_struct("Context")
217 .field("method", self.request.method())
218 .field("uri", self.request.uri())
219 .field("queue", &self.queue)
220 .field("fetched", &self.response.initialized())
221 .finish_non_exhaustive()
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use std::future::Future;
228 use std::sync::atomic::{AtomicUsize, Ordering};
229
230 use super::*;
231 use crate::backend::Noop;
232 use crate::crawler::Depth;
233 use crate::dataset::InMemDataset;
234 use crate::frontier::seed::Seed;
235 use crate::http::RequestExt;
236
237 #[tokio::test]
238 async fn standalone_context() {
239 let request = Seed::get("https://example.com/").unwrap().into_request();
240 let cx = Context::new(request, Noop).with_dataset(InMemDataset::<u32>::new());
241
242 cx.dataset::<u32>().unwrap().write(1).await.unwrap();
243 assert!(cx.dataset::<String>().is_err());
244
245 cx.queue().follow("https://example.com/next").await.unwrap();
246 assert_eq!(cx.queue().size_hint(), Some(1));
247 assert_eq!(cx.request().depth(), Depth::ROOT);
248
249 let response = cx.response().await.unwrap();
250 assert!(response.status().is_success());
251 }
252
253 struct Counting {
256 fetches: AtomicUsize,
257 fail: usize,
258 body: std::sync::Mutex<&'static str>,
259 }
260
261 impl Session for Counting {
262 fn fetch(&self, _request: &Request) -> impl Future<Output = Result<Response>> + Send {
263 let fetch = self.fetches.fetch_add(1, Ordering::SeqCst);
264 std::future::ready(if fetch < self.fail {
265 Err(crate::Error::new(crate::ErrorKind::Http))
266 } else {
267 Ok(Response::new(Bytes::from_static(
268 self.body.lock().unwrap().as_bytes(),
269 )))
270 })
271 }
272 }
273
274 #[tokio::test]
275 async fn fetches_once_and_keeps_a_snapshot_of_the_body() {
276 let request = Seed::get("https://example.com/").unwrap().into_request();
277 let client = Counting {
278 fetches: AtomicUsize::new(0),
279 fail: 1,
280 body: std::sync::Mutex::new("loaded"),
281 };
282 let cx = Context::new(request, client);
283
284 assert!(cx.response().await.is_err());
286 assert_eq!(cx.body().await.unwrap(), "loaded");
287 *cx.session().body.lock().unwrap() = "clicked";
288 assert_eq!(cx.body().await.unwrap(), "loaded");
289 assert_eq!(cx.response().await.unwrap().body(), "loaded");
290 assert!(cx.response().await.unwrap().status().is_success());
291 assert_eq!(cx.session().fetches.load(Ordering::SeqCst), 2);
292 }
293}