use std::sync::Mutex as StdMutex;
use tokio::sync::Notify;
use zakura_chain::block;
use super::{request::BlockSizeEstimate, state::BlockBudgetLedger};
pub(super) const DEFAULT_BS_SIZE_FLOOR_BYTES: u64 = 1024;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) struct WorkItem {
pub(super) hash: block::Hash,
pub(super) estimated_bytes: u64,
pub(super) budget: BlockBudgetLedger,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub(super) struct WorkReturnOutcome {
pub(super) released_bytes: u64,
pub(super) returned_count: u64,
pub(super) already_pending_count: u64,
pub(super) held_count: u64,
pub(super) released_count: u64,
pub(super) missing_count: u64,
pub(super) min_height: Option<block::Height>,
pub(super) max_height: Option<block::Height>,
}
#[derive(Debug)]
struct WorkQueueInner {
pending: std::collections::BTreeMap<block::Height, WorkItem>,
in_flight: std::collections::BTreeMap<block::Height, WorkItem>,
floor: block::Height,
floor_estimate_bytes: u64,
reserved_bytes: u64,
}
impl WorkQueueInner {
fn estimate_bytes(&self, estimate: BlockSizeEstimate) -> u64 {
estimate_bytes_with(estimate, self.floor_estimate_bytes)
}
}
fn estimate_bytes_with(estimate: BlockSizeEstimate, floor: u64) -> u64 {
let hinted = match estimate {
BlockSizeEstimate::Confirmed(size) | BlockSizeEstimate::Advertised(size) => u64::from(size),
BlockSizeEstimate::Unknown => block::MAX_BLOCK_BYTES,
};
hinted.max(floor).min(block::MAX_BLOCK_BYTES)
}
#[derive(Debug)]
pub(super) struct WorkQueue {
inner: StdMutex<WorkQueueInner>,
available: Notify,
}
impl WorkQueue {
pub(super) fn new(floor: block::Height) -> Self {
Self {
inner: StdMutex::new(WorkQueueInner {
pending: std::collections::BTreeMap::new(),
in_flight: std::collections::BTreeMap::new(),
floor,
floor_estimate_bytes: DEFAULT_BS_SIZE_FLOOR_BYTES,
reserved_bytes: 0,
}),
available: Notify::new(),
}
}
#[cfg(test)]
pub(super) fn set_estimate_floor_for_tests(&self, floor: u64) {
let mut inner = self.lock();
inner.floor_estimate_bytes = floor.max(1);
}
fn lock(&self) -> std::sync::MutexGuard<'_, WorkQueueInner> {
self.inner
.lock()
.expect("work queue mutex is never poisoned")
}
pub(super) fn extend(
&self,
items: impl IntoIterator<Item = (block::Height, block::Hash, BlockSizeEstimate)>,
) -> usize {
let mut inserted = 0usize;
{
let mut inner = self.lock();
for (height, hash, size) in items {
if height <= inner.floor
|| inner.pending.contains_key(&height)
|| inner.in_flight.contains_key(&height)
{
continue;
}
let estimated_bytes = inner.estimate_bytes(size);
inner.pending.insert(
height,
WorkItem {
hash,
estimated_bytes,
budget: BlockBudgetLedger::Released,
},
);
inserted += 1;
}
}
if inserted > 0 {
self.available.notify_waiters();
}
inserted
}
pub(super) fn take_in_range(
&self,
low: block::Height,
high: block::Height,
max: usize,
) -> Vec<(block::Height, WorkItem)> {
if max == 0 || low > high {
return Vec::new();
}
let mut inner = self.lock();
let mut taken: Vec<(block::Height, WorkItem)> = Vec::new();
let mut next_expected: Option<block::Height> = None;
for (height, item) in inner.pending.range(low..=high) {
if let Some(expected) = next_expected {
if *height != expected {
break;
}
}
taken.push((*height, *item));
if taken.len() >= max {
break;
}
match height.0.checked_add(1) {
Some(raw) => next_expected = Some(block::Height(raw)),
None => break,
}
}
for (height, item) in &taken {
inner.pending.remove(height);
inner.in_flight.insert(*height, *item);
}
taken
}
pub(super) fn take_in_range_budgeted(
&self,
low: block::Height,
high: block::Height,
max_count: usize,
max_estimated_bytes: u64,
) -> Vec<(block::Height, WorkItem)> {
debug_assert!(
max_count > 0 && low <= high,
"take_in_range_budgeted requires a positive count and low <= high, \
got max_count={max_count}, low={low:?}, high={high:?}"
);
if max_count == 0 || low > high {
return Vec::new();
}
let mut inner = self.lock();
let mut taken: Vec<(block::Height, WorkItem)> = Vec::new();
let mut estimated_bytes = 0u64;
let mut next_expected: Option<block::Height> = None;
for (height, item) in inner.pending.range(low..=high) {
if let Some(expected) = next_expected {
if *height != expected {
break;
}
}
let next_estimated_bytes = estimated_bytes.saturating_add(item.estimated_bytes);
if !taken.is_empty() && next_estimated_bytes > max_estimated_bytes {
break;
}
taken.push((*height, *item));
estimated_bytes = next_estimated_bytes;
if taken.len() >= max_count {
break;
}
match height.0.checked_add(1) {
Some(raw) => next_expected = Some(block::Height(raw)),
None => break,
}
}
for (height, item) in &taken {
inner.pending.remove(height);
inner.in_flight.insert(*height, *item);
}
taken
}
#[cfg(test)]
pub(super) fn return_items(&self, heights: impl IntoIterator<Item = block::Height>) {
let mut moved = false;
{
let mut inner = self.lock();
for height in heights {
if let Some(item) = inner.in_flight.remove(&height) {
inner.pending.insert(height, item);
moved = true;
}
}
}
if moved {
self.available.notify_waiters();
}
}
pub(super) fn return_items_quiet(&self, heights: impl IntoIterator<Item = block::Height>) {
let mut inner = self.lock();
for height in heights {
if let Some(item) = inner.in_flight.remove(&height) {
inner.pending.insert(height, item);
}
}
}
pub(super) fn mark_reserved(&self, heights: impl IntoIterator<Item = block::Height>) -> u64 {
let mut marked = 0u64;
let mut inner = self.lock();
for height in heights {
let Some(item) = inner.in_flight.get_mut(&height) else {
continue;
};
if item.budget.current_charge() != 0 {
continue;
}
item.budget = BlockBudgetLedger::reserved(item.estimated_bytes);
marked = marked.saturating_add(item.estimated_bytes);
}
inner.reserved_bytes = inner.reserved_bytes.saturating_add(marked);
marked
}
pub(super) fn settle_active_reserved_height(
&self,
height: block::Height,
actual: u64,
) -> Option<i128> {
let mut inner = self.lock();
let (reserved_before, delta) = {
let item = inner.in_flight.get_mut(&height)?;
if !item.budget.is_reserved() {
return None;
}
(item.budget.reserved_charge(), item.budget.settle(actual))
};
inner.reserved_bytes = inner.reserved_bytes.saturating_sub(reserved_before);
Some(delta)
}
pub(super) fn mark_held_direct(&self, height: block::Height, actual: u64) -> u64 {
let mut inner = self.lock();
if let Some(item) = inner.in_flight.get_mut(&height) {
let reserved_before = item.budget.reserved_charge();
let previous_charge = item.budget.release();
item.budget = BlockBudgetLedger::Held(actual);
inner.reserved_bytes = inner.reserved_bytes.saturating_sub(reserved_before);
return previous_charge;
}
if let Some(mut item) = inner.pending.remove(&height) {
let reserved_before = item.budget.reserved_charge();
let previous_charge = item.budget.release();
item.budget = BlockBudgetLedger::Held(actual);
inner.in_flight.insert(height, item);
inner.reserved_bytes = inner.reserved_bytes.saturating_sub(reserved_before);
return previous_charge;
}
0
}
#[cfg(test)]
pub(super) fn release_heights(&self, heights: impl IntoIterator<Item = block::Height>) -> u64 {
let mut released = 0u64;
let mut reserved_removed = 0u64;
let mut inner = self.lock();
for height in heights {
if let Some(item) = inner.in_flight.get_mut(&height) {
reserved_removed = reserved_removed.saturating_add(item.budget.reserved_charge());
released = released.saturating_add(item.budget.release());
} else if let Some(item) = inner.pending.get_mut(&height) {
reserved_removed = reserved_removed.saturating_add(item.budget.reserved_charge());
released = released.saturating_add(item.budget.release());
}
}
inner.reserved_bytes = inner.reserved_bytes.saturating_sub(reserved_removed);
released
}
pub(super) fn release_reserved_heights(
&self,
heights: impl IntoIterator<Item = block::Height>,
) -> u64 {
let mut released = 0u64;
let mut inner = self.lock();
for height in heights {
if let Some(item) = inner.in_flight.get_mut(&height) {
if item.budget.is_reserved() {
released = released.saturating_add(item.budget.release_reserved());
}
} else if let Some(item) = inner.pending.get_mut(&height) {
if item.budget.is_reserved() {
released = released.saturating_add(item.budget.release_reserved());
}
}
}
inner.reserved_bytes = inner.reserved_bytes.saturating_sub(released);
released
}
pub(super) fn release_and_return_items(
&self,
heights: impl IntoIterator<Item = block::Height>,
) -> u64 {
let mut moved = false;
let mut released = 0u64;
let mut reserved_removed = 0u64;
{
let mut inner = self.lock();
for height in heights {
if let Some(mut item) = inner.in_flight.remove(&height) {
reserved_removed =
reserved_removed.saturating_add(item.budget.reserved_charge());
released = released.saturating_add(item.budget.release());
inner.pending.insert(height, item);
moved = true;
}
}
inner.reserved_bytes = inner.reserved_bytes.saturating_sub(reserved_removed);
}
if moved {
self.available.notify_waiters();
}
released
}
pub(super) fn release_reserved_and_return_items(
&self,
heights: impl IntoIterator<Item = block::Height>,
) -> u64 {
self.release_reserved_and_return_items_detailed(heights)
.released_bytes
}
pub(super) fn release_reserved_and_return_items_detailed(
&self,
heights: impl IntoIterator<Item = block::Height>,
) -> WorkReturnOutcome {
let mut moved = false;
let mut outcome = WorkReturnOutcome::default();
{
let mut inner = self.lock();
for height in heights {
outcome.min_height = Some(
outcome
.min_height
.map_or(height, |current| current.min(height)),
);
outcome.max_height = Some(
outcome
.max_height
.map_or(height, |current| current.max(height)),
);
let Some(item) = inner.in_flight.get(&height) else {
if inner.pending.contains_key(&height) {
outcome.already_pending_count =
outcome.already_pending_count.saturating_add(1);
} else {
outcome.missing_count = outcome.missing_count.saturating_add(1);
}
continue;
};
match item.budget {
BlockBudgetLedger::Held(_) => {
outcome.held_count = outcome.held_count.saturating_add(1);
continue;
}
BlockBudgetLedger::Released => {
outcome.released_count = outcome.released_count.saturating_add(1);
continue;
}
BlockBudgetLedger::Reserved(_) => {}
}
let mut item = inner
.in_flight
.remove(&height)
.expect("reserved item exists because it was just checked");
outcome.released_bytes =
outcome.released_bytes.saturating_add(item.budget.release());
outcome.returned_count = outcome.returned_count.saturating_add(1);
inner.pending.insert(height, item);
moved = true;
}
inner.reserved_bytes = inner.reserved_bytes.saturating_sub(outcome.released_bytes);
}
if moved {
self.available.notify_waiters();
}
outcome
}
pub(super) fn advance_floor(&self, floor: block::Height) -> u64 {
let mut inner = self.lock();
inner.floor = inner.floor.max(floor);
let floor = inner.floor;
let mut released = 0u64;
while let Some((&height, _)) = inner.pending.first_key_value() {
if height > floor {
break;
}
let (_, mut item) = inner
.pending
.pop_first()
.expect("first_key_value returned Some");
released = released.saturating_add(item.budget.release_reserved());
}
while let Some((&height, _)) = inner.in_flight.first_key_value() {
if height > floor {
break;
}
let (_, mut item) = inner
.in_flight
.pop_first()
.expect("first_key_value returned Some");
released = released.saturating_add(item.budget.release_reserved());
}
inner.reserved_bytes = inner.reserved_bytes.saturating_sub(released);
released
}
pub(super) fn reset_above(&self, floor: block::Height) -> u64 {
let mut inner = self.lock();
inner.floor = floor;
let mut released = 0u64;
while let Some((&height, _)) = inner.pending.last_key_value() {
if height <= floor {
break;
}
let (_, mut item) = inner
.pending
.pop_last()
.expect("last_key_value returned Some");
released = released.saturating_add(item.budget.release_reserved());
}
while let Some((&height, _)) = inner.in_flight.last_key_value() {
if height <= floor {
break;
}
let (_, mut item) = inner
.in_flight
.pop_last()
.expect("last_key_value returned Some");
released = released.saturating_add(item.budget.release_reserved());
}
inner.reserved_bytes = inner.reserved_bytes.saturating_sub(released);
released
}
#[allow(dead_code)]
pub(super) fn subscribe_available(&self) -> &Notify {
&self.available
}
pub(super) fn pending_len(&self) -> usize {
self.lock().pending.len()
}
pub(super) fn in_flight_len(&self) -> usize {
self.lock().in_flight.len()
}
pub(super) fn reserved_above(&self, floor: block::Height) -> (u64, u64) {
let inner = self.lock();
inner
.in_flight
.range((std::ops::Bound::Excluded(floor), std::ops::Bound::Unbounded))
.fold((0u64, 0u64), |(bytes, count), (_, item)| {
let charge = item.budget.reserved_charge();
if charge == 0 {
(bytes, count)
} else {
(bytes.saturating_add(charge), count.saturating_add(1))
}
})
}
pub(super) fn reserved_bytes(&self) -> u64 {
self.lock().reserved_bytes
}
#[cfg(test)]
pub(super) fn reserved_bytes_scanned(&self) -> u64 {
let inner = self.lock();
inner
.pending
.values()
.chain(inner.in_flight.values())
.map(|item| item.budget.reserved_charge())
.fold(0u64, u64::saturating_add)
}
pub(super) fn pending_run_count(&self) -> usize {
let inner = self.lock();
let mut runs = 0usize;
let mut previous: Option<block::Height> = None;
for height in inner.pending.keys() {
let contiguous =
previous.and_then(|previous| previous.0.checked_add(1)) == Some(height.0);
if !contiguous {
runs += 1;
}
previous = Some(*height);
}
runs
}
pub(super) fn min_pending(&self) -> Option<block::Height> {
self.lock().pending.keys().next().copied()
}
pub(super) fn min_in_flight(&self) -> Option<block::Height> {
self.lock().in_flight.keys().next().copied()
}
pub(super) fn first_pending_in_range(
&self,
low: block::Height,
high: block::Height,
) -> Option<block::Height> {
if low > high {
return None;
}
self.lock()
.pending
.range(low..=high)
.next()
.map(|(height, _)| *height)
}
pub(super) fn max_in_flight(&self) -> Option<block::Height> {
self.lock().in_flight.keys().next_back().copied()
}
pub(super) fn max_claimed(&self) -> Option<block::Height> {
let inner = self.lock();
inner
.pending
.keys()
.next_back()
.copied()
.max(inner.in_flight.keys().next_back().copied())
}
pub(super) fn hash_for_height(&self, height: block::Height) -> Option<block::Hash> {
let inner = self.lock();
inner
.pending
.get(&height)
.or_else(|| inner.in_flight.get(&height))
.map(|item| item.hash)
}
pub(super) fn pending_contains(&self, height: block::Height) -> bool {
self.lock().pending.contains_key(&height)
}
pub(super) fn reserved_in_flight_charge(&self, height: block::Height) -> Option<u64> {
self.lock().in_flight.get(&height).and_then(|item| {
item.budget
.is_reserved()
.then(|| item.budget.reserved_charge())
})
}
pub(super) fn in_flight_contains(&self, height: block::Height) -> bool {
self.lock().in_flight.contains_key(&height)
}
}