use std::time::{Duration, Instant};
use zakura_chain::block;
use super::{
config::{ZakuraBlockSyncConfig, MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES},
state::next_height,
};
const ABOVE_FLOOR_DEADLINE_MIN_BYTES_PER_SEC: u64 = 256 * 1024;
const FLOOR_DEADLINE_MIN_BYTES_PER_SEC: u64 = 1024 * 1024;
pub const DESERIALIZED_MEM_FACTOR: u64 = 4;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) struct AdmissionSnapshot {
pub(super) download_floor: block::Height,
pub(super) verified_block_tip: block::Height,
pub(super) reorder_buffered_bytes: u64,
pub(super) reorder_buffered_blocks: u64,
pub(super) applying_buffered_bytes: u64,
pub(super) applying_buffered_blocks: u64,
pub(super) sequencer_input_queued_bytes: u64,
pub(super) reserved_above_floor_bytes: u64,
pub(super) reserved_above_floor_blocks: u64,
pub(super) budget_available: u64,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum RequestPriority {
Floor,
AboveFloor,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum AdmissionOutcome {
Admit(AdmissionGrant),
LookaheadAtCap,
InflightBudgetEmpty,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) struct AdmissionGrant {
pub(super) priority: RequestPriority,
pub(super) take_high: block::Height,
pub(super) max_request_bytes: u64,
}
pub(super) fn floor_rescue_high(download_floor: block::Height) -> block::Height {
next_height(download_floor).unwrap_or(download_floor)
}
pub(super) fn request_priority(
download_floor: block::Height,
start_height: block::Height,
) -> RequestPriority {
if start_height <= floor_rescue_high(download_floor) {
RequestPriority::Floor
} else {
RequestPriority::AboveFloor
}
}
pub(super) fn request_deadline(
priority: RequestPriority,
queued_at: Instant,
request_timeout: Duration,
floor_rescue_timeout: Duration,
estimated_bytes: u64,
btlbw_bytes_per_sec: Option<u64>,
) -> Instant {
match priority {
RequestPriority::Floor => {
let rate = btlbw_bytes_per_sec
.unwrap_or(0)
.max(FLOOR_DEADLINE_MIN_BYTES_PER_SEC);
let transfer = Duration::from_secs_f64(estimated_bytes as f64 / rate as f64);
queued_at + floor_rescue_timeout + transfer
}
RequestPriority::AboveFloor => {
let rate = btlbw_bytes_per_sec
.unwrap_or(0)
.max(ABOVE_FLOOR_DEADLINE_MIN_BYTES_PER_SEC);
let transfer = Duration::from_secs_f64(estimated_bytes as f64 / rate as f64);
queued_at + request_timeout + transfer
}
}
}
const COMMIT_WINDOW_EXEMPT_SPAN_BLOCKS: u32 = MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES as u32;
pub(super) const LOOKAHEAD_BLOCK_HARD_CAP: u64 = 262_144;
fn commit_window_high(snapshot: &AdmissionSnapshot) -> block::Height {
block::Height(
snapshot
.verified_block_tip
.0
.saturating_add(COMMIT_WINDOW_EXEMPT_SPAN_BLOCKS),
)
}
#[derive(Copy, Clone, Debug)]
pub(super) struct RetainedPipelineBytes {
pub(super) reorder_buffered_bytes: u64,
pub(super) applying_buffered_bytes: u64,
pub(super) sequencer_input_queued_bytes: u64,
}
impl RetainedPipelineBytes {
pub(super) fn wire_bytes(self) -> u64 {
self.reorder_buffered_bytes
.saturating_add(self.applying_buffered_bytes)
.saturating_add(self.sequencer_input_queued_bytes)
}
}
impl AdmissionSnapshot {
fn retained(&self) -> RetainedPipelineBytes {
RetainedPipelineBytes {
reorder_buffered_bytes: self.reorder_buffered_bytes,
applying_buffered_bytes: self.applying_buffered_bytes,
sequencer_input_queued_bytes: self.sequencer_input_queued_bytes,
}
}
}
fn estimated_resident_pipeline_bytes(snapshot: &AdmissionSnapshot) -> u64 {
snapshot
.retained()
.wire_bytes()
.saturating_add(snapshot.reserved_above_floor_bytes)
.saturating_mul(DESERIALIZED_MEM_FACTOR)
}
fn held_blocks(snapshot: &AdmissionSnapshot) -> u64 {
snapshot
.reorder_buffered_blocks
.saturating_add(snapshot.applying_buffered_blocks)
.saturating_add(snapshot.reserved_above_floor_blocks)
}
fn lookahead_over_budget(config: &ZakuraBlockSyncConfig, snapshot: &AdmissionSnapshot) -> bool {
estimated_resident_pipeline_bytes(snapshot) >= config.effective_max_reorder_lookahead_bytes()
|| held_blocks(snapshot) >= LOOKAHEAD_BLOCK_HARD_CAP
}
pub(super) fn admit(
config: &ZakuraBlockSyncConfig,
snapshot: AdmissionSnapshot,
start_height: block::Height,
servable_high: block::Height,
response_byte_cap: u64,
) -> AdmissionOutcome {
let priority = request_priority(snapshot.download_floor, start_height);
let window_high = commit_window_high(&snapshot);
let (take_high, max_request_bytes) = if start_height <= window_high {
(
servable_high.min(window_high),
snapshot.budget_available.min(response_byte_cap),
)
} else {
if lookahead_over_budget(config, &snapshot) {
return AdmissionOutcome::LookaheadAtCap;
}
let remaining_wire_bytes = config
.effective_max_reorder_lookahead_bytes()
.saturating_sub(estimated_resident_pipeline_bytes(&snapshot))
/ DESERIALIZED_MEM_FACTOR;
if remaining_wire_bytes == 0 {
return AdmissionOutcome::LookaheadAtCap;
}
(
servable_high,
snapshot
.budget_available
.min(remaining_wire_bytes)
.min(response_byte_cap),
)
};
let max_request_bytes = if priority == RequestPriority::Floor {
max_request_bytes.max(1)
} else {
max_request_bytes
};
if max_request_bytes == 0 {
return AdmissionOutcome::InflightBudgetEmpty;
}
AdmissionOutcome::Admit(AdmissionGrant {
priority,
take_high,
max_request_bytes,
})
}
#[cfg(test)]
mod tests {
use super::*;
const TIMEOUT: Duration = Duration::from_secs(8);
const RESCUE: Duration = Duration::from_secs(2);
#[test]
fn floor_request_leash_is_size_aware() {
let now = Instant::now();
let deadline = request_deadline(
RequestPriority::Floor,
now,
TIMEOUT,
RESCUE,
2_000_000,
None,
);
assert_eq!(
deadline,
now + RESCUE + Duration::from_secs_f64(2_000_000_f64 / (1024_f64 * 1024_f64))
);
}
#[test]
fn above_floor_deadline_grows_with_body_size() {
let now = Instant::now();
let small = request_deadline(
RequestPriority::AboveFloor,
now,
TIMEOUT,
RESCUE,
256 * 1024,
None,
);
let large = request_deadline(
RequestPriority::AboveFloor,
now,
TIMEOUT,
RESCUE,
2 * 1024 * 1024,
None,
);
assert_eq!(small, now + TIMEOUT + Duration::from_secs(1));
assert_eq!(large, now + TIMEOUT + Duration::from_secs(8));
assert!(large > small);
}
#[test]
fn above_floor_deadline_shrinks_as_measured_rate_rises() {
let now = Instant::now();
let fast = request_deadline(
RequestPriority::AboveFloor,
now,
TIMEOUT,
RESCUE,
2 * 1024 * 1024,
Some(64 * 1024 * 1024),
);
assert!(fast > now + TIMEOUT);
assert!(fast < now + TIMEOUT + Duration::from_millis(100));
}
#[test]
fn checkpoint_range_fits_under_one_gib_inflight_budget() {
use super::super::config::{
BS_CHECKPOINT_RANGE_BYTE_FLOOR, BS_PER_BLOCK_WORST_CASE_BYTES,
MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES,
};
let config = ZakuraBlockSyncConfig {
max_inflight_block_bytes: 1024 * 1024 * 1024,
..ZakuraBlockSyncConfig::default()
};
let range_resident = BS_CHECKPOINT_RANGE_BYTE_FLOOR.saturating_mul(DESERIALIZED_MEM_FACTOR);
assert!(
config.effective_max_reorder_lookahead_bytes() >= range_resident,
"effective resident look-ahead ({}) must hold one checkpoint range ({})",
config.effective_max_reorder_lookahead_bytes(),
range_resident,
);
let range_blocks = u32::try_from(MIN_BS_CHECKPOINT_SUBMITTED_BLOCK_APPLIES)
.expect("checkpoint range block count fits in u32");
let snapshot = AdmissionSnapshot {
download_floor: block::Height(range_blocks - 1),
verified_block_tip: block::Height(0),
reorder_buffered_bytes: 0,
reorder_buffered_blocks: 0,
applying_buffered_bytes: BS_CHECKPOINT_RANGE_BYTE_FLOOR - BS_PER_BLOCK_WORST_CASE_BYTES,
applying_buffered_blocks: u64::from(range_blocks) - 1,
sequencer_input_queued_bytes: 0,
reserved_above_floor_bytes: 0,
reserved_above_floor_blocks: 0,
budget_available: config.max_inflight_block_bytes,
};
assert!(
matches!(
admit(
&config,
snapshot,
block::Height(range_blocks),
block::Height(range_blocks),
u64::MAX
),
AdmissionOutcome::Admit(_)
),
"the final block of a checkpoint range must be admissible under a 1 GiB in-flight budget",
);
assert!(
matches!(
admit(
&config,
snapshot,
block::Height(range_blocks + 1),
block::Height(range_blocks + 1),
u64::MAX
),
AdmissionOutcome::Admit(_)
),
"the first gated height above the commit window must still be admissible",
);
}
#[test]
fn clamp_reorder_lookahead_floors_sub_range_configs() {
use super::super::config::BS_CHECKPOINT_RANGE_BYTE_FLOOR;
let mut config = ZakuraBlockSyncConfig {
max_reorder_lookahead_bytes: 1024 * 1024, ..ZakuraBlockSyncConfig::default()
};
config.clamp_reorder_lookahead_to_floor();
assert!(
config.max_reorder_lookahead_bytes
>= BS_CHECKPOINT_RANGE_BYTE_FLOOR.saturating_mul(DESERIALIZED_MEM_FACTOR)
);
}
}