use std::{
collections::{BTreeMap, BTreeSet, HashMap},
sync::Mutex as StdMutex,
time::{Duration, Instant},
};
use zakura_chain::block;
use super::{
config::{clamp_advertised_blocks, clamp_advertised_inflight, clamp_advertised_response_bytes},
state::EFFECTIVE_BS_OUTBOUND_INFLIGHT_PER_PEER,
BlockSyncStatus, ServicePeerDirection, ZakuraPeerId,
};
use crate::zakura::ZakuraConnId;
#[derive(Clone, Debug)]
pub(super) struct Entry {
pub(super) direction: ServicePeerDirection,
pub(super) servable_low: block::Height,
pub(super) servable_high: block::Height,
pub(super) received_status: bool,
pub(super) max_blocks_per_response: u32,
pub(super) max_inflight_requests: u32,
pub(super) max_response_bytes: u32,
pub(super) outstanding: BTreeMap<block::Height, OutstandingMeta>,
pub(super) slots: SlotDiagnostics,
pub(super) floor_watchdog_avoid: BTreeMap<block::Height, Instant>,
pub(super) generation: u64,
pub(super) conn_id: Option<ZakuraConnId>,
}
impl Entry {
fn new(
direction: ServicePeerDirection,
config: &super::ZakuraBlockSyncConfig,
generation: u64,
) -> Self {
Self {
direction,
servable_low: block::Height::MIN,
servable_high: block::Height::MIN,
received_status: false,
max_blocks_per_response: config.advertised_max_blocks_per_response(),
max_inflight_requests: config.advertised_max_inflight_requests(),
max_response_bytes: config.advertised_max_response_bytes(),
outstanding: BTreeMap::new(),
slots: SlotDiagnostics::default(),
floor_watchdog_avoid: BTreeMap::new(),
generation,
conn_id: None,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct SessionPark {
conn_id: Option<ZakuraConnId>,
deadline: Instant,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum SessionAdmission {
Parked,
Readmitted { generation: u64 },
Fresh { generation: u64 },
}
impl SessionAdmission {
#[cfg(test)]
pub(super) fn generation(self) -> u64 {
match self {
SessionAdmission::Parked => panic!("admission was refused by an active park"),
SessionAdmission::Readmitted { generation }
| SessionAdmission::Fresh { generation } => generation,
}
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
struct BodyRetryKey {
header_generation: zakura_header_chain::HeaderGeneration,
branch: zakura_header_chain::BranchId,
body_work_epoch: zakura_header_chain::BodyWorkEpoch,
hash: block::Hash,
}
impl BodyRetryKey {
fn new(scope: zakura_header_chain::BodyWorkAuthority, hash: block::Hash) -> Self {
Self {
header_generation: scope.header_generation,
branch: scope.branch,
body_work_epoch: scope.body_work_epoch,
hash,
}
}
}
pub(super) fn retry_deadline_instant(deadline: chrono::DateTime<chrono::Utc>) -> Instant {
let monotonic_now = Instant::now();
let delay = deadline
.signed_duration_since(chrono::Utc::now())
.to_std()
.unwrap_or(Duration::ZERO);
monotonic_now
.checked_add(delay)
.unwrap_or_else(|| monotonic_now + Duration::from_secs(10 * 60))
}
#[derive(Copy, Clone, Debug, Default)]
pub(super) struct SlotDiagnostics {
pub(super) hard_capacity: usize,
pub(super) effective_window: usize,
pub(super) available_slots: usize,
pub(super) outstanding_requests: usize,
pub(super) bbr_rtprop_ms: Option<u64>,
}
#[derive(Copy, Clone, Debug)]
pub(super) struct OutstandingMeta {
pub(super) owner: zakura_header_chain::BodyWorkOwner,
pub(super) hash: block::Hash,
pub(super) estimated_bytes: u64,
pub(super) queued_at: Instant,
pub(super) deadline: Instant,
}
#[derive(Clone, Debug)]
pub(super) struct OutstandingClaim {
pub(super) peer: ZakuraPeerId,
pub(super) height: block::Height,
pub(super) meta: OutstandingMeta,
}
#[derive(Debug)]
pub(super) struct PeerRegistry {
peers: StdMutex<HashMap<ZakuraPeerId, Entry>>,
session_parks: StdMutex<HashMap<ZakuraPeerId, SessionPark>>,
body_retry_avoid: StdMutex<HashMap<(zakura_header_chain::SourceId, BodyRetryKey), Instant>>,
body_retry_all: StdMutex<HashMap<BodyRetryKey, Instant>>,
next_generation: std::sync::atomic::AtomicU64,
}
impl Default for PeerRegistry {
fn default() -> Self {
Self::new()
}
}
impl PeerRegistry {
pub(super) fn new() -> Self {
Self {
peers: StdMutex::new(HashMap::new()),
session_parks: StdMutex::new(HashMap::new()),
body_retry_avoid: StdMutex::new(HashMap::new()),
body_retry_all: StdMutex::new(HashMap::new()),
next_generation: std::sync::atomic::AtomicU64::new(1),
}
}
pub(super) fn eligible_sources(
&self,
height: block::Height,
) -> BTreeSet<zakura_header_chain::SourceId> {
self.lock()
.iter()
.filter(|(_, entry)| {
entry.received_status
&& entry.servable_low <= height
&& height <= entry.servable_high
})
.map(|(peer, _)| zakura_header_chain::SourceId::from_digest(peer.digest()))
.collect()
}
pub(super) fn defer_body_retry(
&self,
sources: impl IntoIterator<Item = zakura_header_chain::SourceId>,
scope: zakura_header_chain::BodyWorkAuthority,
hash: block::Hash,
until: Instant,
) {
let key = BodyRetryKey::new(scope, hash);
let sources: std::collections::BTreeSet<_> = sources.into_iter().collect();
let mut retries = self.body_retry_lock();
retries.retain(|(source, candidate), _| *candidate != key || sources.contains(source));
for source in sources {
retries.insert((source, key), until);
}
}
pub(super) fn set_persisted_body_alarm(
&self,
alarm: Option<(zakura_header_chain::BodyWorkAuthority, block::Hash, Instant)>,
) {
let mut retries = self.body_retry_all_lock();
retries.clear();
if let Some((scope, hash, until)) = alarm {
retries.insert(BodyRetryKey::new(scope, hash), until);
}
}
pub(super) fn clear_body_retry(
&self,
scope: zakura_header_chain::BodyWorkAuthority,
hash: block::Hash,
) {
let key = BodyRetryKey::new(scope, hash);
self.body_retry_lock()
.retain(|(_, candidate), _| *candidate != key);
self.body_retry_all_lock().remove(&key);
}
pub(super) fn retain_body_retry_scope(
&self,
current: Option<zakura_header_chain::BodyWorkAuthority>,
) {
self.body_retry_lock().retain(|(_, key), _| {
current.is_some_and(|scope| {
key.header_generation == scope.header_generation
&& key.branch == scope.branch
&& key.body_work_epoch == scope.body_work_epoch
})
});
self.body_retry_all_lock().retain(|key, _| {
current.is_some_and(|scope| {
key.header_generation == scope.header_generation
&& key.branch == scope.branch
&& key.body_work_epoch == scope.body_work_epoch
})
});
}
pub(super) fn refresh_body_retry_scope(&self, current: zakura_header_chain::BodyWorkAuthority) {
let mut all_retries = self.body_retry_all_lock();
let mut retries = self.body_retry_lock();
*all_retries = std::mem::take(&mut *all_retries)
.into_iter()
.map(|(mut key, deadline)| {
key.header_generation = current.header_generation;
key.branch = current.branch;
key.body_work_epoch = current.body_work_epoch;
(key, deadline)
})
.collect();
*retries = std::mem::take(&mut *retries)
.into_iter()
.map(|((source, mut key), deadline)| {
key.header_generation = current.header_generation;
key.branch = current.branch;
key.body_work_epoch = current.body_work_epoch;
((source, key), deadline)
})
.collect();
}
pub(super) fn is_body_retry_avoided(
&self,
peer: &ZakuraPeerId,
scope: zakura_header_chain::BodyWorkAuthority,
hash: block::Hash,
now: Instant,
) -> bool {
let key = BodyRetryKey::new(scope, hash);
let source = zakura_header_chain::SourceId::from_digest(peer.digest());
let mut all_retries = self.body_retry_all_lock();
all_retries.retain(|_, until| *until > now);
if all_retries.get(&key).is_some_and(|until| *until > now) {
return true;
}
let mut retries = self.body_retry_lock();
retries.retain(|_, until| *until > now);
retries
.get(&(source, key))
.is_some_and(|until| *until > now)
}
#[cfg(test)]
fn has_live_body_retry_deadline(
&self,
peer: &ZakuraPeerId,
hash: block::Hash,
now: Instant,
) -> bool {
let source = zakura_header_chain::SourceId::from_digest(peer.digest());
let mut all_retries = self.body_retry_all_lock();
all_retries.retain(|_, until| *until > now);
if all_retries.keys().any(|key| key.hash == hash) {
return true;
}
let mut retries = self.body_retry_lock();
retries.retain(|_, until| *until > now);
retries
.keys()
.any(|(candidate, key)| *candidate == source && key.hash == hash)
}
pub(super) fn next_body_retry_deadline(
&self,
peer: &ZakuraPeerId,
now: Instant,
) -> Option<Instant> {
let source = zakura_header_chain::SourceId::from_digest(peer.digest());
let all_deadline = {
let mut retries = self.body_retry_all_lock();
retries.retain(|_, until| *until > now);
retries.values().copied().min()
};
let mut retries = self.body_retry_lock();
retries.retain(|_, until| *until > now);
let source_deadline = retries
.iter()
.filter_map(|((candidate, _), until)| (*candidate == source).then_some(*until))
.min();
all_deadline.into_iter().chain(source_deadline).min()
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<ZakuraPeerId, Entry>> {
self.peers
.lock()
.expect("peer registry mutex is never poisoned")
}
fn body_retry_lock(
&self,
) -> std::sync::MutexGuard<'_, HashMap<(zakura_header_chain::SourceId, BodyRetryKey), Instant>>
{
self.body_retry_avoid
.lock()
.expect("body retry registry mutex is never poisoned")
}
fn body_retry_all_lock(&self) -> std::sync::MutexGuard<'_, HashMap<BodyRetryKey, Instant>> {
self.body_retry_all
.lock()
.expect("global body retry registry mutex is never poisoned")
}
fn lock_session_parks(&self) -> std::sync::MutexGuard<'_, HashMap<ZakuraPeerId, SessionPark>> {
self.session_parks
.lock()
.expect("peer registry session-park mutex is never poisoned")
}
pub(super) fn park_session(
&self,
peer: &ZakuraPeerId,
conn_id: ZakuraConnId,
generation: u64,
deadline: Instant,
) -> bool {
let peers = self.lock();
if peers
.get(peer)
.is_none_or(|entry| entry.generation != generation || entry.conn_id != Some(conn_id))
{
return false;
}
self.lock_session_parks().insert(
peer.clone(),
SessionPark {
conn_id: Some(conn_id),
deadline,
},
);
true
}
#[cfg(test)]
pub(super) fn park_peer_until(&self, peer: &ZakuraPeerId, deadline: Instant) {
self.lock_session_parks().insert(
peer.clone(),
SessionPark {
conn_id: None,
deadline,
},
);
}
#[cfg(test)]
pub(super) fn park_session_for_test(
&self,
peer: &ZakuraPeerId,
conn_id: ZakuraConnId,
deadline: Instant,
) {
self.lock_session_parks().insert(
peer.clone(),
SessionPark {
conn_id: Some(conn_id),
deadline,
},
);
}
pub(super) fn peer_park_deadline(&self, peer: &ZakuraPeerId, now: Instant) -> Option<Instant> {
let mut session_parks = self.lock_session_parks();
session_parks.retain(|_, park| park.deadline > now || park.conn_id.is_some());
session_parks
.get(peer)
.filter(|park| park.deadline > now)
.map(|park| park.deadline)
}
pub(super) fn is_peer_parked(&self, peer: &ZakuraPeerId, now: Instant) -> bool {
self.peer_park_deadline(peer, now).is_some()
}
pub(super) fn has_expired_session_park(
&self,
peer: &ZakuraPeerId,
conn_id: ZakuraConnId,
now: Instant,
) -> bool {
self.lock_session_parks()
.get(peer)
.is_some_and(|park| park.conn_id == Some(conn_id) && park.deadline <= now)
}
pub(super) fn connection_closed(
&self,
peer: &ZakuraPeerId,
conn_id: ZakuraConnId,
now: Instant,
) {
let mut peers = self.lock();
let mut session_parks = self.lock_session_parks();
if let Some(entry) = peers.get_mut(peer) {
if entry.conn_id == Some(conn_id) {
entry.conn_id = None;
}
}
let Some(park) = session_parks.get_mut(peer) else {
return;
};
if park.conn_id != Some(conn_id) {
return;
}
if park.deadline <= now {
session_parks.remove(peer);
} else {
park.conn_id = None;
}
}
pub(super) fn admit_session(
&self,
peer: &ZakuraPeerId,
direction: ServicePeerDirection,
config: &super::ZakuraBlockSyncConfig,
conn_id: ZakuraConnId,
now: Instant,
) -> SessionAdmission {
let mut peers = self.lock();
let mut session_parks = self.lock_session_parks();
if session_parks
.get(peer)
.is_some_and(|park| park.deadline > now)
{
return SessionAdmission::Parked;
}
#[allow(deprecated)]
let generation = self
.next_generation
.fetch_update(
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
|generation| generation.checked_add(1),
)
.unwrap_or_else(|_| panic!("block-sync routine generation counter is exhausted"));
peers
.entry(peer.clone())
.and_modify(|entry| {
entry.direction = direction;
entry.outstanding.clear();
entry.floor_watchdog_avoid.clear();
entry.generation = generation;
entry.conn_id = Some(conn_id);
})
.or_insert_with(|| Entry {
conn_id: Some(conn_id),
..Entry::new(direction, config, generation)
});
let readmitted = session_parks
.remove(peer)
.is_some_and(|park| park.conn_id == Some(conn_id));
if readmitted {
SessionAdmission::Readmitted { generation }
} else {
SessionAdmission::Fresh { generation }
}
}
pub(super) fn remove(&self, peer: &ZakuraPeerId) {
self.lock().remove(peer);
}
pub(super) fn upsert_status(
&self,
peer: &ZakuraPeerId,
generation: u64,
status: BlockSyncStatus,
) {
let mut peers = self.lock();
let Some(entry) = peers.get_mut(peer) else {
return;
};
if entry.generation != generation {
return;
}
entry.servable_low = status.servable_low;
entry.servable_high = status.servable_high;
entry.max_blocks_per_response = clamp_advertised_blocks(status.max_blocks_per_response);
entry.max_inflight_requests = clamp_advertised_inflight(status.max_inflight_requests);
entry.max_response_bytes = clamp_advertised_response_bytes(status.max_response_bytes);
entry.received_status = true;
}
pub(super) fn set_outstanding(
&self,
peer: &ZakuraPeerId,
generation: u64,
outstanding: BTreeMap<block::Height, OutstandingMeta>,
) {
let mut peers = self.lock();
if let Some(entry) = peers.get_mut(peer) {
if entry.generation == generation {
entry.outstanding = outstanding;
}
}
}
pub(super) fn clear_outstanding(&self, peer: &ZakuraPeerId, generation: u64) {
let mut peers = self.lock();
if let Some(entry) = peers.get_mut(peer) {
if entry.generation == generation {
entry.outstanding.clear();
}
}
}
pub(super) fn publish_slots(
&self,
peer: &ZakuraPeerId,
generation: u64,
slots: SlotDiagnostics,
) {
let mut peers = self.lock();
if let Some(entry) = peers.get_mut(peer) {
if entry.generation == generation {
entry.slots = slots;
}
}
}
pub(super) fn slot_summary(&self) -> SlotSummary {
let peers = self.lock();
let mut summary = SlotSummary::default();
for entry in peers.values() {
summary.outstanding_requests = summary
.outstanding_requests
.saturating_add(entry.slots.outstanding_requests);
if !entry.received_status {
continue;
}
summary.capacity = summary.capacity.saturating_add(entry.slots.hard_capacity);
summary.effective_window = summary
.effective_window
.saturating_add(entry.slots.effective_window);
summary.available = summary
.available
.saturating_add(entry.slots.available_slots);
if entry.slots.available_slots == 0 {
summary.saturated_peers = summary.saturated_peers.saturating_add(1);
}
}
summary
}
pub(super) fn has_outstanding_request(&self, height: block::Height, hash: block::Hash) -> bool {
let peers = self.lock();
peers.values().any(|entry| {
entry
.outstanding
.get(&height)
.is_some_and(|meta| meta.hash == hash)
})
}
pub(super) fn has_outstanding_height(&self, height: block::Height) -> bool {
let peers = self.lock();
peers
.values()
.any(|entry| entry.outstanding.contains_key(&height))
}
pub(super) fn peer_has_outstanding_height(
&self,
peer: &ZakuraPeerId,
height: block::Height,
) -> bool {
let peers = self.lock();
peers
.get(peer)
.is_some_and(|entry| entry.outstanding.contains_key(&height))
}
pub(super) fn total_unreceived(&self) -> usize {
let peers = self.lock();
peers.values().map(|entry| entry.outstanding.len()).sum()
}
pub(super) fn any_outstanding_at_or_above(&self, at_or_above: block::Height) -> bool {
let peers = self.lock();
peers.values().any(|entry| {
entry
.outstanding
.keys()
.any(|height| *height >= at_or_above)
})
}
pub(super) fn any_outstanding_conflicts_at(
&self,
height: block::Height,
hash: block::Hash,
) -> bool {
let peers = self.lock();
peers.values().any(|entry| {
entry
.outstanding
.get(&height)
.is_some_and(|expected| expected.hash != hash)
})
}
pub(super) fn has_received_status(&self, peer: &ZakuraPeerId) -> bool {
let peers = self.lock();
peers.get(peer).is_some_and(|entry| entry.received_status)
}
pub(super) fn peers_with_status(&self) -> usize {
let peers = self.lock();
peers.values().filter(|entry| entry.received_status).count()
}
pub(super) fn candidate_snapshot(
&self,
) -> Vec<(ZakuraPeerId, bool, block::Height, block::Height)> {
let peers = self.lock();
peers
.iter()
.map(|(peer, entry)| {
(
peer.clone(),
entry.received_status,
entry.servable_low,
entry.servable_high,
)
})
.collect()
}
pub(super) fn direction_status_counts(&self) -> DirectionStatusCounts {
let peers = self.lock();
let mut counts = DirectionStatusCounts::default();
for entry in peers.values() {
match entry.direction {
ServicePeerDirection::Inbound => {
counts.inbound += 1;
if entry.received_status {
counts.inbound_with_status += 1;
}
}
ServicePeerDirection::Outbound => {
counts.outbound += 1;
if entry.received_status {
counts.outbound_with_status += 1;
}
}
}
}
counts
}
pub(super) fn floor_gap_servable(&self, height: block::Height) -> (usize, usize) {
let peers = self.lock();
let mut servable = 0usize;
let mut outstanding = 0usize;
for entry in peers.values() {
if entry.received_status
&& entry.servable_low <= height
&& height <= entry.servable_high
{
servable = servable.saturating_add(1);
}
if entry.outstanding.contains_key(&height) {
outstanding = outstanding.saturating_add(1);
}
}
(servable, outstanding)
}
pub(super) fn earliest_outstanding_deadline_at(
&self,
height: block::Height,
) -> Option<Instant> {
let peers = self.lock();
peers
.values()
.filter_map(|entry| entry.outstanding.get(&height).map(|meta| meta.deadline))
.min()
}
pub(super) fn floor_has_preferred_unsaturated_server(
&self,
height: block::Height,
self_peer: &ZakuraPeerId,
self_rtprop_ms: Option<u64>,
allow_equal_score: bool,
) -> bool {
let self_score = self_rtprop_ms.unwrap_or(u64::MAX);
let peers = self.lock();
peers.iter().any(|(peer, entry)| {
if peer == self_peer || !entry.can_serve_with_room(height) {
return false;
}
let other_score = entry.slots.bbr_rtprop_ms.unwrap_or(u64::MAX);
if allow_equal_score {
other_score <= self_score
} else {
other_score < self_score
}
})
}
pub(super) fn outstanding_claims_at(&self, height: block::Height) -> Vec<OutstandingClaim> {
let peers = self.lock();
peers
.iter()
.filter_map(|(peer, entry)| {
entry.outstanding.get(&height).map(|meta| OutstandingClaim {
peer: peer.clone(),
height,
meta: *meta,
})
})
.collect()
}
pub(super) fn clear_outstanding_height_for_owner(
&self,
peer: &ZakuraPeerId,
height: block::Height,
owner: zakura_header_chain::BodyWorkOwner,
) -> bool {
let mut peers = self.lock();
let Some(entry) = peers.get_mut(peer) else {
return false;
};
if entry.outstanding.get(&height).map(|meta| meta.owner) != Some(owner) {
return false;
}
entry.outstanding.remove(&height);
true
}
pub(super) fn avoid_floor_height_until(
&self,
peer: &ZakuraPeerId,
height: block::Height,
until: Instant,
) {
let mut peers = self.lock();
if let Some(entry) = peers.get_mut(peer) {
entry.floor_watchdog_avoid.insert(height, until);
}
}
pub(super) fn is_floor_height_avoided(
&self,
peer: &ZakuraPeerId,
height: block::Height,
now: Instant,
) -> bool {
let mut peers = self.lock();
let Some(entry) = peers.get_mut(peer) else {
return false;
};
entry.floor_watchdog_avoid.retain(|_, until| *until > now);
entry
.floor_watchdog_avoid
.get(&height)
.is_some_and(|until| *until > now)
}
pub(super) fn next_floor_avoid_deadline(
&self,
peer: &ZakuraPeerId,
now: Instant,
) -> Option<Instant> {
let mut peers = self.lock();
let entry = peers.get_mut(peer)?;
entry.floor_watchdog_avoid.retain(|_, until| *until > now);
entry.floor_watchdog_avoid.values().min().copied()
}
}
impl Entry {
fn can_serve_with_room(&self, height: block::Height) -> bool {
self.received_status
&& self.servable_low <= height
&& height <= self.servable_high
&& self.slots.available_slots > 0
}
}
#[derive(Copy, Clone, Debug, Default)]
pub(super) struct SlotSummary {
pub(super) capacity: usize,
pub(super) effective_window: usize,
pub(super) available: usize,
pub(super) saturated_peers: usize,
pub(super) outstanding_requests: usize,
}
#[derive(Copy, Clone, Debug, Default)]
pub(super) struct DirectionStatusCounts {
pub(super) inbound: usize,
pub(super) outbound: usize,
pub(super) inbound_with_status: usize,
pub(super) outbound_with_status: usize,
}
pub(super) fn hard_outbound_capacity(max_inflight_requests: u32) -> usize {
usize::try_from(max_inflight_requests)
.expect("u32 max inflight requests fits in usize on supported targets")
.min(EFFECTIVE_BS_OUTBOUND_INFLIGHT_PER_PEER)
}
#[cfg(test)]
mod floor_bias_tests {
use super::*;
fn peer(byte: u8) -> ZakuraPeerId {
ZakuraPeerId::new(vec![byte; 32]).expect("32-byte test peer id is valid")
}
fn register_with_rtprop(
reg: &PeerRegistry,
config: &super::super::ZakuraBlockSyncConfig,
peer: &ZakuraPeerId,
low: u32,
high: u32,
available: usize,
bbr_rtprop_ms: Option<u64>,
) {
let generation = reg
.admit_session(
peer,
ServicePeerDirection::Outbound,
config,
0,
Instant::now(),
)
.generation();
reg.upsert_status(
peer,
generation,
BlockSyncStatus {
servable_low: block::Height(low),
servable_high: block::Height(high),
..BlockSyncStatus::default()
},
);
reg.publish_slots(
peer,
generation,
SlotDiagnostics {
available_slots: available,
bbr_rtprop_ms,
..SlotDiagnostics::default()
},
);
}
fn register(
reg: &PeerRegistry,
config: &super::super::ZakuraBlockSyncConfig,
peer: &ZakuraPeerId,
low: u32,
high: u32,
available: usize,
) {
register_with_rtprop(reg, config, peer, low, high, available, None);
}
#[test]
fn body_retry_backoff_is_exact_and_supplier_local() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let (failed, alternate) = (peer(1), peer(2));
register(®, &config, &failed, 1, 1, 1);
register(®, &config, &alternate, 1, 1, 1);
let scope = super::super::test_work_scope();
let hash = block::Hash([8; 32]);
let now = Instant::now();
let until = now + std::time::Duration::from_secs(1);
reg.defer_body_retry(
[zakura_header_chain::SourceId::from_digest([1; 32])],
scope,
hash,
until,
);
assert!(reg.is_body_retry_avoided(&failed, scope, hash, now));
assert!(!reg.is_body_retry_avoided(&alternate, scope, hash, now));
reg.remove(&failed);
register(®, &config, &failed, 1, 1, 1);
assert!(
reg.is_body_retry_avoided(&failed, scope, hash, now),
"reconnecting the same supplier must not bypass its retry deadline"
);
assert!(!reg.is_body_retry_avoided(&failed, scope, block::Hash([9; 32]), now));
assert_eq!(reg.next_body_retry_deadline(&failed, now), Some(until));
assert!(!reg.is_body_retry_avoided(
&failed,
scope,
hash,
until + std::time::Duration::from_millis(1)
));
reg.defer_body_retry(
[zakura_header_chain::SourceId::from_digest([1; 32])],
scope,
hash,
until,
);
reg.retain_body_retry_scope(Some(zakura_header_chain::BodyWorkAuthority {
header: zakura_header_chain::HeaderWorkAuthority {
header_generation: zakura_header_chain::HeaderGeneration::new(10),
..scope.header
},
..scope
}));
assert!(!reg.is_body_retry_avoided(&failed, scope, hash, now));
}
#[test]
fn refreshing_the_retry_scope_rekeys_both_maps_without_a_suppression_gap() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = std::sync::Arc::new(PeerRegistry::new());
let peer = peer(1);
register(®, &config, &peer, 1, 1, 1);
let scope = super::super::test_work_scope();
let hash = block::Hash([8; 32]);
let now = Instant::now();
let until = now + std::time::Duration::from_secs(60);
let refreshed = zakura_header_chain::BodyWorkAuthority {
header: zakura_header_chain::HeaderWorkAuthority {
header_generation: zakura_header_chain::HeaderGeneration::new(10),
branch: zakura_header_chain::BranchId::new(
scope.branch.anchor_hash,
block::Hash([7; 32]),
),
},
..scope
};
reg.defer_body_retry(
[zakura_header_chain::SourceId::from_digest([1; 32])],
scope,
hash,
until,
);
reg.set_persisted_body_alarm(Some((scope, hash, until)));
reg.refresh_body_retry_scope(refreshed);
assert!(
reg.is_body_retry_avoided(&peer, refreshed, hash, now),
"a compatible refresh must carry every deadline to the new authority"
);
assert!(
!reg.is_body_retry_avoided(&peer, scope, hash, now),
"the pre-refresh authority no longer keys a live deadline"
);
assert_eq!(reg.next_body_retry_deadline(&peer, now), Some(until));
for phase in ["per supplier", "all suppliers"] {
reg.clear_body_retry(refreshed, hash);
if phase == "per supplier" {
reg.defer_body_retry(
[zakura_header_chain::SourceId::from_digest(peer.digest())],
scope,
hash,
until,
);
} else {
reg.set_persisted_body_alarm(Some((scope, hash, until)));
}
assert!(reg.is_body_retry_avoided(&peer, scope, hash, now));
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let reader = std::thread::spawn({
let reg = std::sync::Arc::clone(®);
let stop = std::sync::Arc::clone(&stop);
let peer = peer.clone();
move || {
while !stop.load(std::sync::atomic::Ordering::Relaxed) {
assert!(
reg.has_live_body_retry_deadline(&peer, hash, now),
"a concurrent rekey must never expose an unsuppressed body \
through the {phase} map"
);
}
}
});
for round in 0..20_000 {
let current = if round % 2 == 0 { refreshed } else { scope };
reg.refresh_body_retry_scope(current);
}
stop.store(true, std::sync::atomic::Ordering::Relaxed);
reader.join().expect("the reader thread observes no gap");
}
}
#[test]
fn refreshing_retry_suppliers_removes_only_departed_supplier_deferrals() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let (first, second) = (peer(1), peer(2));
register(®, &config, &first, 1, 1, 1);
register(®, &config, &second, 1, 1, 1);
let scope = super::super::test_work_scope();
let hash = block::Hash([8; 32]);
let now = Instant::now();
let until = now + std::time::Duration::from_secs(60);
reg.defer_body_retry(
[zakura_header_chain::SourceId::from_digest([1; 32])],
scope,
hash,
until,
);
reg.defer_body_retry(
[zakura_header_chain::SourceId::from_digest([2; 32])],
scope,
hash,
until,
);
assert!(
!reg.is_body_retry_avoided(&first, scope, hash, now),
"a departed supplier must not retain a stale per-supplier deferral"
);
assert!(reg.is_body_retry_avoided(&second, scope, hash, now));
reg.set_persisted_body_alarm(Some((scope, hash, until)));
assert!(
reg.is_body_retry_avoided(&first, scope, hash, now),
"refreshing supplier-specific deferrals must not reopen a durable alarm"
);
assert!(reg.is_body_retry_avoided(&second, scope, hash, now));
}
#[test]
fn persisted_body_alarm_is_exact_global_and_survives_reconnect() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let (first, second) = (peer(1), peer(2));
register(®, &config, &first, 1, 1, 1);
register(®, &config, &second, 1, 1, 1);
let scope = super::super::test_work_scope();
let hash = block::Hash([8; 32]);
let now = Instant::now();
let until = now + std::time::Duration::from_secs(60);
reg.set_persisted_body_alarm(Some((scope, hash, until)));
assert!(reg.is_body_retry_avoided(&first, scope, hash, now));
assert!(reg.is_body_retry_avoided(&second, scope, hash, now));
assert!(!reg.is_body_retry_avoided(&first, scope, block::Hash([9; 32]), now));
assert!(!reg.is_body_retry_avoided(
&first,
zakura_header_chain::BodyWorkAuthority {
header: zakura_header_chain::HeaderWorkAuthority {
header_generation: zakura_header_chain::HeaderGeneration::new(10),
..scope.header
},
..scope
},
hash,
now
));
assert_eq!(reg.next_body_retry_deadline(&first, now), Some(until));
reg.remove(&first);
register(®, &config, &first, 1, 1, 1);
assert!(reg.is_body_retry_avoided(&first, scope, hash, now));
assert!(!reg.is_body_retry_avoided(
&first,
scope,
hash,
until + std::time::Duration::from_millis(1)
));
reg.set_persisted_body_alarm(Some((scope, hash, until)));
reg.clear_body_retry(scope, hash);
assert!(!reg.is_body_retry_avoided(&first, scope, hash, now));
}
#[test]
fn bypass_defers_to_an_equal_or_faster_unsaturated_other_server() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let (a, b) = (peer(1), peer(2));
register_with_rtprop(®, &config, &a, 0, 1000, 0, Some(50));
register_with_rtprop(®, &config, &b, 0, 1000, 3, Some(50));
assert!(reg.floor_has_preferred_unsaturated_server(block::Height(100), &a, Some(50), true));
assert!(!reg.floor_has_preferred_unsaturated_server(
block::Height(100),
&b,
Some(50),
true
));
}
#[test]
fn normal_path_defers_only_to_a_strictly_faster_server() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let (slow, fast) = (peer(1), peer(2));
register_with_rtprop(®, &config, &slow, 0, 1000, 3, Some(120));
register_with_rtprop(®, &config, &fast, 0, 1000, 3, Some(40));
assert!(reg.floor_has_preferred_unsaturated_server(
block::Height(100),
&slow,
Some(120),
false
));
assert!(!reg.floor_has_preferred_unsaturated_server(
block::Height(100),
&fast,
Some(40),
false
));
}
#[test]
fn normal_path_keeps_equal_carriers_eligible() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let (a, b) = (peer(1), peer(2));
register_with_rtprop(®, &config, &a, 0, 1000, 3, Some(50));
register_with_rtprop(®, &config, &b, 0, 1000, 3, Some(50));
assert!(!reg.floor_has_preferred_unsaturated_server(
block::Height(100),
&a,
Some(50),
false
));
assert!(!reg.floor_has_preferred_unsaturated_server(
block::Height(100),
&b,
Some(50),
false
));
}
#[test]
fn saturated_fast_peer_does_not_defer_to_slower_unsaturated_peer() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let (fast, slow) = (peer(1), peer(2));
register_with_rtprop(®, &config, &fast, 0, 1000, 0, Some(40));
register_with_rtprop(®, &config, &slow, 0, 1000, 3, Some(120));
assert!(!reg.floor_has_preferred_unsaturated_server(
block::Height(100),
&fast,
Some(40),
true
));
}
#[test]
fn bypasses_when_every_server_is_saturated() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let (a, b) = (peer(1), peer(2));
register(®, &config, &a, 0, 1000, 0);
register(®, &config, &b, 0, 1000, 0);
assert!(!reg.floor_has_preferred_unsaturated_server(block::Height(100), &a, None, true));
}
#[test]
fn ignores_an_unsaturated_peer_that_cannot_serve_the_floor() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let (a, b) = (peer(1), peer(2));
register(®, &config, &a, 0, 1000, 0);
register(®, &config, &b, 500, 1000, 3);
assert!(!reg.floor_has_preferred_unsaturated_server(block::Height(100), &a, None, true));
}
#[test]
fn floor_avoid_deadline_prunes_expired_entries_and_returns_next_wake() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let peer = peer(1);
reg.admit_session(
&peer,
ServicePeerDirection::Outbound,
&config,
0,
Instant::now(),
);
let now = Instant::now();
reg.avoid_floor_height_until(
&peer,
block::Height(1),
now - std::time::Duration::from_secs(1),
);
reg.avoid_floor_height_until(
&peer,
block::Height(2),
now + std::time::Duration::from_secs(2),
);
reg.avoid_floor_height_until(
&peer,
block::Height(3),
now + std::time::Duration::from_secs(1),
);
assert_eq!(
reg.next_floor_avoid_deadline(&peer, now),
Some(now + std::time::Duration::from_secs(1)),
);
assert!(!reg.is_floor_height_avoided(&peer, block::Height(1), now));
assert!(reg.is_floor_height_avoided(&peer, block::Height(2), now));
}
#[test]
fn outstanding_cleanup_requires_the_exact_request_owner() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let peer = peer(1);
let generation = reg
.admit_session(
&peer,
ServicePeerDirection::Outbound,
&config,
0,
Instant::now(),
)
.generation();
let current_owner = super::super::test_work_owner();
let stale_owner = zakura_header_chain::BodyWorkOwner {
request_id: std::num::NonZeroU64::new(current_owner.request_id.get() + 1)
.expect("the incremented test request ID is nonzero"),
..current_owner
};
let height = block::Height(1);
reg.set_outstanding(
&peer,
generation,
BTreeMap::from([(
height,
OutstandingMeta {
owner: current_owner,
hash: block::Hash([1; 32]),
estimated_bytes: 100,
queued_at: Instant::now(),
deadline: Instant::now(),
},
)]),
);
let refreshed_scope = zakura_header_chain::BodyWorkAuthority {
header: zakura_header_chain::HeaderWorkAuthority {
header_generation: zakura_header_chain::HeaderGeneration::new(
current_owner.header_generation.get().saturating_add(1),
),
branch: zakura_header_chain::BranchId::new(
current_owner.branch.anchor_hash,
block::Hash([7; 32]),
),
},
..current_owner.authority
};
assert_ne!(refreshed_scope, current_owner.authority());
assert_eq!(
refreshed_scope.body_work_epoch,
current_owner.authority().body_work_epoch
);
assert!(reg.has_outstanding_request(height, block::Hash([1; 32])));
assert!(!reg.has_outstanding_request(height, block::Hash([2; 32])));
assert_eq!(reg.total_unreceived(), 1);
assert!(!reg.clear_outstanding_height_for_owner(&peer, height, stale_owner));
assert!(reg.peer_has_outstanding_height(&peer, height));
assert!(reg.clear_outstanding_height_for_owner(&peer, height, current_owner));
assert!(!reg.peer_has_outstanding_height(&peer, height));
}
#[test]
fn parked_peer_expires_after_cooldown() {
let reg = PeerRegistry::new();
let peer = peer(1);
let now = Instant::now();
reg.park_peer_until(&peer, now + std::time::Duration::from_secs(1));
assert!(reg.is_peer_parked(&peer, now));
assert!(!reg.is_peer_parked(&peer, now + std::time::Duration::from_secs(2)));
}
#[test]
fn expired_session_park_is_consumed_by_same_connection_readmission() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let peer = peer(2);
let conn_id = 7;
let now = Instant::now();
let generation = reg
.admit_session(&peer, ServicePeerDirection::Outbound, &config, conn_id, now)
.generation();
assert!(reg.park_session(
&peer,
conn_id,
generation,
now + std::time::Duration::from_secs(1),
));
assert_eq!(
reg.peer_park_deadline(&peer, now),
Some(now + std::time::Duration::from_secs(1)),
);
assert!(reg.has_expired_session_park(
&peer,
conn_id,
now + std::time::Duration::from_secs(2),
));
assert!(matches!(
reg.admit_session(
&peer,
ServicePeerDirection::Outbound,
&config,
conn_id,
now + std::time::Duration::from_secs(2),
),
SessionAdmission::Readmitted { .. }
));
assert!(!reg.has_expired_session_park(
&peer,
conn_id,
now + std::time::Duration::from_secs(2),
));
}
#[test]
fn active_park_atomically_refuses_admission() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let peer = peer(5);
let conn_id = 7;
let now = Instant::now();
let generation = reg
.admit_session(&peer, ServicePeerDirection::Outbound, &config, conn_id, now)
.generation();
let deadline = now + std::time::Duration::from_secs(1);
assert!(reg.park_session(&peer, conn_id, generation, deadline));
assert_eq!(
reg.admit_session(&peer, ServicePeerDirection::Outbound, &config, conn_id, now),
SessionAdmission::Parked,
);
assert_eq!(reg.peer_park_deadline(&peer, now), Some(deadline));
}
#[test]
fn expired_park_from_a_different_connection_admits_fresh() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let peer = peer(6);
let old_conn_id = 7;
let new_conn_id = 8;
let now = Instant::now();
let generation = reg
.admit_session(
&peer,
ServicePeerDirection::Outbound,
&config,
old_conn_id,
now,
)
.generation();
assert!(reg.park_session(
&peer,
old_conn_id,
generation,
now + std::time::Duration::from_secs(1),
));
let later = now + std::time::Duration::from_secs(2);
assert!(matches!(
reg.admit_session(
&peer,
ServicePeerDirection::Outbound,
&config,
new_conn_id,
later
),
SessionAdmission::Fresh { .. }
));
assert!(!reg.has_expired_session_park(&peer, old_conn_id, later));
}
#[test]
fn routine_on_a_closed_connection_cannot_park() {
let config = super::super::ZakuraBlockSyncConfig::default();
let reg = PeerRegistry::new();
let peer = peer(7);
let conn_id = 7;
let now = Instant::now();
let generation = reg
.admit_session(&peer, ServicePeerDirection::Outbound, &config, conn_id, now)
.generation();
reg.connection_closed(&peer, conn_id, now);
assert!(!reg.park_session(
&peer,
conn_id,
generation,
now + std::time::Duration::from_secs(1),
));
assert!(!reg.is_peer_parked(&peer, now));
}
#[test]
fn connection_cleanup_preserves_cooldown_without_gating_a_fresh_connection() {
let reg = PeerRegistry::new();
let peer = peer(3);
let old_conn_id = 7;
let new_conn_id = 8;
let now = Instant::now();
let deadline = now + std::time::Duration::from_secs(1);
let generation = reg
.admit_session(
&peer,
ServicePeerDirection::Outbound,
&super::super::ZakuraBlockSyncConfig::default(),
old_conn_id,
now,
)
.generation();
assert!(reg.park_session(&peer, old_conn_id, generation, deadline));
reg.connection_closed(&peer, old_conn_id, now);
assert_eq!(reg.peer_park_deadline(&peer, now), Some(deadline));
assert!(!reg.has_expired_session_park(
&peer,
old_conn_id,
now + std::time::Duration::from_secs(2),
));
assert!(!reg.has_expired_session_park(
&peer,
new_conn_id,
now + std::time::Duration::from_secs(2),
));
assert!(!reg.is_peer_parked(&peer, now + std::time::Duration::from_secs(2),));
}
#[test]
fn superseded_routine_cannot_park_the_replacement_generation() {
let reg = PeerRegistry::new();
let peer = peer(4);
let config = super::super::ZakuraBlockSyncConfig::default();
let now = Instant::now();
let old_generation = reg
.admit_session(&peer, ServicePeerDirection::Outbound, &config, 7, now)
.generation();
let _new_generation = reg
.admit_session(&peer, ServicePeerDirection::Outbound, &config, 7, now)
.generation();
assert!(!reg.park_session(
&peer,
7,
old_generation,
now + std::time::Duration::from_secs(1),
));
assert!(!reg.is_peer_parked(&peer, now));
}
}