leyline/core/crawl/
clock.rs1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4use std::time::Duration;
5
6use futures_util::stream::{FuturesUnordered, Stream, StreamExt};
7use tokio::time::{Instant, Sleep};
8use tokio_util::sync::WaitForCancellationFutureOwned;
9
10use super::frontier::{Queued, page_key};
11use super::{Crawl, CrawlPage, SkipReason};
12use crate::core::config::Origin;
13use crate::core::deadline::Deadline;
14use crate::core::error::Result;
15
16const ROOM_RECHECK: Duration = Duration::from_millis(250);
17const ALARM_FLOOR: Duration = Duration::from_millis(10);
18
19#[derive(Default)]
20pub(super) struct Clock {
21 pub(super) stopped: Option<Pin<Box<WaitForCancellationFutureOwned>>>,
22 alarm: Option<Pin<Box<Sleep>>>,
23 alarm_at: Option<Instant>,
24 delayed: Vec<(Instant, Queued)>,
25}
26
27impl Crawl {
28 pub(super) fn later(&mut self, mut queued: Queued, wait: Duration) {
29 let Some(at) = Instant::now().checked_add(wait) else {
30 self.stats.skip(SkipReason::Status);
31 return self.settle(page_key(&queued.url));
32 };
33 queued.attempts += 1;
34 self.clock.delayed.push((at, queued));
35 self.set_alarm(at);
36 }
37
38 pub(super) fn promote_due(&mut self) {
39 let now = Instant::now();
40 let (due, waiting): (Vec<_>, Vec<_>) = std::mem::take(&mut self.clock.delayed)
41 .into_iter()
42 .partition(|(at, _)| *at <= now);
43 self.clock.delayed = waiting;
44 for (_, queued) in due {
45 self.frontier.push_back(queued);
46 }
47 }
48
49 pub(super) fn set_alarm(&mut self, at: Instant) {
50 let at = at.max(Instant::now() + ALARM_FLOOR);
51 if self.clock.alarm_at.is_none_or(|current| at < current) {
52 self.clock.alarm = Some(Deadline::alarm(at));
53 self.clock.alarm_at = Some(at);
54 }
55 }
56
57 pub(super) fn plan_alarm(&mut self) {
58 let now = Instant::now();
59 let sitemap_hosts: Vec<Origin> = self
60 .sitemap_queue
61 .iter()
62 .filter(|file| file.not_before.is_none_or(|at| at <= now))
63 .filter_map(|file| Origin::of(&file.url))
64 .collect();
65 let blocked = self
66 .frontier
67 .origins()
68 .chain(sitemap_hosts.iter())
69 .map(|origin| self.host_wait(origin))
70 .chain(self.clock.delayed.iter().map(|(at, _)| Some(*at)))
71 .chain(
72 self.sitemap_queue
73 .iter()
74 .map(|file| file.not_before.filter(|at| *at > now)),
75 )
76 .flatten()
77 .min();
78 if let Some(at) = blocked.filter(|_| !self.budget_spent()) {
79 self.set_alarm(at);
80 }
81 }
82
83 fn host_wait(&self, origin: &Origin) -> Option<Instant> {
84 let own = self.session.host_limits();
85 match self.pace.ready_at(origin).max(own.ready_at(origin)) {
86 Some(at) => Some(at),
87 None if !(self.pace.ready(origin) && own.ready(origin)) => {
88 Some(Instant::now() + ROOM_RECHECK)
89 }
90 None => None,
91 }
92 }
93
94 fn waiting_work(&self) -> bool {
95 !self.budget_spent()
96 && (!self.frontier.is_empty()
97 || !self.clock.delayed.is_empty()
98 || !self.sitemap_queue.is_empty())
99 }
100
101 fn stopped(&mut self, cx: &mut Context<'_>) -> bool {
102 let signalled = self
103 .clock
104 .stopped
105 .as_mut()
106 .is_some_and(|stopped| stopped.as_mut().poll(cx).is_ready());
107 signalled || self.session.is_shut_down()
108 }
109
110 fn halt(&mut self) {
111 self.frontier.clear();
112 self.sitemap_queue.clear();
113 self.clock.delayed.clear();
114 self.clock.alarm = None;
115 self.clock.alarm_at = None;
116 self.tasks = FuturesUnordered::new();
117 }
118}
119
120impl Stream for Crawl {
121 type Item = Result<CrawlPage>;
122
123 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
124 let this = self.get_mut();
125 this.start();
126 loop {
127 if this.stopped(cx) {
128 this.halt();
129 return Poll::Ready(this.ready.pop_front());
130 }
131 this.schedule();
132 if let Some(item) = this.ready.pop_front() {
133 return Poll::Ready(Some(item));
134 }
135 if this.limit_met() && this.pages_in_flight == 0 {
136 this.halt();
137 return Poll::Ready(None);
138 }
139 match this.tasks.poll_next_unpin(cx) {
140 Poll::Ready(Some(event)) => {
141 this.handle(event);
142 continue;
143 }
144 Poll::Ready(None) if !this.waiting_work() => {
145 if this.release_held() {
146 continue;
147 }
148 return Poll::Ready(None);
149 }
150 Poll::Ready(None) | Poll::Pending => {}
151 }
152 let fired = this
153 .clock
154 .alarm
155 .as_mut()
156 .is_some_and(|alarm| alarm.as_mut().poll(cx).is_ready());
157 if !fired {
158 return Poll::Pending;
159 }
160 this.clock.alarm = None;
161 this.clock.alarm_at = None;
162 }
163 }
164}