1use crate::{
13 DialLimit, Error,
14 engine::Engine,
15 model::{OverlayPeerStatus, OverlayStatus},
16 peer::{PeerSource, SyncPeers},
17 peer_inventory::{placed, selected},
18 peer_protocol::Placement,
19 recorded::{RecordedError, unix_seconds},
20};
21use libp2p::{Multiaddr, PeerId};
22use range::{Accepted, Opened, Range, accept, open};
23use std::{
24 sync::{
25 Arc, Mutex,
26 atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering},
27 },
28 time::{Duration, Instant},
29};
30
31mod range;
32
33pub type ConnectedPeers = Arc<dyn Fn() -> u64 + Send + Sync>;
34pub type Listeners = Arc<dyn Fn() -> Vec<Multiaddr> + Send + Sync>;
35
36pub struct OverlayInfo {
38 pub peer_id: String,
39 pub listen: Listeners,
40 pub announce: Vec<String>,
41 pub peer_book: u64,
42 pub connected: ConnectedPeers,
43}
44
45pub struct PeerSync {
47 pub peers: SyncPeers,
48 pub info: OverlayInfo,
49}
50
51struct Peer {
52 label: String,
53 id: String,
54 peer_id: PeerId,
55 source: PeerSource,
56 range: Mutex<Option<Range>>,
57 floor: AtomicU64,
60 last_request: Mutex<Option<Instant>>,
61 last_error: Mutex<Option<RecordedError>>,
62 backoff: Mutex<Backoff>,
63 busy_turns: AtomicU32,
64 idle_turns: AtomicU32,
65 last_progress: Mutex<Option<u64>>,
66 connected_since: Mutex<Option<u64>>,
68}
69
70struct Backoff {
73 failures: u32,
74 until: Option<u64>,
75}
76
77impl Peer {
78 fn backoff_until(&self, now: u64) -> Result<Option<u64>, Error> {
79 let backoff = self.backoff.lock().map_err(|_| poisoned())?;
80 Ok(backoff.until.filter(|until| *until > now))
81 }
82
83 fn last_progress(&self) -> Result<Option<u64>, Error> {
84 Ok(*self.last_progress.lock().map_err(|_| poisoned())?)
85 }
86
87 fn hold_off(&self, now: u64, seconds: u64) -> Result<(), Error> {
90 let until = now.checked_add(seconds).unwrap_or(u64::MAX);
91 let mut backoff = self.backoff.lock().map_err(|_| poisoned())?;
92 backoff.until = Some(backoff.until.map_or(until, |current| current.max(until)));
93 Ok(())
94 }
95
96 fn wait_closed(&self, stop: &AtomicBool, deadline: Instant) -> bool {
99 let until = deadline.min(Instant::now() + Scheduler::EVICT_WAIT);
100 while self.source.connected() {
101 if stop.load(Ordering::Acquire) || Instant::now() >= until {
102 return false;
103 }
104 std::thread::sleep(Duration::from_millis(25));
105 }
106 true
107 }
108}
109
110enum Pause {
113 Done,
114 Abandon,
115 Error(Error),
116 Blame(Error),
117}
118
119enum Turn {
122 Quiet,
123 Busy(Error),
124 Retry(Error),
125 Blamed(Error),
126}
127
128fn blame(error: Error) -> Pause {
131 match error {
132 Error::Busy(_)
133 | Error::PeerBusy
134 | Error::Transport(_)
135 | Error::DialDenied(_)
136 | Error::Stale => Pause::Error(error),
137 other => Pause::Blame(other),
138 }
139}
140
141impl From<Error> for Pause {
142 fn from(error: Error) -> Self {
143 Pause::Error(error)
144 }
145}
146
147struct Round<'a> {
151 stop: &'a AtomicBool,
152 deadline: Instant,
153 fetched: usize,
154 requested_page: bool,
155 contacted: bool,
156 progressed: bool,
157}
158
159impl Round<'_> {
160 fn open(&self) -> Result<(), Pause> {
161 if self.stop.load(Ordering::Acquire) || Instant::now() >= self.deadline {
162 return Err(Pause::Done);
163 }
164 Ok(())
165 }
166}
167
168pub(crate) struct Scheduler {
169 offered: SyncPeers,
170 peers: Mutex<Vec<Arc<Peer>>>,
171 turn: AtomicUsize,
172 configured_turn: AtomicBool,
173 configured_next: AtomicUsize,
174 info: OverlayInfo,
175 admitted: AtomicU64,
176 rejected: AtomicU64,
177 penalized: Mutex<Vec<PeerId>>,
178 steps: AtomicU64,
179}
180
181fn poisoned() -> Error {
182 Error::Invalid("peer synchronization state poisoned".into())
183}
184
185fn rotate(
187 peers: &[Arc<Peer>],
188 turn: &AtomicUsize,
189 omitting: &[String],
190 now: u64,
191) -> Result<Option<Arc<Peer>>, Error> {
192 for _ in 0..peers.len() {
193 let peer = &peers[turn.fetch_add(1, Ordering::Relaxed) % peers.len()];
194 if !omitting.contains(&peer.id) && peer.backoff_until(now)?.is_none() {
195 return Ok(Some(Arc::clone(peer)));
196 }
197 }
198 Ok(None)
199}
200
201fn configured(peer: &Arc<Peer>) -> bool {
202 peer.source.configured()
203}
204
205fn evictable(peer: &Peer, step: u64, now: u64) -> Result<bool, Error> {
209 let settled = peer
210 .connected_since
211 .lock()
212 .map_err(|_| poisoned())?
213 .is_some_and(|since| step >= since.saturating_add(2));
214 let idle = peer
215 .last_progress()?
216 .is_none_or(|at| now >= at.saturating_add(Scheduler::IDLE_BACKOFF));
217 Ok(settled && idle && peer.source.outbound())
218}
219
220fn inventory_peer(label: String, source: PeerSource) -> Result<Peer, Error> {
221 let peer_id = match label.parse::<PeerId>() {
222 Ok(id) => id,
223 Err(error) => {
224 return Err(Error::Invalid(format!("inventory peer label: {error}")));
225 }
226 };
227 Ok(Peer {
228 label,
229 id: peer_id.to_string(),
230 peer_id,
231 source,
232 range: Mutex::new(None),
233 floor: AtomicU64::new(0),
234 last_request: Mutex::new(None),
235 last_error: Mutex::new(None),
236 backoff: Mutex::new(Backoff {
237 failures: 0,
238 until: None,
239 }),
240 busy_turns: AtomicU32::new(0),
241 idle_turns: AtomicU32::new(0),
242 last_progress: Mutex::new(None),
243 connected_since: Mutex::new(None),
244 })
245}
246
247impl Scheduler {
248 pub const RANGE_HEIGHTS: u64 = 4096;
250 pub const PAGE_ENTRIES: u32 = 512;
252 pub const ROUND_FETCHES: usize = 16;
254 pub const ROUND_TIME: Duration = Duration::from_secs(20);
256 pub const REQUEST_SPACING: Duration = Duration::from_millis(250);
258 pub const BACKOFF_SECONDS: u64 = 30;
260 pub const BACKOFF_CAP: u64 = 86_400;
261 pub const BUSY_TURNS: u32 = 10;
263 pub const MISSING_ANSWERS: u32 = 2;
265 pub const IDLE_TURNS: u32 = 3;
270 pub const IDLE_BACKOFF: u64 = 60;
271 pub const EVICT_WAIT: Duration = Duration::from_secs(2);
273 const MAX_PEERS: usize = 1024;
274
275 pub(crate) fn new(sync: PeerSync) -> Result<Self, Error> {
277 let scheduler = Self {
278 offered: sync.peers,
279 peers: Mutex::new(Vec::new()),
280 turn: AtomicUsize::new(0),
281 configured_turn: AtomicBool::new(false),
282 configured_next: AtomicUsize::new(0),
283 info: sync.info,
284 admitted: AtomicU64::new(0),
285 rejected: AtomicU64::new(0),
286 penalized: Mutex::new(Vec::new()),
287 steps: AtomicU64::new(0),
288 };
289 scheduler.refresh(scheduler.offered.sources(&[]), unix_seconds()?)?;
290 Ok(scheduler)
291 }
292
293 pub(crate) fn step(&self, engine: &Engine, stop: &AtomicBool) -> Result<(), Error> {
297 let omitting = engine.omitting_peers()?;
298 let now = unix_seconds()?;
299 self.refresh(self.offered.sources(&omitting), now)?;
300 let step = self.steps.fetch_add(1, Ordering::Relaxed);
301 self.observe_connections(step)?;
302 let Some(peer) = self.next_peer(&omitting, now)? else {
303 return Ok(());
304 };
305 let deadline = Instant::now() + Self::ROUND_TIME;
306 let denied = self.turn(&peer, engine, stop, deadline, now)?;
307 if denied && self.free_slot_for(&peer, step, stop, deadline, now)? {
308 self.turn(&peer, engine, stop, deadline, now)?;
309 }
310 Ok(())
311 }
312
313 fn turn(
316 &self,
317 peer: &Peer,
318 engine: &Engine,
319 stop: &AtomicBool,
320 deadline: Instant,
321 now: u64,
322 ) -> Result<bool, Error> {
323 let mut round = Round {
324 stop,
325 deadline,
326 fetched: 0,
327 requested_page: false,
328 contacted: false,
329 progressed: false,
330 };
331 let outcome = self.advance(peer, engine, &mut round);
332 if stop.load(Ordering::Acquire) {
333 return Ok(false);
334 }
335 self.track_progress(peer, &outcome, &round, now)?;
336 let denied = matches!(
337 &outcome,
338 Ok(Turn::Retry(Error::DialDenied(DialLimit::Established)))
339 );
340 let result = self.judge(peer, outcome, now)?;
341 *peer.last_error.lock().map_err(|_| poisoned())? = RecordedError::from_result(&result);
342 Ok(denied)
343 }
344
345 fn observe_connections(&self, step: u64) -> Result<(), Error> {
347 for peer in self.snapshot()? {
348 let mut since = peer.connected_since.lock().map_err(|_| poisoned())?;
349 *since = match (peer.source.connected(), *since) {
350 (true, Some(first)) => Some(first),
351 (true, None) => Some(step),
352 (false, _gone) => None,
353 };
354 }
355 Ok(())
356 }
357
358 pub(crate) fn backed_off(&self, now: u64) -> Result<Vec<String>, Error> {
360 let mut skipped = Vec::new();
361 for peer in self.snapshot()? {
362 if peer.backoff_until(now)?.is_some() {
363 skipped.push(peer.id.clone());
364 }
365 }
366 Ok(skipped)
367 }
368
369 fn track_progress(
374 &self,
375 peer: &Peer,
376 outcome: &Result<Turn, Error>,
377 round: &Round,
378 now: u64,
379 ) -> Result<(), Error> {
380 if round.progressed {
381 peer.idle_turns.store(0, Ordering::Relaxed);
382 *peer.last_progress.lock().map_err(|_| poisoned())? = Some(now);
383 return Ok(());
384 }
385 if !round.contacted || !matches!(outcome, Ok(Turn::Quiet)) {
386 return Ok(());
387 }
388 let idle = peer.idle_turns.fetch_add(1, Ordering::Relaxed) + 1;
389 if idle < Self::IDLE_TURNS {
390 return Ok(());
391 }
392 peer.idle_turns.store(0, Ordering::Relaxed);
393 peer.source.disconnect();
394 peer.hold_off(now, Self::IDLE_BACKOFF)
395 }
396
397 fn free_slot_for(
407 &self,
408 starved: &Peer,
409 step: u64,
410 stop: &AtomicBool,
411 deadline: Instant,
412 now: u64,
413 ) -> Result<bool, Error> {
414 let mut oldest: Option<(Arc<Peer>, Option<u64>)> = None;
415 for peer in self.snapshot()? {
416 if peer.id == starved.id || !evictable(&peer, step, now)? {
417 continue;
418 }
419 let progress = peer.last_progress()?;
420 if oldest.as_ref().is_none_or(|(_, best)| progress < *best) {
421 oldest = Some((peer, progress));
422 }
423 }
424 let Some((victim, _progress)) = oldest else {
425 return Ok(false);
426 };
427 victim.source.disconnect();
428 victim.hold_off(now, Self::IDLE_BACKOFF)?;
429 Ok(victim.wait_closed(stop, deadline))
430 }
431
432 fn judge(
436 &self,
437 peer: &Peer,
438 outcome: Result<Turn, Error>,
439 now: u64,
440 ) -> Result<Result<(), Error>, Error> {
441 match outcome {
442 Ok(Turn::Quiet) => {
443 peer.busy_turns.store(0, Ordering::Relaxed);
444 peer.backoff.lock().map_err(|_| poisoned())?.failures = 0;
445 Ok(Ok(()))
446 }
447 Ok(Turn::Busy(error)) => {
448 let streak = peer.busy_turns.fetch_add(1, Ordering::Relaxed) + 1;
449 if streak < Self::BUSY_TURNS {
450 return Ok(Err(error));
451 }
452 peer.busy_turns.store(0, Ordering::Relaxed);
453 peer.source.disconnect();
454 Ok(Err(Error::Busy(format!(
455 "{error}; busy for {streak} turns, disconnected"
456 ))))
457 }
458 Ok(Turn::Blamed(error)) => {
459 peer.busy_turns.store(0, Ordering::Relaxed);
460 self.penalize(peer, now)?;
461 Ok(Err(error))
462 }
463 Ok(Turn::Retry(error)) | Err(error) => Ok(Err(error)),
464 }
465 }
466
467 fn penalize(&self, peer: &Peer, now: u64) -> Result<(), Error> {
468 peer.source.disconnect();
469 let mut backoff = peer.backoff.lock().map_err(|_| poisoned())?;
470 backoff.failures = backoff.failures.checked_add(1).unwrap_or(u32::MAX);
471 let delay = 2u64
472 .checked_pow(backoff.failures - 1)
473 .and_then(|factor| factor.checked_mul(Self::BACKOFF_SECONDS))
474 .map_or(Self::BACKOFF_CAP, |delay| delay.min(Self::BACKOFF_CAP));
475 backoff.until = Some(now.checked_add(delay).unwrap_or(u64::MAX));
476 drop(backoff);
477 let mut penalized = self.penalized.lock().map_err(|_| poisoned())?;
478 if !penalized.contains(&peer.peer_id) {
479 penalized.push(peer.peer_id);
480 }
481 Ok(())
482 }
483
484 pub(crate) fn drain_penalized(&self) -> Result<Vec<PeerId>, Error> {
486 let mut penalized = self.penalized.lock().map_err(|_| poisoned())?;
487 Ok(std::mem::take(&mut *penalized))
488 }
489
490 fn refresh(&self, offered: Vec<(String, PeerSource)>, now: u64) -> Result<(), Error> {
495 if offered.len() > Self::MAX_PEERS {
496 return Err(Error::Invalid("too many inventory peers".into()));
497 }
498 let mut peers = self.peers.lock().map_err(|_| poisoned())?;
499 let mut next: Vec<Arc<Peer>> = Vec::with_capacity(offered.len());
500 for (label, source) in offered {
501 let candidate = inventory_peer(label, source)?;
502 match peers.iter().find(|known| known.id == candidate.id) {
503 Some(known) => next.push(Arc::clone(known)),
504 None => next.push(Arc::new(candidate)),
505 }
506 }
507 let mut held = Vec::new();
508 for known in peers.iter() {
509 if !next.iter().any(|kept| kept.id == known.id) && known.backoff_until(now)?.is_some() {
510 held.push(Arc::clone(known));
511 }
512 }
513 let room = Self::MAX_PEERS.saturating_sub(next.len());
515 next.extend(held.split_off(held.len().saturating_sub(room)));
516 *peers = next;
517 Ok(())
518 }
519
520 fn snapshot(&self) -> Result<Vec<Arc<Peer>>, Error> {
521 Ok(self.peers.lock().map_err(|_| poisoned())?.clone())
522 }
523
524 fn next_peer(&self, omitting: &[String], now: u64) -> Result<Option<Arc<Peer>>, Error> {
527 let peers = self.snapshot()?;
528 let (configured, live): (Vec<_>, Vec<_>) = peers.into_iter().partition(configured);
529 let mut groups = [(&configured, &self.configured_next), (&live, &self.turn)];
530 if self.configured_turn.fetch_xor(true, Ordering::Relaxed) {
531 groups.reverse();
532 }
533 for (peers, turn) in groups {
534 if let Some(peer) = rotate(peers, turn, omitting, now)? {
535 return Ok(Some(peer));
536 }
537 }
538 Ok(None)
539 }
540
541 fn advance(&self, peer: &Peer, engine: &Engine, round: &mut Round) -> Result<Turn, Error> {
542 let mut slot = peer.range.lock().map_err(|_| poisoned())?;
543 if slot.is_none() {
544 match open(&peer.id, engine, peer.floor.load(Ordering::Relaxed))? {
545 Opened::Range(range) => *slot = Some(range),
546 Opened::Synced => round.progressed = true,
549 Opened::AboveTip => (),
551 }
552 }
553 match self.walk(peer, engine, &mut slot, round) {
556 Ok(()) | Err(Pause::Done) => Ok(Turn::Quiet),
557 Err(Pause::Abandon) | Err(Pause::Error(Error::Stale)) => {
558 *slot = None;
559 Ok(Turn::Quiet)
560 }
561 Err(Pause::Error(Error::PeerBusy)) => Ok(Turn::Busy(Error::PeerBusy)),
562 Err(Pause::Error(
563 retry @ (Error::Busy(_) | Error::Transport(_) | Error::DialDenied(_)),
564 )) => Ok(Turn::Retry(retry)),
565 Err(Pause::Blame(error)) => {
566 *slot = None;
567 Ok(Turn::Blamed(error))
568 }
569 Err(Pause::Error(error)) => {
570 *slot = None;
571 Err(error)
572 }
573 }
574 }
575
576 fn walk(
577 &self,
578 peer: &Peer,
579 engine: &Engine,
580 slot: &mut Option<Range>,
581 round: &mut Round,
582 ) -> Result<(), Pause> {
583 while let Some(range) = slot.as_mut() {
584 round.open()?;
585 if let Some(&entry) = range.entries.get(range.next) {
586 if !selected(engine, &entry)? {
587 return Err(Pause::Abandon);
588 }
589 if engine.record(entry.txid)?.is_none() {
590 if round.fetched == Self::ROUND_FETCHES {
591 return Err(Pause::Done);
592 }
593 round.fetched += 1;
594 pace(peer, round)?;
595 let missing = &mut range.missing;
596 round.progressed |=
597 self.acquire(peer, engine, &entry, missing, round.deadline)?;
598 } else {
599 round.progressed = true;
600 }
601 range.next += 1;
602 } else if range.more {
603 if round.requested_page {
604 return Err(Pause::Done);
605 }
606 round.requested_page = true;
607 pace(peer, round)?;
608 let Some(page) = peer
609 .source
610 .inventory_until(
611 (range.from, range.to),
612 range.after,
613 Self::PAGE_ENTRIES,
614 round.stop,
615 round.deadline,
616 )
617 .map_err(blame)?
618 else {
619 return Err(Pause::Done);
620 };
621 match accept(range, page).map_err(Pause::Blame)? {
622 Accepted::Walk => (),
623 Accepted::Unscanned => *slot = None,
624 Accepted::Floor(floor) => {
625 peer.floor.store(floor, Ordering::Relaxed);
626 *slot = open(&peer.id, engine, floor)?.range();
627 }
628 }
629 } else {
630 let through = range.to.min(range.scanned_through);
631 engine.record_peer_coverage(&peer.id, range.from, through, range.generation)?;
632 round.progressed = true;
633 *slot = None;
634 }
635 }
636 Ok(())
637 }
638
639 fn acquire(
645 &self,
646 peer: &Peer,
647 engine: &Engine,
648 entry: &Placement,
649 missing: &mut u32,
650 deadline: Instant,
651 ) -> Result<bool, Pause> {
652 let fetched = peer
653 .source
654 .fetch_until(entry.txid, deadline)
655 .map_err(blame)?;
656 let Some(package) = fetched else {
657 *missing += 1;
658 if *missing < Self::MISSING_ANSWERS {
659 return Ok(false);
660 }
661 return Err(Pause::Blame(Error::Invalid(
662 "peer reported advertised records missing".into(),
663 )));
664 };
665 *missing = 0;
666 let receipt = match engine.admit_record(package) {
667 Ok(receipt) => receipt,
668 Err(invalid @ Error::Invalid(_)) => {
669 self.rejected.fetch_add(1, Ordering::Relaxed);
670 return Err(Pause::Blame(invalid));
671 }
672 Err(local) => return Err(Pause::Error(local)),
673 };
674 self.admitted.fetch_add(1, Ordering::Relaxed);
675 if !placed(&receipt, entry) {
676 return Err(Pause::Blame(Error::Invalid(
677 "peer inventory placement disagrees with its proven record".into(),
678 )));
679 }
680 Ok(true)
681 }
682
683 pub(crate) fn status(&self, engine: &Engine) -> Result<OverlayStatus, Error> {
684 let start = engine.status()?.application_start.height;
685 let now = unix_seconds()?;
686 let known = self.snapshot()?;
687 let mut peers = Vec::with_capacity(known.len());
688 for peer in &known {
689 let cursor = engine.peer_cursor(&peer.id)?;
690 let synced_through = cursor.checked_sub(1).filter(|height| *height >= start);
691 let recorded = peer.last_error.lock().map_err(|_| poisoned())?.clone();
692 let (last_error, last_error_at) = RecordedError::split(recorded);
693 let mut status = OverlayPeerStatus::default();
694 status.label = peer.label.clone();
695 status.cursor = cursor;
696 status.synced_through = synced_through;
697 status.last_error = last_error;
698 status.last_error_at = last_error_at;
699 status.backoff_until = peer.backoff_until(now)?;
700 status.last_progress_at = peer.last_progress()?;
701 peers.push(status);
702 }
703 let mut status = OverlayStatus::default();
704 status.peer_id = self.info.peer_id.clone();
705 status.listen = (self.info.listen)()
706 .iter()
707 .map(Multiaddr::to_string)
708 .collect();
709 status.announce = self.info.announce.clone();
710 status.peer_book = self.info.peer_book;
711 status.connected = (self.info.connected)();
712 status.admitted = self.admitted.load(Ordering::Relaxed);
713 status.rejected = self.rejected.load(Ordering::Relaxed);
714 status.peers = peers;
715 Ok(status)
716 }
717}
718
719fn pace(peer: &Peer, round: &mut Round) -> Result<(), Pause> {
723 let mut last = peer.last_request.lock().map_err(|_| poisoned())?;
724 if let Some(previous) = *last {
725 let ready = previous + Scheduler::REQUEST_SPACING;
726 while Instant::now() < ready {
727 round.open()?;
728 let wait = ready.saturating_duration_since(Instant::now());
729 std::thread::sleep(wait.min(Duration::from_millis(50)));
730 }
731 }
732 round.open()?;
733 *last = Some(Instant::now());
734 round.contacted = true;
735 Ok(())
736}