1mod basic;
4mod builder;
5mod engine;
6mod start;
7
8pub use basic::{BasicContext, BasicKind};
9pub use builder::{CrawlerBuildError, CrawlerBuilder};
10pub use start::{IntoStartRequest, IntoStartRequests};
11
12use crate::{
13 autoscale::AutoscaledPool,
14 config::Configuration,
15 errors::CrawlError,
16 events::{EventBus, EventStream, HandledRequest, ResultStream},
17 handler::{FailedRequestHandler, RequestHandler},
18 link_extraction::CrawlPolicy,
19 request::Request,
20 statistics::{FinalStatistics, StatisticsHandle, StatisticsSnapshot},
21 storage::{AddOptions, BatchAddHandle, RequestQueue, RequestSource},
22};
23use futures_util::future::BoxFuture;
24use std::{
25 fmt,
26 sync::{
27 Arc, Weak,
28 atomic::{AtomicBool, AtomicU64, Ordering},
29 },
30 time::Duration,
31};
32
33use engine::{Engine, EngineOptions};
34
35pub struct Crawler<K: CrawlerKind> {
37 kind: Arc<K>,
38 shared: Arc<CrawlerShared>,
39 config: Arc<Configuration>,
40 handler: Arc<dyn RequestHandler<K::Context>>,
41 failed_handler: Option<Arc<dyn FailedRequestHandler>>,
42 kvs: Option<Arc<dyn crate::storage::KeyValueStore>>,
43 storage: Option<Arc<dyn crate::storage::StorageClient>>,
44 opts: EngineOptions,
45 started: AtomicBool,
46}
47
48pub type BasicCrawler = Crawler<BasicKind>;
50
51impl<K: CrawlerKind> Crawler<K> {
52 pub fn builder(kind: K) -> CrawlerBuilder<K> {
54 CrawlerBuilder::new(kind)
55 }
56
57 pub async fn run(&self, start: impl IntoStartRequests) -> Result<FinalStatistics, CrawlError> {
61 if self.started.swap(true, Ordering::SeqCst) {
62 return Err(CrawlError::non_retryable(anyhow::anyhow!(
63 "this crawler has already been run"
64 )));
65 }
66 let start_requests = start.into_start_requests()?;
67 let env = CrawlerEnv {
68 shared: self.shared.clone(),
69 config: self.config.clone(),
70 storage: self.storage.clone(),
71 kvs: self.kvs.clone(),
72 };
73 self.kind.start(&env).await?;
74 let result = async {
75 let sources = start_requests
76 .into_iter()
77 .map(RequestSource::from)
78 .collect();
79 let batch = tokio::time::timeout(
80 self.opts.internal_operation_timeout,
81 self.shared.queue.add_batch(sources, AddOptions::default()),
82 )
83 .await
84 .map_err(|_| CrawlError::retry(anyhow::anyhow!("queue add timed out")))??;
85 let _ = batch.wait().await?;
86 self.shared.notify.notify_waiters();
87 Engine {
88 kind: self.kind.clone(),
89 handler: self.handler.clone(),
90 failed_handler: self.failed_handler.clone(),
91 shared: self.shared.clone(),
92 kvs: self.kvs.clone(),
93 opts: self.opts.clone(),
94 }
95 .run()
96 .await
97 }
98 .await;
99 if let Err(error) = self.kind.stop(&env).await {
100 tracing::warn!(%error, "crawler kind stop failed");
101 }
102 result
103 }
104
105 pub fn handle(&self) -> CrawlerHandle {
107 CrawlerHandle::new(Arc::downgrade(&self.shared))
108 }
109 pub async fn add_requests(
111 &self,
112 reqs: impl IntoIterator<Item = Request> + Send,
113 ) -> Result<(), CrawlError> {
114 let _ = self.handle().add_requests(reqs).await?.wait().await?;
115 Ok(())
116 }
117 pub fn results(&self) -> ResultStream {
119 self.shared.results_tx.subscribe()
120 }
121 pub fn events(&self) -> EventStream {
123 self.shared.events.subscribe()
124 }
125 pub fn stats(&self) -> StatisticsHandle {
127 self.shared.stats.clone()
128 }
129 pub fn autoscaler_snapshot(&self) -> AutoscalerSnapshot {
131 AutoscalerSnapshot::from_pool(&self.shared.pool)
132 }
133 pub fn stop(&self) {
135 self.handle().stop();
136 }
137 pub fn abort(&self) {
139 self.handle().abort();
140 }
141}
142
143pub(crate) struct CrawlerShared {
144 pub(crate) queue: Arc<dyn RequestQueue>,
145 pub(crate) stats: StatisticsHandle,
146 pub(crate) events: EventBus,
147 pub(crate) results_tx: tokio::sync::broadcast::Sender<HandledRequest>,
148 pub(crate) drain: tokio_util::sync::CancellationToken,
149 pub(crate) cancel: tokio_util::sync::CancellationToken,
150 pub(crate) notify: tokio::sync::Notify,
151 pub(crate) internal_operation_timeout: Duration,
152 pub(crate) pool: Arc<AutoscaledPool>,
153 enqueue_admission: Arc<tokio::sync::Mutex<()>>,
154 enqueue_admissions: Arc<AtomicU64>,
155 crawl_policy: Option<Arc<CrawlPolicy>>,
156}
157
158impl CrawlerShared {
159 #[allow(dead_code)]
164 pub(crate) fn new(
165 queue: Arc<dyn RequestQueue>,
166 events: EventBus,
167 results_capacity: usize,
168 internal_operation_timeout: Duration,
169 pool: Arc<AutoscaledPool>,
170 ) -> Self {
171 debug_assert!(results_capacity >= 1);
172 let (results_tx, _) = tokio::sync::broadcast::channel(results_capacity);
173 Self {
174 queue,
175 stats: StatisticsHandle::new(),
176 events,
177 results_tx,
178 drain: tokio_util::sync::CancellationToken::new(),
179 cancel: tokio_util::sync::CancellationToken::new(),
180 notify: tokio::sync::Notify::new(),
181 internal_operation_timeout,
182 pool,
183 enqueue_admission: Arc::new(tokio::sync::Mutex::new(())),
184 enqueue_admissions: Arc::new(AtomicU64::new(0)),
185 crawl_policy: None,
186 }
187 }
188
189 pub(crate) fn new_with_policy(
190 queue: Arc<dyn RequestQueue>,
191 events: EventBus,
192 results_capacity: usize,
193 internal_operation_timeout: Duration,
194 pool: Arc<AutoscaledPool>,
195 crawl_policy: Option<Arc<CrawlPolicy>>,
196 ) -> Self {
197 let mut shared = Self::new(
198 queue,
199 events,
200 results_capacity,
201 internal_operation_timeout,
202 pool,
203 );
204 shared.crawl_policy = crawl_policy;
205 shared
206 }
207
208 pub fn request_queue(&self) -> &Arc<dyn RequestQueue> {
210 &self.queue
211 }
212
213 pub fn crawl_policy(&self) -> Option<&Arc<CrawlPolicy>> {
215 self.crawl_policy.as_ref()
216 }
217}
218
219#[non_exhaustive]
221#[derive(Debug, Clone, Copy)]
222pub struct AutoscalerSnapshot {
223 pub desired_concurrency: usize,
225 pub min_concurrency: usize,
227 pub max_concurrency: usize,
229 pub is_fixed: bool,
231}
232
233impl AutoscalerSnapshot {
234 fn from_pool(pool: &AutoscaledPool) -> Self {
235 Self {
236 desired_concurrency: pool.desired_concurrency(),
237 min_concurrency: pool.min_concurrency(),
238 max_concurrency: pool.max_concurrency(),
239 is_fixed: pool.is_fixed(),
240 }
241 }
242}
243
244#[derive(Clone)]
246pub struct CrawlerHandle {
247 inner: Weak<CrawlerShared>,
248}
249
250pub(crate) struct EnqueueAdmissionReservation {
251 admissions: Arc<AtomicU64>,
252 committed: bool,
253}
254
255impl EnqueueAdmissionReservation {
256 pub(crate) fn commit(mut self) {
257 self.committed = true;
258 }
259}
260
261impl Drop for EnqueueAdmissionReservation {
262 fn drop(&mut self) {
263 if !self.committed {
264 self.admissions.fetch_sub(1, Ordering::SeqCst);
265 }
266 }
267}
268
269impl fmt::Debug for CrawlerHandle {
270 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
271 formatter
272 .debug_struct("CrawlerHandle")
273 .field("alive", &(self.inner.strong_count() > 0))
274 .finish()
275 }
276}
277
278impl CrawlerHandle {
279 pub(crate) fn new(inner: Weak<CrawlerShared>) -> Self {
280 Self { inner }
281 }
282
283 pub async fn add_requests(
285 &self,
286 reqs: impl IntoIterator<Item = Request> + Send,
287 ) -> Result<BatchAddHandle, CrawlError> {
288 self.add_requests_with_options(reqs, AddOptions::default())
289 .await
290 }
291
292 pub async fn add_requests_with_options(
294 &self,
295 reqs: impl IntoIterator<Item = Request> + Send,
296 options: AddOptions,
297 ) -> Result<BatchAddHandle, CrawlError> {
298 let shared = self.inner.upgrade().ok_or_else(|| {
299 CrawlError::non_retryable(anyhow::anyhow!("crawler is no longer running"))
300 })?;
301 let sources = reqs.into_iter().map(RequestSource::from).collect();
302 let handle = tokio::time::timeout(
303 shared.internal_operation_timeout,
304 shared.queue.add_batch(sources, options),
305 )
306 .await
307 .map_err(|_| CrawlError::retry(anyhow::anyhow!("queue add timed out")))??;
308 let handle = handle.notify_on_completion({
309 let shared = shared.clone();
310 move || shared.notify.notify_waiters()
311 });
312 Ok(handle)
313 }
314
315 pub(crate) async fn lock_enqueue_admission(
316 &self,
317 ) -> Result<tokio::sync::OwnedMutexGuard<()>, CrawlError> {
318 let shared = self.inner.upgrade().ok_or_else(|| {
319 CrawlError::non_retryable(anyhow::anyhow!("crawler is no longer running"))
320 })?;
321 Ok(shared.enqueue_admission.clone().lock_owned().await)
322 }
323
324 pub(crate) fn synchronize_enqueue_admissions(
325 &self,
326 observed_queue_count: u64,
327 ) -> Result<u64, CrawlError> {
328 let shared = self.inner.upgrade().ok_or_else(|| {
329 CrawlError::non_retryable(anyhow::anyhow!("crawler is no longer running"))
330 })?;
331 let previous = shared
332 .enqueue_admissions
333 .fetch_max(observed_queue_count, Ordering::SeqCst);
334 Ok(previous.max(observed_queue_count))
335 }
336
337 pub(crate) fn reserve_enqueue_admission(
338 &self,
339 ) -> Result<EnqueueAdmissionReservation, CrawlError> {
340 let shared = self.inner.upgrade().ok_or_else(|| {
341 CrawlError::non_retryable(anyhow::anyhow!("crawler is no longer running"))
342 })?;
343 shared.enqueue_admissions.fetch_add(1, Ordering::SeqCst);
344 Ok(EnqueueAdmissionReservation {
345 admissions: shared.enqueue_admissions.clone(),
346 committed: false,
347 })
348 }
349
350 pub fn stats(&self) -> Option<StatisticsSnapshot> {
352 self.inner.upgrade().map(|shared| shared.stats.snapshot())
353 }
354
355 pub fn autoscaler_snapshot(&self) -> Option<AutoscalerSnapshot> {
357 self.inner
358 .upgrade()
359 .map(|shared| AutoscalerSnapshot::from_pool(&shared.pool))
360 }
361
362 pub fn events(&self) -> Option<EventStream> {
364 self.inner.upgrade().map(|shared| shared.events.subscribe())
365 }
366
367 pub fn results(&self) -> Option<crate::events::ResultStream> {
369 self.inner
370 .upgrade()
371 .map(|shared| shared.results_tx.subscribe())
372 }
373
374 pub fn request_queue(&self) -> Option<Arc<dyn RequestQueue>> {
376 self.inner
377 .upgrade()
378 .map(|shared| shared.request_queue().clone())
379 }
380
381 pub fn crawl_policy(&self) -> Option<Arc<CrawlPolicy>> {
383 self.inner
384 .upgrade()
385 .and_then(|shared| shared.crawl_policy().cloned())
386 }
387
388 pub fn stop(&self) {
390 if let Some(shared) = self.inner.upgrade() {
391 shared.drain.cancel();
392 shared.notify.notify_waiters();
393 }
394 }
395
396 pub fn abort(&self) {
398 if let Some(shared) = self.inner.upgrade() {
399 shared.cancel.cancel();
400 shared.notify.notify_waiters();
401 }
402 }
403}
404
405pub struct CrawlerEnv {
407 pub(crate) shared: Arc<CrawlerShared>,
408 pub(crate) config: Arc<Configuration>,
409 pub(crate) storage: Option<Arc<dyn crate::storage::StorageClient>>,
410 pub(crate) kvs: Option<Arc<dyn crate::storage::KeyValueStore>>,
411}
412
413impl CrawlerEnv {
414 pub fn events(&self) -> &EventBus {
416 &self.shared.events
417 }
418
419 pub fn stats(&self) -> &StatisticsHandle {
421 &self.shared.stats
422 }
423
424 pub fn config(&self) -> &Configuration {
426 &self.config
427 }
428
429 pub fn storage_client(&self) -> Option<&Arc<dyn crate::storage::StorageClient>> {
431 self.storage.as_ref()
432 }
433
434 pub fn kvs(&self) -> Option<&Arc<dyn crate::storage::KeyValueStore>> {
436 self.kvs.as_ref()
437 }
438
439 pub fn request_queue(&self) -> &Arc<dyn RequestQueue> {
441 &self.shared.queue
442 }
443
444 pub fn handle(&self) -> CrawlerHandle {
446 CrawlerHandle::new(Arc::downgrade(&self.shared))
447 }
448}
449
450#[non_exhaustive]
452pub struct RequestPrep {
453 pub request: Request,
455}
456
457#[non_exhaustive]
459pub struct RequestEnv<'a> {
460 pub request: Arc<Request>,
462 pub crawler: CrawlerHandle,
464 pub events: &'a EventBus,
466 pub overrides: crate::retry_strategy::AttemptOverrides,
468}
469
470impl<'a> RequestEnv<'a> {
471 pub fn duplicate(&self) -> RequestEnv<'a> {
475 RequestEnv {
476 request: Arc::clone(&self.request),
477 crawler: self.crawler.clone(),
478 events: self.events,
479 overrides: self.overrides.clone(),
480 }
481 }
482}
483
484#[derive(Debug, Clone, Default)]
497#[non_exhaustive]
498pub struct AttemptObservation {
499 pub status: Option<http::StatusCode>,
501 pub loaded_url: Option<url::Url>,
503 pub session_id: Option<crate::session::SessionId>,
505 pub proxy_info: Option<crate::proxy::ProxyInfo>,
507 pub response_bytes: Option<usize>,
509}
510
511pub enum RequestOutcome<C> {
513 Handled(C),
515 HandlerFailed {
517 ctx: C,
519 error: Arc<CrawlError>,
521 },
522 ExecuteFailed {
524 request: Arc<Request>,
526 error: Arc<CrawlError>,
528 },
529}
530
531pub trait CrawlerKind: Send + Sync + 'static {
533 type Context: Send + Clone + 'static;
541
542 fn start<'a>(&'a self, env: &'a CrawlerEnv) -> BoxFuture<'a, Result<(), CrawlError>> {
544 let _ = env;
545 Box::pin(async { Ok(()) })
546 }
547
548 fn before_request<'a>(
550 &'a self,
551 prep: &'a mut RequestPrep,
552 ) -> BoxFuture<'a, Result<(), CrawlError>> {
553 let _ = prep;
554 Box::pin(async { Ok(()) })
555 }
556
557 fn execute<'a>(
559 &'a self,
560 env: RequestEnv<'a>,
561 ) -> BoxFuture<'a, Result<Self::Context, CrawlError>>;
562
563 fn observe(&self, ctx: &Self::Context) -> AttemptObservation {
566 let _ = ctx;
567 AttemptObservation::default()
568 }
569
570 fn after_success<'a>(
572 &'a self,
573 ctx: &'a mut Self::Context,
574 ) -> BoxFuture<'a, Result<(), CrawlError>> {
575 let _ = ctx;
576 Box::pin(async { Ok(()) })
577 }
578
579 fn cleanup(
581 &self,
582 outcome: RequestOutcome<Self::Context>,
583 ) -> BoxFuture<'_, Result<(), CrawlError>>;
584
585 fn stop<'a>(&'a self, env: &'a CrawlerEnv) -> BoxFuture<'a, Result<(), CrawlError>> {
587 let _ = env;
588 Box::pin(async { Ok(()) })
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use crate::storage::{Lease, LeaseId, ProcessedRequest, ReclaimOptions, StorageResult};
596 use std::sync::Mutex;
597
598 #[derive(Default)]
599 struct TestQueue(Mutex<Vec<Request>>);
600
601 #[async_trait::async_trait]
602 impl RequestQueue for TestQueue {
603 async fn add(&self, request: Request, _: AddOptions) -> StorageResult<ProcessedRequest> {
604 let mut requests = self.0.lock().unwrap();
605 let duplicate = requests
606 .iter()
607 .any(|known| known.unique_key == request.unique_key);
608 let info = ProcessedRequest {
609 request_id: request.id.clone(),
610 unique_key: request.unique_key.clone(),
611 was_already_present: duplicate,
612 was_already_handled: false,
613 };
614 if !duplicate {
615 requests.push(request);
616 }
617 Ok(info)
618 }
619
620 async fn add_batch(
621 &self,
622 requests: Vec<RequestSource>,
623 options: AddOptions,
624 ) -> StorageResult<BatchAddHandle> {
625 let mut added = Vec::with_capacity(requests.len());
626 for source in requests {
627 let RequestSource::Request(request) = source;
628 added.push(self.add(request, options.clone()).await?);
629 }
630 Ok(BatchAddHandle::ready(added))
631 }
632
633 async fn fetch_next(&self) -> StorageResult<Option<Lease>> {
634 Ok(None)
635 }
636 async fn mark_handled(&self, _: Lease) -> StorageResult<()> {
637 Ok(())
638 }
639 async fn reclaim(&self, _: Lease, _: ReclaimOptions) -> StorageResult<()> {
640 Ok(())
641 }
642 async fn renew(&self, _: &LeaseId, _: Duration) -> StorageResult<()> {
643 Ok(())
644 }
645 async fn abandon(&self, _: Lease) -> StorageResult<()> {
646 Ok(())
647 }
648 async fn is_empty(&self) -> StorageResult<bool> {
649 Ok(self.0.lock().unwrap().is_empty())
650 }
651 async fn is_finished(&self) -> StorageResult<bool> {
652 self.is_empty().await
653 }
654 async fn handled_count(&self) -> StorageResult<u64> {
655 Ok(0)
656 }
657 async fn pending_count(&self) -> StorageResult<u64> {
658 Ok(self.0.lock().unwrap().len() as u64)
659 }
660 }
661
662 pub(super) fn shared() -> Arc<CrawlerShared> {
663 let queue = Arc::new(TestQueue::default());
664 Arc::new(CrawlerShared::new(
665 queue,
666 EventBus::default(),
667 8,
668 Duration::from_secs(1),
669 Arc::new(AutoscaledPool::new(
670 crate::autoscale::AutoscaledPoolOptions {
671 fixed_concurrency: Some(8),
672 ..Default::default()
673 },
674 )),
675 ))
676 }
677
678 #[tokio::test]
679 async fn crawler_handle_adds_deduplicated_requests_and_observes_liveness() {
680 let shared = shared();
681 let queue = shared.queue.clone();
682 let handle = CrawlerHandle::new(Arc::downgrade(&shared));
683 let request = Request::get("https://example.com/item").build().unwrap();
684 let batch = handle
685 .add_requests([request.clone(), request])
686 .await
687 .unwrap();
688 assert_eq!(batch.added.len(), 2);
689 assert!(!batch.added[0].was_already_present);
690 assert!(batch.added[1].was_already_present);
691 assert_eq!(batch.wait().await.unwrap().processed.len(), 2);
692 assert_eq!(queue.pending_count().await.unwrap(), 1);
693 assert!(handle.stats().is_some());
694 let autoscaler = handle.autoscaler_snapshot().unwrap();
695 assert_eq!(autoscaler.desired_concurrency, 8);
696 assert!(autoscaler.is_fixed);
697 assert!(handle.events().is_some());
698 assert!(handle.results().is_some());
699 assert_eq!(format!("{handle:?}"), "CrawlerHandle { alive: true }");
700
701 drop(shared);
702 assert!(handle.add_requests(Vec::new()).await.is_err());
703 assert!(handle.stats().is_none());
704 assert!(handle.autoscaler_snapshot().is_none());
705 assert_eq!(format!("{handle:?}"), "CrawlerHandle { alive: false }");
706 }
707
708 #[tokio::test]
709 async fn crawler_handle_stop_and_abort_cancel_their_tokens() {
710 let shared = shared();
711 let handle = CrawlerHandle::new(Arc::downgrade(&shared));
712 handle.stop();
713 assert!(shared.drain.is_cancelled());
714 handle.abort();
715 assert!(shared.cancel.is_cancelled());
716 }
717}