1use std::cell::Cell;
4use std::collections::{HashMap, HashSet};
5use std::pin::pin;
6use std::sync::Arc;
7use std::time::Duration;
8
9use ::http::StatusCode;
10use tokio::task::{Id, JoinError, JoinSet};
11use tokio::time::{Instant, sleep_until, timeout};
12use tracing::Instrument;
13
14use crate::backend::Backend;
15use crate::context::{Context, FETCH, Fetch, FetchScope};
16use crate::crawler::concurrency::{Feedback, Signal};
17use crate::crawler::connector::Connector;
18use crate::crawler::event::{
19 Emitter, Event, Failure, OutcomeKind, RequestInfo, SkipReason, StopReason,
20};
21use crate::crawler::hosts::{Admit, Hosts};
22#[cfg(feature = "robots")]
23use crate::crawler::robots::{BackendSource, RobotsCache, RobotsSource, Verdict};
24use crate::crawler::{Crawler, Live, Stats, Tag};
25use crate::frontier::queue::Queue;
26use crate::frontier::seed::Seed;
27use crate::frontier::{Ack, NextSeed, Report};
28use crate::identity::{Health, Identity};
29use crate::sync::lock;
30use crate::worker::{Outcome, Scope, Worker};
31use crate::{Error, ErrorKind, Result, TRACING_TARGET};
32
33const MAX_PARKED: usize = 4096;
36
37#[cfg(feature = "robots")]
39struct RobotsRun {
40 cache: RobotsCache,
41 source: Arc<dyn RobotsSource>,
42 agent: Arc<str>,
43 tasks: JoinSet<RobotsEvent>,
44}
45
46#[cfg(feature = "robots")]
48type RobotsEvent = (String, Result<robotxt::Robots>);
49
50#[cfg(not(feature = "robots"))]
52enum RobotsRun {}
53
54#[cfg(not(feature = "robots"))]
55type RobotsEvent = std::convert::Infallible;
56
57async fn next_robots(robots: &mut Option<RobotsRun>) -> Option<RobotsEvent> {
59 #[cfg(feature = "robots")]
60 if let Some(robots) = robots
61 && let Some(joined) = robots.tasks.join_next().await
62 {
63 return joined.ok();
64 }
65
66 #[cfg(not(feature = "robots"))]
67 let _ = robots;
68 std::future::pending().await
69}
70
71#[cfg_attr(
73 not(feature = "robots"),
74 expect(dead_code, reason = "robots.txt only disallows with `robots`")
75)]
76enum Access {
77 Allow(Seed),
78 Disallow(Seed),
79 Parked,
80}
81
82struct Run<B> {
84 tasks: JoinSet<(Outcome, Fetch)>,
85 in_flight: HashMap<Id, InFlight>,
87 robots: Option<RobotsRun>,
88 hosts: Hosts,
89 connector: Arc<Connector<B>>,
91 events: Emitter,
92 aborted: HashSet<Option<Tag>>,
93 started: usize,
94 stopping: bool,
95 grace: Option<Instant>,
96 error: Option<Error>,
97}
98
99struct InFlight {
101 seed: Seed,
102 identity: Option<Identity>,
104 host: Option<String>,
106 started: Instant,
108}
109
110fn handled(request: RequestInfo, outcome: &Outcome, elapsed: Duration) -> Event {
112 let error = match outcome {
113 Outcome::Fail(error) | Outcome::Fatal(error) => Some(Failure::from(error)),
114 _ => None,
115 };
116 Event::Handled {
117 request,
118 outcome: OutcomeKind::from(outcome),
119 error,
120 elapsed,
121 }
122}
123
124pub(super) fn signal(outcome: &Outcome) -> Signal {
129 match outcome {
130 Outcome::Fail(error)
131 if error.kind() == ErrorKind::Timeout
132 || matches!(
133 error.status().map(|status| status.as_u16()),
134 Some(429 | 503)
135 ) =>
136 {
137 Signal::Throttled
138 }
139 Outcome::Fail(_) | Outcome::Fatal(_) | Outcome::Retry { .. } | Outcome::Backoff { .. } => {
140 Signal::Failed
141 }
142 Outcome::Continue | Outcome::Abort(_) | Outcome::Stop => Signal::Ok,
143 }
144}
145
146impl<B: Backend> Run<B> {
147 fn finish_host(
150 &mut self,
151 host: Option<String>,
152 seed: &Seed,
153 started: Instant,
154 fetch: Fetch,
155 outcome: &Outcome,
156 ) {
157 let now = Instant::now();
158 let pause = match outcome {
162 Outcome::Fail(error) if error.is_retryable() => error.retry_after(),
163 _ => None,
164 };
165 let signal = if pause.is_some() {
166 Signal::Failed
167 } else {
168 signal(outcome)
169 };
170 let throttled = signal == Signal::Throttled || pause.is_some();
173 let Some(host) = host.or_else(|| throttled.then(|| self.hosts.track(seed))) else {
174 return;
175 };
176 let feedback = Feedback {
177 started,
178 latency: fetch.latency,
179 unchanged: fetch.status == Some(StatusCode::NOT_MODIFIED),
180 signal,
181 };
182 if let Some(slowdown) = self.hosts.finished(&host, Some(feedback), now) {
183 self.events.emit(|| Event::HostSlowed {
184 host: host.clone(),
185 slowdown,
186 });
187 }
188 if let Some(pause) = pause
189 && self.hosts.pause(&host, now + pause)
190 {
191 self.events.emit(|| Event::HostPaused {
192 host,
193 duration: pause,
194 });
195 }
196 }
197
198 fn stop(&mut self, reason: StopReason, grace: Option<Duration>) {
199 if self.stopping {
200 return;
201 }
202 tracing::info!(target: TRACING_TARGET, in_flight = self.tasks.len(), %reason, "stopping crawl");
203 self.events.emit(|| Event::Stopping { reason });
204 self.stopping = true;
205 self.grace = grace.map(|grace| Instant::now() + grace);
206 }
207
208 fn fail(&mut self, error: Error, grace: Option<Duration>) {
211 tracing::error!(target: TRACING_TARGET, %error, "crawl failed");
212 self.error.get_or_insert(error);
213 self.stop(StopReason::Failed, grace);
214 }
215
216 fn is_aborted(&self, seed: &Seed) -> bool {
217 self.aborted.contains(&seed.tag().cloned())
218 }
219
220 #[cfg_attr(
222 not(feature = "robots"),
223 expect(clippy::unused_self, reason = "no robots.txt state")
224 )]
225 fn robots_busy(&self) -> bool {
226 #[cfg(feature = "robots")]
227 return self
228 .robots
229 .as_ref()
230 .is_some_and(|robots| !robots.tasks.is_empty());
231 #[cfg(not(feature = "robots"))]
232 false
233 }
234
235 #[cfg_attr(
237 not(feature = "robots"),
238 expect(clippy::unused_self, reason = "no robots.txt state")
239 )]
240 fn robots_access(&mut self, seed: Seed) -> Access {
241 #[cfg(feature = "robots")]
242 if let Some(robots) = &mut self.robots {
243 return match robots.cache.admit(seed, Instant::now()) {
244 Verdict::Allow(seed) => Access::Allow(seed),
245 Verdict::Disallow(seed) => Access::Disallow(seed),
246 Verdict::Parked => Access::Parked,
247 };
248 }
249 Access::Allow(seed)
250 }
251
252 #[cfg_attr(
255 not(feature = "robots"),
256 expect(clippy::unused_self, reason = "no robots.txt state")
257 )]
258 fn robots_tick(&mut self) -> Option<Instant> {
259 #[cfg(feature = "robots")]
260 if let Some(robots) = &mut self.robots {
261 robots.cache.retry_due(Instant::now());
262 for (origin, url) in robots.cache.take_fetches() {
263 let fetch = robots.source.fetch(url, robots.agent.clone());
264 robots.tasks.spawn(async move { (origin, fetch.await) });
265 }
266 return robots.cache.earliest();
267 }
268 None
269 }
270
271 fn parked(&self) -> usize {
273 #[cfg(feature = "robots")]
274 let robots = self
275 .robots
276 .as_ref()
277 .map_or(0, |robots| robots.cache.parked());
278 #[cfg(not(feature = "robots"))]
279 let robots = 0;
280 self.hosts.parked() + robots
281 }
282
283 fn drain(&mut self) -> Vec<Seed> {
285 let seeds = self.hosts.drain();
286 #[cfg(feature = "robots")]
287 let seeds = match &mut self.robots {
288 Some(robots) => {
289 robots.tasks.abort_all();
290 seeds.into_iter().chain(robots.cache.drain()).collect()
291 }
292 None => seeds,
293 };
294 seeds
295 }
296}
297
298impl<B, W> Crawler<B, W>
299where
300 B: Backend,
301 W: Worker<B::Session>,
302{
303 pub async fn run(&self) -> Result<Stats> {
312 let seeds = std::mem::take(&mut *lock(&self.seeds));
313 for seed in seeds {
314 self.frontier.push(seed).await?;
315 }
316
317 let started = Instant::now();
318 *lock(&self.live) = Live {
319 stats: Stats::default(),
320 started: Some(started),
321 running: true,
322 };
323
324 let settings = &self.settings;
325 let deadline = settings.time_limit.map(|limit| started + limit);
326 let mut run = self.new_run();
327 tracing::info!(target: TRACING_TARGET, concurrency = settings.concurrency.get(), "crawl started");
328 self.events.emit(|| Event::Started);
329
330 let mut wait_timer = pin!(sleep_until(started));
334 let mut armed_wait = None;
335 let mut deadline_timer = pin!(sleep_until(deadline.unwrap_or(started)));
336 let mut grace_timer = pin!(sleep_until(started));
337 let mut armed_grace = None;
338 let mut shutdown = pin!(self.shutdown.cancelled());
339
340 loop {
341 let wait = if run.stopping {
342 None
343 } else {
344 self.fill(&mut run).await
345 };
346
347 if run.tasks.is_empty() && !run.robots_busy() && (run.stopping || wait.is_none()) {
348 break;
349 }
350 if let Some(at) = wait
351 && armed_wait != wait
352 {
353 wait_timer.as_mut().reset(at);
354 armed_wait = wait;
355 }
356 if let Some(at) = run.grace
357 && armed_grace != run.grace
358 {
359 grace_timer.as_mut().reset(at);
360 armed_grace = run.grace;
361 }
362
363 tokio::select! {
366 biased;
367 () = &mut shutdown, if !run.stopping => {
368 run.stop(StopReason::Shutdown, settings.shutdown_grace);
369 }
370 () = &mut deadline_timer, if deadline.is_some() && !run.stopping => {
371 run.stop(StopReason::TimeLimit, settings.shutdown_grace);
372 }
373 () = &mut grace_timer, if run.grace.is_some() => {
374 tracing::warn!(target: TRACING_TARGET, in_flight = run.tasks.len(), "shutdown grace period over, cancelling requests");
375 run.grace = None;
376 run.tasks.abort_all();
377 }
378 Some(joined) = run.tasks.join_next_with_id() => self.complete(&mut run, joined).await,
379 Some(fetched) = next_robots(&mut run.robots) => self.robots_fetched(&mut run, fetched).await,
380 () = &mut wait_timer, if wait.is_some() => {
381 armed_wait = None;
382 }
383 }
384 }
385
386 for seed in run.drain() {
387 self.report(&mut run, Report::Cancelled(seed)).await;
388 }
389
390 let stats = {
391 let mut live = lock(&self.live);
392 live.running = false;
393 live.stats.elapsed = started.elapsed();
394 live.stats
395 };
396 tracing::info!(
397 target: TRACING_TARGET,
398 requests = stats.requests,
399 succeeded = stats.succeeded,
400 failed = stats.failed,
401 retried = stats.retried,
402 skipped = stats.skipped,
403 cancelled = stats.cancelled,
404 elapsed_ms = stats.elapsed.as_millis(),
405 "crawl finished",
406 );
407 self.events.emit(|| Event::Finished { stats });
408
409 match run.error {
410 Some(error) => Err(error),
411 None => Ok(stats),
412 }
413 }
414
415 fn new_run(&self) -> Run<B> {
417 let backend = self.backend.clone();
418 let connector = Arc::new(match &self.identities {
419 Some(identities) => Connector::with_identities(backend, identities.clone()),
420 None => Connector::new(backend),
421 });
422 let robots = self.robots_run(&connector);
423 let hosts = Hosts::new(self.settings.hosts, robots.is_some());
426 Run {
427 tasks: JoinSet::new(),
428 in_flight: HashMap::new(),
429 robots,
430 hosts,
431 connector,
432 events: self.events.clone(),
433 aborted: HashSet::new(),
434 started: 0,
435 stopping: false,
436 grace: None,
437 error: None,
438 }
439 }
440
441 async fn fill(&self, run: &mut Run<B>) -> Option<Instant> {
444 let settings = &self.settings;
445 let mut wait = run.robots_tick();
446
447 while run.tasks.len() < settings.concurrency.get() {
448 if settings.max_requests.is_some_and(|max| run.started >= max) {
449 run.stop(StopReason::RequestBudget, settings.shutdown_grace);
450 return None;
451 }
452
453 if let Some(seed) = run.hosts.take_ready(Instant::now()) {
454 self.start(run, seed).await;
455 continue;
456 }
457 if run.parked() >= MAX_PARKED {
458 break;
459 }
460
461 match self.frontier.pop().await {
462 Ok(NextSeed::Ready(seed)) => self.admit(run, seed).await,
463 Ok(NextSeed::Wait(at)) => {
464 wait = wait.min(Some(at)).or(Some(at));
465 break;
466 }
467 Ok(NextSeed::Empty) => break,
468 Err(error) => {
469 run.fail(error, settings.shutdown_grace);
470 return None;
471 }
472 }
473 }
474
475 let robots = run.robots_tick();
476 [wait, robots, run.hosts.earliest()]
477 .into_iter()
478 .flatten()
479 .min()
480 }
481
482 async fn admit(&self, run: &mut Run<B>, seed: Seed) {
485 match run.robots_access(seed) {
486 Access::Allow(seed) => match run.hosts.admit(seed, Instant::now()) {
487 Admit::Start(seed) => self.start(run, seed).await,
488 Admit::Parked => {}
489 },
490 Access::Disallow(seed) => self.skip(run, seed, SkipReason::Disallowed).await,
491 Access::Parked => {}
492 }
493 }
494
495 #[cfg(feature = "robots")]
499 fn robots_run(&self, connector: &Arc<Connector<B>>) -> Option<RobotsRun> {
500 let config = self.settings.robots.clone()?;
501 let source = config
502 .source
503 .clone()
504 .unwrap_or_else(|| Arc::new(BackendSource::new(connector.clone())));
505
506 Some(RobotsRun {
507 agent: config.agent.clone(),
508 cache: RobotsCache::new(config),
509 source,
510 tasks: JoinSet::new(),
511 })
512 }
513
514 #[cfg(not(feature = "robots"))]
515 #[expect(clippy::unused_self, reason = "mirrors the signature with `robots`")]
516 const fn robots_run(&self, _connector: &Arc<Connector<B>>) -> Option<RobotsRun> {
517 None
518 }
519
520 #[cfg(feature = "robots")]
522 async fn robots_fetched(&self, run: &mut Run<B>, (origin, result): RobotsEvent) {
523 let Some(robots) = &mut run.robots else {
524 return;
525 };
526 run.events.emit(|| Event::RobotsFetched {
527 origin: origin.clone(),
528 error: result.as_ref().err().map(Failure::from),
529 });
530 let fetched = robots.cache.fetched(&origin, result, Instant::now());
531 if let Some(delay) = fetched.delay {
532 let host = origin
533 .split_once("://")
534 .map_or(origin.as_str(), |(_, host)| host);
535 run.hosts.raise_delay(host, delay);
536 }
537 for seed in fetched.sitemaps {
538 let uri = seed.uri().clone();
539 if let Err(error) = self.queue().push(seed).await {
540 tracing::warn!(target: TRACING_TARGET, %uri, %error, "failed to queue a sitemap");
541 }
542 }
543 for seed in fetched.released {
544 self.admit(run, seed).await;
545 }
546 }
547
548 #[cfg(not(feature = "robots"))]
549 #[expect(clippy::unused_self, reason = "mirrors the signature with `robots`")]
550 fn robots_fetched(&self, _run: &mut Run<B>, never: RobotsEvent) -> std::future::Ready<()> {
551 match never {}
552 }
553
554 async fn skip(&self, run: &mut Run<B>, seed: Seed, reason: SkipReason) {
556 tracing::debug!(target: TRACING_TARGET, uri = %seed.uri(), %reason, "skipping request");
557 self.tally(|stats| stats.skipped += 1);
558 run.events.emit(|| Event::Skipped {
559 request: (&seed).into(),
560 reason,
561 });
562 self.report(run, Report::Skipped(seed)).await;
563 }
564
565 async fn start(&self, run: &mut Run<B>, seed: Seed) {
567 if run.is_aborted(&seed) {
568 self.skip(run, seed, SkipReason::TagAborted).await;
569 return;
570 }
571
572 run.started += 1;
573 let now = Instant::now();
574 let host = run.hosts.start(&seed, now);
575 run.events.emit(|| Event::RequestStarted {
576 request: (&seed).into(),
577 });
578 let notify = run.events.notify(&seed);
579
580 let worker = self.worker.clone();
581 let queue = self.queue();
582 let datasets = self.datasets.clone();
583 let deadline = self.settings.request_timeout;
584 let span = tracing::debug_span!(
585 target: TRACING_TARGET,
586 "request",
587 method = %seed.method(),
588 uri = %seed.uri(),
589 tag = seed.tag().map(tracing::field::display),
590 depth = %seed.depth(),
591 attempt = %seed.attempt(),
592 );
593
594 let connector = run.connector.clone();
597 let mut request = seed.clone().into_request();
598 request.extensions_mut().extend(self.extensions.clone());
599 let identity = connector.identify(&seed, &mut request);
600 let used = identity.as_ref().ok().cloned().flatten();
601
602 let task = async move {
603 let started = Instant::now();
604 let handle = async {
605 let connected = match identity {
606 Ok(identity) => connector.connect(identity.as_ref()).await,
607 Err(error) => Err(error),
608 };
609 let client = match connected {
610 Ok(client) => client,
611 Err(error) => return Outcome::Fail(error),
612 };
613 let queue: Queue = queue.child_of(&request);
614 let cx = Context::from_parts(request, client, queue, datasets);
615 worker.handle(cx).await
616 };
617 let scope = FetchScope {
621 fetch: Cell::default(),
622 notify,
623 };
624 let (outcome, fetch) = FETCH
625 .scope(scope, async {
626 let outcome = match deadline {
627 Some(deadline) => timeout(deadline, handle).await.unwrap_or_else(|_| {
628 let error =
629 Error::new(ErrorKind::Timeout).with_message("request timed out");
630 Outcome::Fail(error)
631 }),
632 None => handle.await,
633 };
634 (outcome, FETCH.with(|scope| scope.fetch.get()))
635 })
636 .await;
637
638 tracing::debug!(
639 target: TRACING_TARGET,
640 outcome = %OutcomeKind::from(&outcome),
641 elapsed_ms = started.elapsed().as_millis(),
642 "request handled",
643 );
644 (outcome, fetch)
645 };
646
647 let id = run.tasks.spawn(Box::pin(task.instrument(span))).id();
649 let in_flight = InFlight {
650 seed,
651 identity: used,
652 host,
653 started: now,
654 };
655 run.in_flight.insert(id, in_flight);
656 }
657
658 async fn complete(&self, run: &mut Run<B>, joined: Result<(Id, (Outcome, Fetch)), JoinError>) {
660 let (id, (outcome, fetch)) = match joined {
661 Ok(joined) => joined,
662 Err(error) if error.is_cancelled() => {
663 let Some(InFlight { seed, host, .. }) = run.in_flight.remove(&error.id()) else {
664 return;
665 };
666 if let Some(host) = host {
667 run.hosts.finished(&host, None, Instant::now());
668 }
669 self.tally(|stats| stats.cancelled += 1);
670 run.events.emit(|| Event::Cancelled {
671 request: (&seed).into(),
672 });
673 self.report(run, Report::Cancelled(seed)).await;
674 return;
675 }
676 Err(error) => {
677 let failure = Error::worker("worker panicked").with_source(error.to_string());
678 (error.id(), (Outcome::Fail(failure), Fetch::default()))
679 }
680 };
681 let Some(InFlight {
682 seed,
683 identity,
684 host,
685 started,
686 }) = run.in_flight.remove(&id)
687 else {
688 return;
689 };
690 let request = run.events.is_active().then(|| RequestInfo::from(&seed));
692 run.events.emit_for(request.as_ref(), |request| {
693 handled(request, &outcome, started.elapsed())
694 });
695
696 run.finish_host(host, &seed, started, fetch, &outcome);
697 if let Some(identity) = &identity {
698 run.connector
699 .finished(identity, Health::of(&outcome, fetch.status));
700 }
701 self.tally(|stats| {
702 stats.requests += 1;
703 if fetch.status == Some(StatusCode::NOT_MODIFIED) {
704 stats.unchanged += 1;
705 }
706 });
707
708 self.settle(run, seed, outcome, request).await;
709 }
710
711 async fn settle(
714 &self,
715 run: &mut Run<B>,
716 seed: Seed,
717 outcome: Outcome,
718 request: Option<RequestInfo>,
719 ) {
720 let uri = seed.uri().clone();
721 let mut failure = None;
722 let mut failed = false;
723 let report = match outcome {
724 Outcome::Continue => {
725 self.tally(|stats| stats.succeeded += 1);
726 Report::Done(seed)
727 }
728 Outcome::Stop => {
729 self.tally(|stats| stats.succeeded += 1);
730 run.stop(StopReason::Requested, self.settings.shutdown_grace);
731 Report::Done(seed)
732 }
733 Outcome::Abort(scope) => {
734 let tag = match scope {
735 Scope::SameTag => seed.tag().cloned(),
736 Scope::Tag(tag) => Some(tag),
737 };
738 tracing::info!(target: TRACING_TARGET, tag = tag.as_ref().map(tracing::field::display), "aborting tag");
739 run.aborted.insert(tag);
740 self.tally(|stats| stats.succeeded += 1);
741 Report::Done(seed)
742 }
743 Outcome::Retry { after } => Report::Retry { seed, after },
744 Outcome::Backoff { scope, duration } => self.backoff(run, seed, scope, duration).await,
745 Outcome::Fatal(error) => {
746 failed = true;
747 run.fail(error.with_uri(uri.clone()), self.settings.shutdown_grace);
748 Report::Failed(seed)
749 }
750 Outcome::Fail(error) => {
751 let error = match error.uri() {
752 Some(_) => error,
753 None => error.with_uri(uri.clone()),
754 };
755 if error.is_retryable() {
756 tracing::debug!(target: TRACING_TARGET, %uri, %error, "request failed, retrying");
757 let after = error.retry_after();
760 failure = Some(error);
761 Report::Retry { seed, after }
762 } else {
763 tracing::warn!(target: TRACING_TARGET, %uri, %error, "request failed");
764 failed = true;
765 Report::Failed(seed)
766 }
767 }
768 };
769
770 let retry = match &report {
771 Report::Retry { after, .. } => Some(*after),
772 _ => None,
773 };
774 match self.report(run, report).await {
775 Some(Ack::Applied) if let Some(after) = retry => {
776 self.tally(|stats| stats.retried += 1);
777 run.events
778 .emit_for(request.as_ref(), |request| Event::Retried {
779 request,
780 after,
781 });
782 }
783 Some(Ack::RetryRefused) => {
784 failed = true;
785 tracing::warn!(
786 target: TRACING_TARGET,
787 %uri,
788 error = failure.as_ref().map(tracing::field::display),
789 "request failed, out of retries",
790 );
791 }
792 _ => {}
793 }
794 if failed {
795 self.tally(|stats| stats.failed += 1);
796 run.events
797 .emit_for(request.as_ref(), |request| Event::Failed { request });
798 }
799 }
800
801 async fn backoff(
804 &self,
805 run: &mut Run<B>,
806 seed: Seed,
807 scope: Scope,
808 duration: Duration,
809 ) -> Report {
810 let tag = match scope {
811 Scope::SameTag => seed.tag().cloned(),
812 Scope::Tag(tag) => Some(tag),
813 };
814 tracing::debug!(
815 target: TRACING_TARGET,
816 tag = tag.as_ref().map(tracing::field::display),
817 duration_ms = duration.as_millis(),
818 "backing off",
819 );
820
821 let until = Instant::now() + duration;
822 self.report(run, Report::Backoff { tag, until }).await;
823 Report::Retry {
824 seed,
825 after: Some(duration),
826 }
827 }
828
829 async fn report(&self, run: &mut Run<B>, report: Report) -> Option<Ack> {
831 match self.frontier.report(report).await {
832 Ok(ack) => Some(ack),
833 Err(error) => {
834 run.fail(error, self.settings.shutdown_grace);
835 None
836 }
837 }
838 }
839}