spire_core/crawler/event/
mod.rs1use std::fmt;
6use std::pin::Pin;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::task::{Context, Poll};
10use std::time::Duration;
11
12use ::http::{Method, StatusCode, Uri};
13use futures::Stream;
14use tokio::sync::broadcast;
15use tokio::sync::broadcast::error::RecvError;
16
17use crate::context::Fetch;
18use crate::crawler::{Attempt, Depth, Stats, Tag};
19use crate::frontier::seed::Seed;
20use crate::worker::Outcome;
21use crate::{Error, ErrorKind};
22
23#[cfg(test)]
24mod tests;
25
26pub(crate) const CAPACITY: usize = 1024;
29
30#[derive(Debug, Clone)]
41#[non_exhaustive]
42pub enum Event {
43 Started,
45 RequestStarted {
47 request: RequestInfo,
49 },
50 Fetched {
54 request: RequestInfo,
56 status: Option<StatusCode>,
60 latency: Duration,
62 },
63 Handled {
65 request: RequestInfo,
67 outcome: OutcomeKind,
69 error: Option<Failure>,
72 elapsed: Duration,
75 },
76 Retried {
78 request: RequestInfo,
80 after: Option<Duration>,
82 },
83 Failed {
86 request: RequestInfo,
88 },
89 Skipped {
91 request: RequestInfo,
93 reason: SkipReason,
95 },
96 Cancelled {
98 request: RequestInfo,
100 },
101 HostSlowed {
103 host: String,
105 slowdown: Slowdown,
107 },
108 HostPaused {
111 host: String,
113 duration: Duration,
115 },
116 RobotsFetched {
118 origin: String,
120 error: Option<Failure>,
122 },
123 Stopping {
126 reason: StopReason,
128 },
129 Finished {
131 stats: Stats,
133 },
134 Lagged {
137 missed: u64,
139 },
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
145#[non_exhaustive]
146pub struct RequestInfo {
147 pub method: Method,
149 pub uri: Uri,
151 pub tag: Option<Tag>,
153 pub depth: Depth,
155 pub attempt: Attempt,
157}
158
159impl From<&Seed> for RequestInfo {
160 fn from(seed: &Seed) -> Self {
161 Self {
162 method: seed.method().clone(),
163 uri: seed.uri().clone(),
164 tag: seed.tag().cloned(),
165 depth: seed.depth(),
166 attempt: seed.attempt(),
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173#[non_exhaustive]
174pub enum OutcomeKind {
175 Continue,
177 Retry,
179 Backoff,
181 Abort,
183 Stop,
185 Fail,
188 Fatal,
190}
191
192impl OutcomeKind {
193 #[must_use]
195 pub const fn as_str(self) -> &'static str {
196 match self {
197 Self::Continue => "continue",
198 Self::Retry => "retry",
199 Self::Backoff => "backoff",
200 Self::Abort => "abort",
201 Self::Stop => "stop",
202 Self::Fail => "fail",
203 Self::Fatal => "fatal",
204 }
205 }
206}
207
208impl fmt::Display for OutcomeKind {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 f.write_str(self.as_str())
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
217#[non_exhaustive]
218pub struct Failure {
219 pub kind: ErrorKind,
221 pub status: Option<StatusCode>,
223 pub message: String,
225}
226
227impl From<&Error> for Failure {
228 fn from(error: &Error) -> Self {
229 Self {
230 kind: error.kind(),
231 status: error.status(),
232 message: error.to_string(),
233 }
234 }
235}
236
237impl fmt::Display for Failure {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 f.write_str(&self.message)
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
245#[non_exhaustive]
246pub enum SkipReason {
247 Disallowed,
249 TagAborted,
251}
252
253impl SkipReason {
254 #[must_use]
256 pub const fn as_str(self) -> &'static str {
257 match self {
258 Self::Disallowed => "disallowed by robots.txt",
259 Self::TagAborted => "tag aborted",
260 }
261 }
262}
263
264impl fmt::Display for SkipReason {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 f.write_str(self.as_str())
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
272#[non_exhaustive]
273pub enum Slowdown {
274 Concurrency {
277 limit: usize,
279 },
280 Gap {
283 gap: Duration,
285 },
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
290#[non_exhaustive]
291pub enum StopReason {
292 Shutdown,
295 TimeLimit,
298 RequestBudget,
301 Requested,
303 Failed,
306}
307
308impl StopReason {
309 #[must_use]
311 pub const fn as_str(self) -> &'static str {
312 match self {
313 Self::Shutdown => "shutdown requested",
314 Self::TimeLimit => "time limit reached",
315 Self::RequestBudget => "request budget spent",
316 Self::Requested => "worker requested stop",
317 Self::Failed => "crawl failed",
318 }
319 }
320}
321
322impl fmt::Display for StopReason {
323 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 f.write_str(self.as_str())
325 }
326}
327
328#[derive(Clone)]
334pub(crate) struct Emitter {
335 sender: broadcast::Sender<Event>,
336 subscribers: Arc<AtomicUsize>,
337}
338
339impl Emitter {
340 pub(crate) fn new(capacity: usize) -> Self {
343 let (sender, _) = broadcast::channel(capacity.max(1));
344 Self {
345 sender,
346 subscribers: Arc::default(),
347 }
348 }
349
350 pub(crate) fn is_active(&self) -> bool {
352 self.subscribers.load(Ordering::Relaxed) > 0
353 }
354
355 pub(crate) fn emit(&self, event: impl FnOnce() -> Event) {
357 if self.is_active() {
358 let _ = self.sender.send(event());
360 }
361 }
362
363 pub(crate) fn emit_for(
366 &self,
367 request: Option<&RequestInfo>,
368 event: impl FnOnce(RequestInfo) -> Event,
369 ) {
370 if let Some(request) = request {
371 self.emit(|| event(request.clone()));
372 }
373 }
374
375 pub(crate) fn subscribe(&self) -> Events {
377 self.subscribers.fetch_add(1, Ordering::Relaxed);
378 let subscription = Subscription {
379 receiver: self.sender.subscribe(),
380 subscribers: self.subscribers.clone(),
381 };
382 let stream = futures::stream::unfold(subscription, |mut subscription| async move {
383 let event = match subscription.receiver.recv().await {
384 Ok(event) => event,
385 Err(RecvError::Lagged(missed)) => Event::Lagged { missed },
386 Err(RecvError::Closed) => return None,
387 };
388 Some((event, subscription))
389 });
390 Events {
391 stream: Box::pin(stream),
392 }
393 }
394
395 pub(crate) fn notify(&self, seed: &Seed) -> Option<Notify> {
398 self.is_active().then(|| Notify {
399 emitter: self.clone(),
400 request: seed.into(),
401 })
402 }
403}
404
405impl fmt::Debug for Emitter {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 f.debug_struct("Emitter")
408 .field("subscribers", &self.subscribers.load(Ordering::Relaxed))
409 .finish_non_exhaustive()
410 }
411}
412
413#[must_use = "streams do nothing unless polled"]
419pub struct Events {
420 stream: Pin<Box<dyn Stream<Item = Event> + Send>>,
421}
422
423impl Stream for Events {
424 type Item = Event;
425
426 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Event>> {
427 self.stream.as_mut().poll_next(cx)
428 }
429}
430
431impl fmt::Debug for Events {
432 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433 f.debug_struct("Events").finish_non_exhaustive()
434 }
435}
436
437struct Subscription {
439 receiver: broadcast::Receiver<Event>,
440 subscribers: Arc<AtomicUsize>,
441}
442
443impl Drop for Subscription {
444 fn drop(&mut self) {
445 self.subscribers.fetch_sub(1, Ordering::Relaxed);
446 }
447}
448
449pub(crate) struct Notify {
451 emitter: Emitter,
452 request: RequestInfo,
453}
454
455impl Notify {
456 pub(crate) fn fetched(&self, fetch: Fetch) {
458 self.emitter.emit(|| Event::Fetched {
459 request: self.request.clone(),
460 status: fetch.status,
461 latency: fetch.latency.unwrap_or_default(),
462 });
463 }
464}
465
466impl From<&Outcome> for OutcomeKind {
467 fn from(outcome: &Outcome) -> Self {
468 match outcome {
469 Outcome::Continue => Self::Continue,
470 Outcome::Retry { .. } => Self::Retry,
471 Outcome::Backoff { .. } => Self::Backoff,
472 Outcome::Abort(_) => Self::Abort,
473 Outcome::Stop => Self::Stop,
474 Outcome::Fail(_) => Self::Fail,
475 Outcome::Fatal(_) => Self::Fatal,
476 }
477 }
478}