use std::fmt;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
use std::time::Duration;
use ::http::{Method, StatusCode, Uri};
use futures::Stream;
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use crate::context::Fetch;
use crate::crawler::{Attempt, Depth, Stats, Tag};
use crate::frontier::seed::Seed;
use crate::worker::Outcome;
use crate::{Error, ErrorKind};
#[cfg(test)]
mod tests;
pub(crate) const CAPACITY: usize = 1024;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Event {
Started,
RequestStarted {
request: RequestInfo,
},
Fetched {
request: RequestInfo,
status: Option<StatusCode>,
latency: Duration,
},
Handled {
request: RequestInfo,
outcome: OutcomeKind,
error: Option<Failure>,
elapsed: Duration,
},
Retried {
request: RequestInfo,
after: Option<Duration>,
},
Failed {
request: RequestInfo,
},
Skipped {
request: RequestInfo,
reason: SkipReason,
},
Cancelled {
request: RequestInfo,
},
HostSlowed {
host: String,
slowdown: Slowdown,
},
HostPaused {
host: String,
duration: Duration,
},
RobotsFetched {
origin: String,
error: Option<Failure>,
},
Stopping {
reason: StopReason,
},
Finished {
stats: Stats,
},
Lagged {
missed: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RequestInfo {
pub method: Method,
pub uri: Uri,
pub tag: Option<Tag>,
pub depth: Depth,
pub attempt: Attempt,
}
impl From<&Seed> for RequestInfo {
fn from(seed: &Seed) -> Self {
Self {
method: seed.method().clone(),
uri: seed.uri().clone(),
tag: seed.tag().cloned(),
depth: seed.depth(),
attempt: seed.attempt(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OutcomeKind {
Continue,
Retry,
Backoff,
Abort,
Stop,
Fail,
Fatal,
}
impl OutcomeKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Continue => "continue",
Self::Retry => "retry",
Self::Backoff => "backoff",
Self::Abort => "abort",
Self::Stop => "stop",
Self::Fail => "fail",
Self::Fatal => "fatal",
}
}
}
impl fmt::Display for OutcomeKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Failure {
pub kind: ErrorKind,
pub status: Option<StatusCode>,
pub message: String,
}
impl From<&Error> for Failure {
fn from(error: &Error) -> Self {
Self {
kind: error.kind(),
status: error.status(),
message: error.to_string(),
}
}
}
impl fmt::Display for Failure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SkipReason {
Disallowed,
TagAborted,
}
impl SkipReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Disallowed => "disallowed by robots.txt",
Self::TagAborted => "tag aborted",
}
}
}
impl fmt::Display for SkipReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Slowdown {
Concurrency {
limit: usize,
},
Gap {
gap: Duration,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StopReason {
Shutdown,
TimeLimit,
RequestBudget,
Requested,
Failed,
}
impl StopReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Shutdown => "shutdown requested",
Self::TimeLimit => "time limit reached",
Self::RequestBudget => "request budget spent",
Self::Requested => "worker requested stop",
Self::Failed => "crawl failed",
}
}
}
impl fmt::Display for StopReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone)]
pub(crate) struct Emitter {
sender: broadcast::Sender<Event>,
subscribers: Arc<AtomicUsize>,
}
impl Emitter {
pub(crate) fn new(capacity: usize) -> Self {
let (sender, _) = broadcast::channel(capacity.max(1));
Self {
sender,
subscribers: Arc::default(),
}
}
pub(crate) fn is_active(&self) -> bool {
self.subscribers.load(Ordering::Relaxed) > 0
}
pub(crate) fn emit(&self, event: impl FnOnce() -> Event) {
if self.is_active() {
let _ = self.sender.send(event());
}
}
pub(crate) fn emit_for(
&self,
request: Option<&RequestInfo>,
event: impl FnOnce(RequestInfo) -> Event,
) {
if let Some(request) = request {
self.emit(|| event(request.clone()));
}
}
pub(crate) fn subscribe(&self) -> Events {
self.subscribers.fetch_add(1, Ordering::Relaxed);
let subscription = Subscription {
receiver: self.sender.subscribe(),
subscribers: self.subscribers.clone(),
};
let stream = futures::stream::unfold(subscription, |mut subscription| async move {
let event = match subscription.receiver.recv().await {
Ok(event) => event,
Err(RecvError::Lagged(missed)) => Event::Lagged { missed },
Err(RecvError::Closed) => return None,
};
Some((event, subscription))
});
Events {
stream: Box::pin(stream),
}
}
pub(crate) fn notify(&self, seed: &Seed) -> Option<Notify> {
self.is_active().then(|| Notify {
emitter: self.clone(),
request: seed.into(),
})
}
}
impl fmt::Debug for Emitter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Emitter")
.field("subscribers", &self.subscribers.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
#[must_use = "streams do nothing unless polled"]
pub struct Events {
stream: Pin<Box<dyn Stream<Item = Event> + Send>>,
}
impl Stream for Events {
type Item = Event;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Event>> {
self.stream.as_mut().poll_next(cx)
}
}
impl fmt::Debug for Events {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Events").finish_non_exhaustive()
}
}
struct Subscription {
receiver: broadcast::Receiver<Event>,
subscribers: Arc<AtomicUsize>,
}
impl Drop for Subscription {
fn drop(&mut self) {
self.subscribers.fetch_sub(1, Ordering::Relaxed);
}
}
pub(crate) struct Notify {
emitter: Emitter,
request: RequestInfo,
}
impl Notify {
pub(crate) fn fetched(&self, fetch: Fetch) {
self.emitter.emit(|| Event::Fetched {
request: self.request.clone(),
status: fetch.status,
latency: fetch.latency.unwrap_or_default(),
});
}
}
impl From<&Outcome> for OutcomeKind {
fn from(outcome: &Outcome) -> Self {
match outcome {
Outcome::Continue => Self::Continue,
Outcome::Retry { .. } => Self::Retry,
Outcome::Backoff { .. } => Self::Backoff,
Outcome::Abort(_) => Self::Abort,
Outcome::Stop => Self::Stop,
Outcome::Fail(_) => Self::Fail,
Outcome::Fatal(_) => Self::Fatal,
}
}
}