use crate::prelude::*;
use gcore::errors::Result;
use parity_scale_codec::{Decode, Encode};
#[cfg(not(test))]
use crate::exec;
#[cfg(test)]
use tests::exec_mock as exec;
#[cfg(not(test))]
use crate::ReservationId;
#[cfg(test)]
use tests::ReservationIdMock as ReservationId;
#[derive(Clone, Copy, Debug, TypeInfo, Hash, Encode, Decode)]
pub struct Reservation {
id: ReservationId,
amount: u64,
valid_until: u32,
}
impl From<Reservation> for ReservationId {
fn from(res: Reservation) -> Self {
res.id
}
}
impl Reservation {
pub fn reserve(amount: u64, duration: u32) -> Result<Self> {
let block_height = exec::block_height();
Ok(Self {
id: ReservationId::reserve(amount, duration)?,
amount,
valid_until: duration.saturating_add(block_height),
})
}
pub fn unreserve(self) -> Result<u64> {
self.id.unreserve()
}
pub fn id(&self) -> ReservationId {
self.id
}
pub fn amount(&self) -> u64 {
self.amount
}
pub fn valid_until(&self) -> u32 {
self.valid_until
}
}
#[derive(Default, Clone, Debug, TypeInfo, Hash, Encode, Decode)]
pub struct Reservations(Vec<Reservation>);
impl Reservations {
pub const fn new() -> Self {
Reservations(Vec::new())
}
pub fn reserve(&mut self, amount: u64, duration: u32) -> Result<()> {
let new_reservation = Reservation::reserve(amount, duration)?;
let insert_range_start = self
.0
.partition_point(|reservation| reservation.amount < amount);
let insert_range_end = self
.0
.partition_point(|reservation| reservation.amount <= amount);
let insert_to =
self.0[insert_range_start..insert_range_end].binary_search_by(|reservation| {
reservation.valid_until.cmp(&new_reservation.valid_until)
});
let insert_to = if insert_range_start == self.0.len() {
self.0.len()
} else {
match insert_to {
Ok(pos) => pos + 1,
Err(pos) => pos,
}
};
if self.0.capacity() == self.0.len() {
self.cleanup();
}
self.0.insert(insert_to, new_reservation);
Ok(())
}
pub fn try_take_reservation(&mut self, amount: u64) -> Option<Reservation> {
let search_from = self
.0
.partition_point(|reservation| reservation.amount < amount);
if search_from < self.0.len() {
let block_height = exec::block_height();
for i in search_from..self.0.len() {
if self.0[i].valid_until > block_height {
let suitable = self
.0
.drain(search_from..=i)
.next_back()
.expect("At least one element in range");
return Some(suitable);
}
}
}
self.0.drain(search_from..);
None
}
pub fn count_valid(&self) -> usize {
let block_height = exec::block_height();
self.0
.iter()
.filter(|reservation| reservation.valid_until > block_height)
.count()
}
pub fn count_all(&self) -> usize {
self.0.len()
}
fn cleanup(&mut self) {
let block_height = exec::block_height();
self.0 = self
.0
.drain(..)
.filter(|reservation| reservation.valid_until > block_height)
.collect();
}
}
#[cfg(test)]
mod tests {
use crate::{Reservations, prelude::*};
use gcore::errors::Result;
use parity_scale_codec::{Decode, Encode};
#[must_use]
#[derive(Clone, Copy, Debug, TypeInfo, Hash, Encode, Decode)]
pub struct ReservationIdMock {
pub valid_until: u32,
pub amount: u64,
}
impl ReservationIdMock {
pub fn reserve(amount: u64, duration: u32) -> Result<ReservationIdMock> {
Ok(ReservationIdMock {
valid_until: duration + exec_mock::block_height(),
amount,
})
}
pub fn unreserve(self) -> Result<u64> {
unreachable!()
}
fn is_valid(&self) -> bool {
self.valid_until > exec_mock::block_height()
}
}
pub mod exec_mock {
static mut BLOCK_HEIGHT: u32 = 0;
pub fn block_height() -> u32 {
unsafe { BLOCK_HEIGHT }
}
pub(super) fn set_block_height(block_height: u32) {
unsafe {
BLOCK_HEIGHT = block_height;
}
}
}
#[test]
fn reservations_expire() -> Result<()> {
exec_mock::set_block_height(0);
let mut reservations = Reservations::new();
reservations.reserve(10_000, 5)?;
reservations.reserve(10_000, 10)?;
exec_mock::set_block_height(5);
assert_eq!(reservations.count_all(), 2);
assert_eq!(reservations.count_valid(), 1);
let reservation = reservations.try_take_reservation(10_000);
assert_eq!(reservation.map(|res| res.id().is_valid()), Some(true));
Ok(())
}
#[test]
fn the_best_possible_reservation_taken() -> Result<()> {
exec_mock::set_block_height(0);
let mut reservations = Reservations::new();
reservations.reserve(10_000, 5)?;
reservations.reserve(10_000, 10)?;
reservations.reserve(10_000, 15)?;
exec_mock::set_block_height(7);
let reservation = reservations.try_take_reservation(10_000);
assert_eq!(reservation.map(|res| res.id().valid_until), Some(10));
let reservation = reservations.try_take_reservation(10_000);
assert_eq!(reservation.map(|res| res.id().valid_until), Some(15));
assert_eq!(reservations.count_valid(), 0);
reservations.reserve(10_000, 100)?;
reservations.reserve(20_000, 100)?;
reservations.reserve(30_000, 100)?;
let reservation = reservations.try_take_reservation(1_000);
assert_eq!(reservation.map(|res| res.id.amount), Some(10_000));
let reservation = reservations.try_take_reservation(1_000);
assert_eq!(reservation.map(|res| res.id.amount), Some(20_000));
Ok(())
}
}