mod concurrency;
mod connector;
mod delay;
pub mod event;
mod hosts;
mod meta;
#[cfg(feature = "robots")]
mod robots;
mod run;
mod stats;
#[cfg(test)]
mod tests;
use std::fmt;
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};
use std::time::Duration;
pub use concurrency::HostConcurrency;
pub use delay::HostDelay;
pub use meta::{Attempt, Depth, Revisit, Tag, Tags};
#[cfg(feature = "robots")]
#[cfg_attr(docsrs, doc(cfg(feature = "robots")))]
pub use robots::Robots;
pub use stats::Stats;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use crate::Result;
use crate::backend::Backend;
use crate::crawler::event::{Emitter, Events};
use crate::crawler::hosts::HostPolicy;
use crate::dataset::{Data, Dataset, Datasets};
use crate::frontier::dynamic::DynFrontier;
use crate::frontier::facade::RetryPolicy;
use crate::frontier::queue::Queue;
use crate::frontier::seed::Seed;
use crate::frontier::{Frontier, FrontierExt, InMemFrontier};
use crate::identity::Identities;
use crate::sync::lock;
use crate::worker::Worker;
const DEFAULT_CONCURRENCY: NonZeroUsize = NonZeroUsize::new(8).unwrap();
pub struct Crawler<B, W> {
backend: Arc<B>,
identities: Option<Identities>,
worker: Arc<W>,
frontier: Arc<dyn DynFrontier>,
datasets: Arc<Datasets>,
extensions: http::Extensions,
seeds: Mutex<Vec<Seed>>,
settings: Settings,
shutdown: CancellationToken,
live: Arc<Mutex<Live>>,
events: Emitter,
}
#[derive(Debug, Clone)]
struct Settings {
concurrency: NonZeroUsize,
max_requests: Option<usize>,
time_limit: Option<Duration>,
request_timeout: Option<Duration>,
shutdown_grace: Option<Duration>,
hosts: HostPolicy,
#[cfg(feature = "robots")]
robots: Option<Robots>,
}
#[derive(Debug, Default)]
struct Live {
stats: Stats,
started: Option<Instant>,
running: bool,
}
impl<B, W> Crawler<B, W> {
pub fn new(backend: B, worker: W) -> Self
where
B: Backend,
W: Worker<B::Session>,
{
let frontier = InMemFrontier::fifo().retry_with(RetryPolicy::default());
let settings = Settings {
concurrency: DEFAULT_CONCURRENCY,
max_requests: None,
time_limit: None,
request_timeout: None,
shutdown_grace: None,
hosts: HostPolicy::default(),
#[cfg(feature = "robots")]
robots: None,
};
Self {
backend: Arc::new(backend),
identities: None,
worker: Arc::new(worker),
frontier: Arc::new(frontier),
datasets: Arc::default(),
extensions: http::Extensions::new(),
seeds: Mutex::default(),
settings,
shutdown: CancellationToken::new(),
live: Arc::default(),
events: Emitter::new(event::CAPACITY),
}
}
#[must_use]
pub fn with_extension<T: Clone + Send + Sync + 'static>(mut self, value: T) -> Self {
self.extensions.insert(value);
self
}
#[must_use]
pub fn with_frontier(mut self, frontier: impl Frontier) -> Self {
self.frontier = Arc::new(frontier);
self
}
#[must_use]
pub fn with_dataset<T, D>(mut self, dataset: D) -> Self
where
T: Send + 'static,
D: Dataset<T>,
{
Arc::make_mut(&mut self.datasets).insert(dataset);
self
}
#[must_use]
pub fn with_concurrency(mut self, limit: usize) -> Self {
self.settings.concurrency = NonZeroUsize::new(limit).unwrap_or(NonZeroUsize::MIN);
self
}
#[must_use]
pub const fn with_max_requests(mut self, max: usize) -> Self {
self.settings.max_requests = Some(max);
self
}
#[must_use]
pub const fn with_time_limit(mut self, limit: Duration) -> Self {
self.settings.time_limit = Some(limit);
self
}
#[must_use]
pub const fn with_request_timeout(mut self, timeout: Duration) -> Self {
self.settings.request_timeout = Some(timeout);
self
}
#[must_use]
pub const fn with_shutdown_grace(mut self, grace: Duration) -> Self {
self.settings.shutdown_grace = Some(grace);
self
}
#[must_use]
pub fn with_host_concurrency(mut self, limit: impl Into<HostConcurrency>) -> Self {
self.settings.hosts.concurrency = Some(limit.into());
self
}
#[must_use]
pub fn with_host_delay(mut self, delay: impl Into<HostDelay>) -> Self {
self.settings.hosts.delay = delay.into();
self
}
#[cfg(feature = "robots")]
#[cfg_attr(docsrs, doc(cfg(feature = "robots")))]
#[must_use]
pub fn with_robots(mut self, robots: impl Into<Robots>) -> Self {
self.settings.robots = Some(robots.into());
self
}
#[must_use]
pub fn with_seed(self, seed: Seed) -> Self {
lock(&self.seeds).push(seed);
self
}
#[must_use]
pub fn with_seeds(self, seeds: impl IntoIterator<Item = Seed>) -> Self {
lock(&self.seeds).extend(seeds);
self
}
#[must_use]
pub fn with_identities(mut self, identities: Identities) -> Self {
self.identities = Some(identities);
self
}
#[must_use]
pub fn backend(&self) -> &B {
&self.backend
}
#[must_use]
pub fn queue(&self) -> Queue {
Queue::from_dyn(self.frontier.clone())
}
pub fn dataset<T: Send + 'static>(&self) -> Result<Data<T>> {
self.datasets.get()
}
#[must_use]
pub fn snapshot(&self) -> Option<Vec<Seed>> {
self.frontier.snapshot()
}
#[must_use]
pub fn stats(&self) -> Stats {
let live = lock(&self.live);
let mut stats = live.stats;
if live.running
&& let Some(started) = live.started
{
stats.elapsed = started.elapsed();
}
stats
}
pub fn events(&self) -> Events {
self.events.subscribe()
}
#[must_use]
pub fn shutdown_token(&self) -> CancellationToken {
self.shutdown.clone()
}
fn tally(&self, update: impl FnOnce(&mut Stats)) {
update(&mut lock(&self.live).stats);
}
}
impl<B, W> fmt::Debug for Crawler<B, W> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Crawler")
.field("settings", &self.settings)
.field("identities", &self.identities)
.field("queued", &self.frontier.size_hint())
.field("datasets", &self.datasets)
.finish_non_exhaustive()
}
}