feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
@@ -0,0 +1,115 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Pezkuwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! On demand assigner pallet benchmarking.
#![cfg(feature = "runtime-benchmarks")]
use super::{Pallet, *};
use crate::{
configuration::{HostConfiguration, Pallet as ConfigurationPallet},
paras::{Pallet as ParasPallet, ParaGenesisArgs, ParaKind, TeyrchainsCache},
shared::Pallet as ParasShared,
};
use alloc::vec;
use frame_benchmarking::v2::*;
use frame_system::RawOrigin;
use sp_runtime::traits::Bounded;
use pezkuwi_primitives::{
HeadData, Id as ParaId, SessionIndex, ValidationCode, ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE,
};
// Constants for the benchmarking
const SESSION_INDEX: SessionIndex = 1;
// Initialize a parathread for benchmarking.
pub fn init_parathread<T>(para_id: ParaId)
where
T: Config + crate::paras::Config + crate::shared::Config,
{
ParasShared::<T>::set_session_index(SESSION_INDEX);
let mut config = HostConfiguration::default();
config.scheduler_params.num_cores = 1;
ConfigurationPallet::<T>::force_set_active_config(config);
let mut teyrchains = TeyrchainsCache::new();
ParasPallet::<T>::initialize_para_now(
&mut teyrchains,
para_id,
&ParaGenesisArgs {
para_kind: ParaKind::Parathread,
genesis_head: HeadData(vec![1, 2, 3, 4]),
validation_code: ValidationCode(vec![1, 2, 3, 4]),
},
);
}
#[benchmarks]
mod benchmarks {
/// We want to fill the queue to the maximum, so exactly one more item fits.
const MAX_FILL_BENCH: u32 = ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE.saturating_sub(1);
use super::*;
#[benchmark]
fn place_order_keep_alive(s: Linear<1, MAX_FILL_BENCH>) {
// Setup
let caller = whitelisted_caller();
let para_id = ParaId::from(111u32);
init_parathread::<T>(para_id);
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
Pallet::<T>::populate_queue(para_id, s);
#[extrinsic_call]
_(RawOrigin::Signed(caller.into()), BalanceOf::<T>::max_value(), para_id)
}
#[benchmark]
fn place_order_allow_death(s: Linear<1, MAX_FILL_BENCH>) {
// Setup
let caller = whitelisted_caller();
let para_id = ParaId::from(111u32);
init_parathread::<T>(para_id);
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
Pallet::<T>::populate_queue(para_id, s);
#[extrinsic_call]
_(RawOrigin::Signed(caller.into()), BalanceOf::<T>::max_value(), para_id)
}
#[benchmark]
fn place_order_with_credits(s: Linear<1, MAX_FILL_BENCH>) {
// Setup
let caller: T::AccountId = whitelisted_caller();
let para_id = ParaId::from(111u32);
init_parathread::<T>(para_id);
Credits::<T>::insert(&caller, BalanceOf::<T>::max_value());
Pallet::<T>::populate_queue(para_id, s);
#[extrinsic_call]
_(RawOrigin::Signed(caller.into()), BalanceOf::<T>::max_value(), para_id)
}
impl_benchmark_test_suite!(
Pallet,
crate::mock::new_test_ext(
crate::on_demand::mock_helpers::GenesisConfigBuilder::default().build()
),
crate::mock::Test
);
}
@@ -0,0 +1,181 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Pezkuwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! A module that is responsible for migration of storage.
use super::*;
use frame_support::{
migrations::VersionedMigration, pallet_prelude::ValueQuery, storage_alias,
traits::UncheckedOnRuntimeUpgrade, weights::Weight,
};
mod v0 {
use super::*;
use alloc::collections::vec_deque::VecDeque;
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone)]
pub(super) struct EnqueuedOrder {
pub para_id: ParaId,
}
/// Keeps track of the multiplier used to calculate the current spot price for the on demand
/// assigner.
/// NOTE: Ignoring the `OnEmpty` field for the migration.
#[storage_alias]
pub(super) type SpotTraffic<T: Config> = StorageValue<Pallet<T>, FixedU128, ValueQuery>;
/// The order storage entry. Uses a VecDeque to be able to push to the front of the
/// queue from the scheduler on session boundaries.
/// NOTE: Ignoring the `OnEmpty` field for the migration.
#[storage_alias]
pub(super) type OnDemandQueue<T: Config> =
StorageValue<Pallet<T>, VecDeque<EnqueuedOrder>, ValueQuery>;
}
mod v1 {
use super::*;
use crate::on_demand::LOG_TARGET;
/// Migration to V1
pub struct UncheckedMigrateToV1<T>(core::marker::PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrateToV1<T> {
fn on_runtime_upgrade() -> Weight {
let mut weight: Weight = Weight::zero();
// Migrate the current traffic value
let config = configuration::ActiveConfig::<T>::get();
QueueStatus::<T>::mutate(|mut queue_status| {
Pallet::<T>::update_spot_traffic(&config, &mut queue_status);
let v0_queue = v0::OnDemandQueue::<T>::take();
// Process the v0 queue into v1.
v0_queue.into_iter().for_each(|enqueued_order| {
// Readding the old orders will use the new systems.
Pallet::<T>::add_on_demand_order(
queue_status,
enqueued_order.para_id,
QueuePushDirection::Back,
);
});
});
// Remove the old storage.
v0::OnDemandQueue::<T>::kill(); // 1 write
v0::SpotTraffic::<T>::kill(); // 1 write
// Config read
weight.saturating_accrue(T::DbWeight::get().reads(1));
// QueueStatus read write (update_spot_traffic)
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
// Kill x 2
weight.saturating_accrue(T::DbWeight::get().writes(2));
log::info!(target: LOG_TARGET, "Migrated on demand assigner storage to v1");
weight
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
let n: u32 = v0::OnDemandQueue::<T>::get().len() as u32;
log::info!(
target: LOG_TARGET,
"Number of orders waiting in the queue before: {n}",
);
Ok(n.encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
log::info!(target: LOG_TARGET, "Running post_upgrade()");
ensure!(
v0::OnDemandQueue::<T>::get().is_empty(),
"OnDemandQueue should be empty after the migration"
);
let expected_len = u32::decode(&mut &state[..]).unwrap();
let queue_status_size = QueueStatus::<T>::get().size();
ensure!(
expected_len == queue_status_size,
"Number of orders should be the same before and after migration"
);
let n_affinity_entries: u32 =
AffinityEntries::<T>::iter().map(|(_index, heap)| heap.len() as u32).sum();
let n_para_id_affinity: u32 = ParaIdAffinity::<T>::iter()
.map(|(_para_id, affinity)| affinity.count as u32)
.sum();
ensure!(
n_para_id_affinity == n_affinity_entries,
"Number of affinity entries should be the same as the counts in ParaIdAffinity"
);
Ok(())
}
}
}
/// Migrate `V0` to `V1` of the storage format.
pub type MigrateV0ToV1<T> = VersionedMigration<
0,
1,
v1::UncheckedMigrateToV1<T>,
Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;
#[cfg(test)]
mod tests {
use super::{v0, v1, UncheckedOnRuntimeUpgrade, Weight};
use crate::mock::{new_test_ext, MockGenesisConfig, OnDemand, Test};
use pezkuwi_primitives::Id as ParaId;
#[test]
fn migration_to_v1_preserves_queue_ordering() {
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
// Place orders for paraids 1..5
for i in 1..=5 {
v0::OnDemandQueue::<Test>::mutate(|queue| {
queue.push_back(v0::EnqueuedOrder { para_id: ParaId::new(i) })
});
}
// Queue has 5 orders
let old_queue = v0::OnDemandQueue::<Test>::get();
assert_eq!(old_queue.len(), 5);
// New queue has 0 orders
assert_eq!(OnDemand::get_queue_status().size(), 0);
// For tests, db weight is zero.
assert_eq!(
<v1::UncheckedMigrateToV1<Test> as UncheckedOnRuntimeUpgrade>::on_runtime_upgrade(),
Weight::zero()
);
// New queue has 5 orders
assert_eq!(OnDemand::get_queue_status().size(), 5);
// Compare each entry from the old queue with the entry in the new queue.
old_queue.iter().zip(OnDemand::get_free_entries().iter()).for_each(
|(old_enq, new_enq)| {
assert_eq!(old_enq.para_id, new_enq.para_id);
},
);
});
}
}
@@ -0,0 +1,87 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Pezkuwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! Helper functions for tests, also used in runtime-benchmarks.
#![cfg(test)]
use super::*;
use crate::{
mock::MockGenesisConfig,
paras::{ParaGenesisArgs, ParaKind},
};
use pezkuwi_primitives::{Balance, HeadData, ValidationCode};
fn default_genesis_config() -> MockGenesisConfig {
MockGenesisConfig {
configuration: crate::configuration::GenesisConfig {
config: crate::configuration::HostConfiguration { ..Default::default() },
},
..Default::default()
}
}
#[derive(Debug)]
pub struct GenesisConfigBuilder {
pub on_demand_cores: u32,
pub on_demand_base_fee: Balance,
pub on_demand_fee_variability: Perbill,
pub on_demand_max_queue_size: u32,
pub on_demand_target_queue_utilization: Perbill,
pub onboarded_on_demand_chains: Vec<ParaId>,
}
impl Default for GenesisConfigBuilder {
fn default() -> Self {
Self {
on_demand_cores: 10,
on_demand_base_fee: 10_000,
on_demand_fee_variability: Perbill::from_percent(1),
on_demand_max_queue_size: 100,
on_demand_target_queue_utilization: Perbill::from_percent(25),
onboarded_on_demand_chains: vec![],
}
}
}
impl GenesisConfigBuilder {
pub(super) fn build(self) -> MockGenesisConfig {
let mut genesis = default_genesis_config();
let config = &mut genesis.configuration.config;
config.scheduler_params.num_cores = self.on_demand_cores;
config.scheduler_params.on_demand_base_fee = self.on_demand_base_fee;
config.scheduler_params.on_demand_fee_variability = self.on_demand_fee_variability;
config.scheduler_params.on_demand_queue_max_size = self.on_demand_max_queue_size;
config.scheduler_params.on_demand_target_queue_utilization =
self.on_demand_target_queue_utilization;
let paras = &mut genesis.paras.paras;
for para_id in self.onboarded_on_demand_chains {
paras.push((
para_id,
ParaGenesisArgs {
genesis_head: HeadData::from(vec![0u8]),
validation_code: ValidationCode::from(vec![0u8]),
para_kind: ParaKind::Parathread,
},
))
}
genesis
}
}
@@ -0,0 +1,813 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Pezkuwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! The teyrchain on demand assignment module.
//!
//! Implements a mechanism for taking in orders for on-demand teyrchain (previously parathreads)
//! assignments. This module is not handled by the initializer but is instead instantiated in the
//! `construct_runtime` macro.
//!
//! The module currently limits parallel execution of blocks from the same `ParaId` via
//! a core affinity mechanism. As long as there exists an affinity for a `CoreIndex` for
//! a specific `ParaId`, orders for blockspace for that `ParaId` will only be assigned to
//! that `CoreIndex`.
//!
//! NOTE: Once we have elastic scaling implemented we might want to extend this module to support
//! ignoring core affinity up to a certain extend. This should be opt-in though as the teyrchain
//! needs to support multiple cores in the same block. If we want to enable a single teyrchain
//! occupying multiple cores in on-demand, we will likely add a separate order type, where the
//! intent can be made explicit.
use sp_runtime::traits::Zero;
mod benchmarking;
pub mod migration;
mod mock_helpers;
mod types;
extern crate alloc;
#[cfg(test)]
mod tests;
use crate::{configuration, paras, scheduler::common::Assignment};
use alloc::collections::BinaryHeap;
use core::mem::take;
use frame_support::{
pallet_prelude::*,
traits::{
defensive_prelude::*,
Currency,
ExistenceRequirement::{self, AllowDeath, KeepAlive},
WithdrawReasons,
},
PalletId,
};
use frame_system::{pallet_prelude::*, Pallet as System};
use pezkuwi_primitives::{CoreIndex, Id as ParaId};
use sp_runtime::{
traits::{AccountIdConversion, One, SaturatedConversion},
FixedPointNumber, FixedPointOperand, FixedU128, Perbill, Saturating,
};
use types::{
BalanceOf, CoreAffinityCount, EnqueuedOrder, QueuePushDirection, QueueStatusType,
SpotTrafficCalculationErr,
};
const LOG_TARGET: &str = "runtime::teyrchains::on-demand";
pub use pallet::*;
pub trait WeightInfo {
fn place_order_allow_death(s: u32) -> Weight;
fn place_order_keep_alive(s: u32) -> Weight;
fn place_order_with_credits(s: u32) -> Weight;
}
/// A weight info that is only suitable for testing.
pub struct TestWeightInfo;
impl WeightInfo for TestWeightInfo {
fn place_order_allow_death(_: u32) -> Weight {
Weight::MAX
}
fn place_order_keep_alive(_: u32) -> Weight {
Weight::MAX
}
fn place_order_with_credits(_: u32) -> Weight {
Weight::MAX
}
}
/// Defines how the account wants to pay for on-demand.
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq)]
enum PaymentType {
/// Use credits to purchase on-demand coretime.
Credits,
/// Use account's free balance to purchase on-demand coretime.
Balance,
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::without_storage_info]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + configuration::Config + paras::Config {
/// The runtime's definition of an event.
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// The runtime's definition of a Currency.
type Currency: Currency<Self::AccountId>;
/// Something that provides the weight of this pallet.
type WeightInfo: WeightInfo;
/// The default value for the spot traffic multiplier.
#[pallet::constant]
type TrafficDefaultValue: Get<FixedU128>;
/// The maximum number of blocks some historical revenue
/// information stored for.
#[pallet::constant]
type MaxHistoricalRevenue: Get<u32>;
/// Identifier for the internal revenue balance.
#[pallet::constant]
type PalletId: Get<PalletId>;
}
/// Creates an empty queue status for an empty queue with initial traffic value.
#[pallet::type_value]
pub(super) fn QueueStatusOnEmpty<T: Config>() -> QueueStatusType {
QueueStatusType { traffic: T::TrafficDefaultValue::get(), ..Default::default() }
}
#[pallet::type_value]
pub(super) fn EntriesOnEmpty<T: Config>() -> BinaryHeap<EnqueuedOrder> {
BinaryHeap::new()
}
/// Maps a `ParaId` to `CoreIndex` and keeps track of how many assignments the scheduler has in
/// it's lookahead. Keeping track of this affinity prevents parallel execution of the same
/// `ParaId` on two or more `CoreIndex`es.
#[pallet::storage]
pub(super) type ParaIdAffinity<T: Config> =
StorageMap<_, Twox64Concat, ParaId, CoreAffinityCount, OptionQuery>;
/// Overall status of queue (both free + affinity entries)
#[pallet::storage]
pub(super) type QueueStatus<T: Config> =
StorageValue<_, QueueStatusType, ValueQuery, QueueStatusOnEmpty<T>>;
/// Priority queue for all orders which don't yet (or not any more) have any core affinity.
#[pallet::storage]
pub(super) type FreeEntries<T: Config> =
StorageValue<_, BinaryHeap<EnqueuedOrder>, ValueQuery, EntriesOnEmpty<T>>;
/// Queue entries that are currently bound to a particular core due to core affinity.
#[pallet::storage]
pub(super) type AffinityEntries<T: Config> = StorageMap<
_,
Twox64Concat,
CoreIndex,
BinaryHeap<EnqueuedOrder>,
ValueQuery,
EntriesOnEmpty<T>,
>;
/// Keeps track of accumulated revenue from on demand order sales.
#[pallet::storage]
pub type Revenue<T: Config> =
StorageValue<_, BoundedVec<BalanceOf<T>, T::MaxHistoricalRevenue>, ValueQuery>;
/// Keeps track of credits owned by each account.
#[pallet::storage]
pub type Credits<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// An order was placed at some spot price amount by orderer ordered_by
OnDemandOrderPlaced { para_id: ParaId, spot_price: BalanceOf<T>, ordered_by: T::AccountId },
/// The value of the spot price has likely changed
SpotPriceSet { spot_price: BalanceOf<T> },
/// An account was given credits.
AccountCredited { who: T::AccountId, amount: BalanceOf<T> },
}
#[pallet::error]
pub enum Error<T> {
/// The order queue is full, `place_order` will not continue.
QueueFull,
/// The current spot price is higher than the max amount specified in the `place_order`
/// call, making it invalid.
SpotPriceHigherThanMaxAmount,
/// The account doesn't have enough credits to purchase on-demand coretime.
InsufficientCredits,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_now: BlockNumberFor<T>) -> Weight {
// Update revenue information storage.
Revenue::<T>::mutate(|revenue| {
if let Some(overdue) =
revenue.force_insert_keep_left(0, 0u32.into()).defensive_unwrap_or(None)
{
// We have some overdue revenue not claimed by the Coretime Chain, let's
// accumulate it at the oldest stored block
if let Some(last) = revenue.last_mut() {
*last = last.saturating_add(overdue);
}
}
});
let config = configuration::ActiveConfig::<T>::get();
// We need to update the spot traffic on block initialize in order to account for idle
// blocks.
QueueStatus::<T>::mutate(|queue_status| {
Self::update_spot_traffic(&config, queue_status);
});
// Reads: `Revenue`, `ActiveConfig`, `QueueStatus`
// Writes: `Revenue`, `QueueStatus`
T::DbWeight::get().reads_writes(3, 2)
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Create a single on demand core order.
/// Will use the spot price for the current block and will reap the account if needed.
///
/// Parameters:
/// - `origin`: The sender of the call, funds will be withdrawn from this account.
/// - `max_amount`: The maximum balance to withdraw from the origin to place an order.
/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
///
/// Errors:
/// - `InsufficientBalance`: from the Currency implementation
/// - `QueueFull`
/// - `SpotPriceHigherThanMaxAmount`
///
/// Events:
/// - `OnDemandOrderPlaced`
#[pallet::call_index(0)]
#[pallet::weight(<T as Config>::WeightInfo::place_order_allow_death(QueueStatus::<T>::get().size()))]
#[allow(deprecated)]
#[deprecated(note = "This will be removed in favor of using `place_order_with_credits`")]
pub fn place_order_allow_death(
origin: OriginFor<T>,
max_amount: BalanceOf<T>,
para_id: ParaId,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
Pallet::<T>::do_place_order(
sender,
max_amount,
para_id,
AllowDeath,
PaymentType::Balance,
)
}
/// Same as the [`place_order_allow_death`](Self::place_order_allow_death) call , but with a
/// check that placing the order will not reap the account.
///
/// Parameters:
/// - `origin`: The sender of the call, funds will be withdrawn from this account.
/// - `max_amount`: The maximum balance to withdraw from the origin to place an order.
/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
///
/// Errors:
/// - `InsufficientBalance`: from the Currency implementation
/// - `QueueFull`
/// - `SpotPriceHigherThanMaxAmount`
///
/// Events:
/// - `OnDemandOrderPlaced`
#[pallet::call_index(1)]
#[pallet::weight(<T as Config>::WeightInfo::place_order_keep_alive(QueueStatus::<T>::get().size()))]
#[allow(deprecated)]
#[deprecated(note = "This will be removed in favor of using `place_order_with_credits`")]
pub fn place_order_keep_alive(
origin: OriginFor<T>,
max_amount: BalanceOf<T>,
para_id: ParaId,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
Pallet::<T>::do_place_order(
sender,
max_amount,
para_id,
KeepAlive,
PaymentType::Balance,
)
}
/// Create a single on demand core order with credits.
/// Will charge the owner's on-demand credit account the spot price for the current block.
///
/// Parameters:
/// - `origin`: The sender of the call, on-demand credits will be withdrawn from this
/// account.
/// - `max_amount`: The maximum number of credits to spend from the origin to place an
/// order.
/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
///
/// Errors:
/// - `InsufficientCredits`
/// - `QueueFull`
/// - `SpotPriceHigherThanMaxAmount`
///
/// Events:
/// - `OnDemandOrderPlaced`
#[pallet::call_index(2)]
#[pallet::weight(<T as Config>::WeightInfo::place_order_with_credits(QueueStatus::<T>::get().size()))]
pub fn place_order_with_credits(
origin: OriginFor<T>,
max_amount: BalanceOf<T>,
para_id: ParaId,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
Pallet::<T>::do_place_order(
sender,
max_amount,
para_id,
KeepAlive,
PaymentType::Credits,
)
}
}
}
// Internal functions and interface to scheduler/wrapping assignment provider.
impl<T: Config> Pallet<T>
where
BalanceOf<T>: FixedPointOperand,
{
/// Take the next queued entry that is available for a given core index.
///
/// Parameters:
/// - `core_index`: The core index
pub fn pop_assignment_for_core(core_index: CoreIndex) -> Option<Assignment> {
let entry: Result<EnqueuedOrder, ()> = QueueStatus::<T>::try_mutate(|queue_status| {
AffinityEntries::<T>::try_mutate(core_index, |affinity_entries| {
let free_entry = FreeEntries::<T>::try_mutate(|free_entries| {
let affinity_next = affinity_entries.peek();
let free_next = free_entries.peek();
let pick_free = match (affinity_next, free_next) {
(None, _) => true,
(Some(_), None) => false,
(Some(a), Some(f)) => f < a,
};
if pick_free {
let entry = free_entries.pop().ok_or(())?;
let (mut affinities, free): (BinaryHeap<_>, BinaryHeap<_>) =
take(free_entries)
.into_iter()
.partition(|e| e.para_id == entry.para_id);
affinity_entries.append(&mut affinities);
*free_entries = free;
Ok(entry)
} else {
Err(())
}
});
let entry = free_entry.or_else(|()| affinity_entries.pop().ok_or(()))?;
queue_status.consume_index(entry.idx);
Ok(entry)
})
});
let assignment = entry.map(|e| Assignment::Pool { para_id: e.para_id, core_index }).ok()?;
Pallet::<T>::increase_affinity(assignment.para_id(), core_index);
Some(assignment)
}
/// Report that an assignment was duplicated by the scheduler.
pub fn assignment_duplicated(para_id: ParaId, core_index: CoreIndex) {
Pallet::<T>::increase_affinity(para_id, core_index);
}
/// Report that the `para_id` & `core_index` combination was processed.
///
/// This should be called once it is clear that the assignment won't get pushed back anymore.
///
/// In other words for each `pop_assignment_for_core` a call to this function or
/// `push_back_assignment` must follow, but only one.
pub fn report_processed(para_id: ParaId, core_index: CoreIndex) {
Pallet::<T>::decrease_affinity_update_queue(para_id, core_index);
}
/// Push an assignment back to the front of the queue.
///
/// The assignment has not been processed yet. Typically used on session boundaries.
///
/// NOTE: We are not checking queue size here. So due to push backs it is possible that we
/// exceed the maximum queue size slightly.
///
/// Parameters:
/// - `para_id`: The para that did not make it.
/// - `core_index`: The core the para was scheduled on.
pub fn push_back_assignment(para_id: ParaId, core_index: CoreIndex) {
Pallet::<T>::decrease_affinity_update_queue(para_id, core_index);
QueueStatus::<T>::mutate(|queue_status| {
Pallet::<T>::add_on_demand_order(queue_status, para_id, QueuePushDirection::Front);
});
}
/// Adds credits to the specified account.
///
/// Parameters:
/// - `who`: Credit receiver.
/// - `amount`: The amount of new credits the account will receive.
pub fn credit_account(who: T::AccountId, amount: BalanceOf<T>) {
Credits::<T>::mutate(who.clone(), |credits| {
*credits = credits.saturating_add(amount);
});
Pallet::<T>::deposit_event(Event::<T>::AccountCredited { who, amount });
}
/// Helper function for `place_order_*` calls. Used to differentiate between placing orders
/// with a keep alive check or to allow the account to be reaped. The amount charged is
/// stored to the pallet account to be later paid out as revenue.
///
/// Parameters:
/// - `sender`: The sender of the call, funds will be withdrawn from this account.
/// - `max_amount`: The maximum balance to withdraw from the origin to place an order.
/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
/// - `existence_requirement`: Whether or not to ensure that the account will not be reaped.
/// - `payment_type`: Defines how the user wants to pay for on-demand.
///
/// Errors:
/// - `InsufficientBalance`: from the Currency implementation
/// - `QueueFull`
/// - `SpotPriceHigherThanMaxAmount`
///
/// Events:
/// - `OnDemandOrderPlaced`
fn do_place_order(
sender: <T as frame_system::Config>::AccountId,
max_amount: BalanceOf<T>,
para_id: ParaId,
existence_requirement: ExistenceRequirement,
payment_type: PaymentType,
) -> DispatchResult {
let config = configuration::ActiveConfig::<T>::get();
QueueStatus::<T>::mutate(|queue_status| {
Self::update_spot_traffic(&config, queue_status);
let traffic = queue_status.traffic;
// Calculate spot price
let spot_price: BalanceOf<T> = traffic.saturating_mul_int(
config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<T>>(),
);
// Is the current price higher than `max_amount`
ensure!(spot_price.le(&max_amount), Error::<T>::SpotPriceHigherThanMaxAmount);
ensure!(
queue_status.size() < config.scheduler_params.on_demand_queue_max_size,
Error::<T>::QueueFull
);
match payment_type {
PaymentType::Balance => {
// Charge the sending account the spot price. The amount will be teleported to
// the broker chain once it requests revenue information.
let amt = T::Currency::withdraw(
&sender,
spot_price,
WithdrawReasons::FEE,
existence_requirement,
)?;
// Consume the negative imbalance and deposit it into the pallet account. Make
// sure the account preserves even without the existential deposit.
let pot = Self::account_id();
if !System::<T>::account_exists(&pot) {
System::<T>::inc_providers(&pot);
}
T::Currency::resolve_creating(&pot, amt);
},
PaymentType::Credits => {
let credits = Credits::<T>::get(&sender);
// Charge the sending account the spot price in credits.
let new_credits_value =
credits.checked_sub(&spot_price).ok_or(Error::<T>::InsufficientCredits)?;
if new_credits_value.is_zero() {
Credits::<T>::remove(&sender);
} else {
Credits::<T>::insert(&sender, new_credits_value);
}
},
}
// Add the amount to the current block's (index 0) revenue information.
Revenue::<T>::mutate(|bounded_revenue| {
if let Some(current_block) = bounded_revenue.get_mut(0) {
*current_block = current_block.saturating_add(spot_price);
} else {
// Revenue has already been claimed in the same block, including the block
// itself. It shouldn't normally happen as revenue claims in the future are
// not allowed.
bounded_revenue.try_push(spot_price).defensive_ok();
}
});
Pallet::<T>::add_on_demand_order(queue_status, para_id, QueuePushDirection::Back);
Pallet::<T>::deposit_event(Event::<T>::OnDemandOrderPlaced {
para_id,
spot_price,
ordered_by: sender,
});
Ok(())
})
}
/// Calculate and update spot traffic.
fn update_spot_traffic(
config: &configuration::HostConfiguration<BlockNumberFor<T>>,
queue_status: &mut QueueStatusType,
) {
let old_traffic = queue_status.traffic;
match Self::calculate_spot_traffic(
old_traffic,
config.scheduler_params.on_demand_queue_max_size,
queue_status.size(),
config.scheduler_params.on_demand_target_queue_utilization,
config.scheduler_params.on_demand_fee_variability,
) {
Ok(new_traffic) => {
// Only update storage on change
if new_traffic != old_traffic {
queue_status.traffic = new_traffic;
// calculate the new spot price
let spot_price: BalanceOf<T> = new_traffic.saturating_mul_int(
config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<T>>(),
);
// emit the event for updated new price
Pallet::<T>::deposit_event(Event::<T>::SpotPriceSet { spot_price });
}
},
Err(err) => {
log::debug!(
target: LOG_TARGET,
"Error calculating spot traffic: {:?}", err
);
},
};
}
/// The spot price multiplier. This is based on the transaction fee calculations defined in:
/// https://research.web3.foundation/Polkadot/overview/token-economics#setting-transaction-fees
///
/// Parameters:
/// - `traffic`: The previously calculated multiplier, can never go below 1.0.
/// - `queue_capacity`: The max size of the order book.
/// - `queue_size`: How many orders are currently in the order book.
/// - `target_queue_utilisation`: How much of the queue_capacity should be ideally occupied,
/// expressed in percentages(perbill).
/// - `variability`: A variability factor, i.e. how quickly the spot price adjusts. This number
/// can be chosen by p/(k*(1-s)) where p is the desired ratio increase in spot price over k
/// number of blocks. s is the target_queue_utilisation. A concrete example: v =
/// 0.05/(20*(1-0.25)) = 0.0033.
///
/// Returns:
/// - A `FixedU128` in the range of `Config::TrafficDefaultValue` - `FixedU128::MAX` on
/// success.
///
/// Errors:
/// - `SpotTrafficCalculationErr::QueueCapacityIsZero`
/// - `SpotTrafficCalculationErr::QueueSizeLargerThanCapacity`
/// - `SpotTrafficCalculationErr::Division`
fn calculate_spot_traffic(
traffic: FixedU128,
queue_capacity: u32,
queue_size: u32,
target_queue_utilisation: Perbill,
variability: Perbill,
) -> Result<FixedU128, SpotTrafficCalculationErr> {
// Return early if queue has no capacity.
if queue_capacity == 0 {
return Err(SpotTrafficCalculationErr::QueueCapacityIsZero);
}
// Return early if queue size is greater than capacity.
if queue_size > queue_capacity {
return Err(SpotTrafficCalculationErr::QueueSizeLargerThanCapacity);
}
// (queue_size / queue_capacity) - target_queue_utilisation
let queue_util_ratio = FixedU128::from_rational(queue_size.into(), queue_capacity.into());
let positive = queue_util_ratio >= target_queue_utilisation.into();
let queue_util_diff = queue_util_ratio.max(target_queue_utilisation.into()) -
queue_util_ratio.min(target_queue_utilisation.into());
// variability * queue_util_diff
let var_times_qud = queue_util_diff.saturating_mul(variability.into());
// variability^2 * queue_util_diff^2
let var_times_qud_pow = var_times_qud.saturating_mul(var_times_qud);
// (variability^2 * queue_util_diff^2)/2
let div_by_two: FixedU128;
match var_times_qud_pow.const_checked_div(2.into()) {
Some(dbt) => div_by_two = dbt,
None => return Err(SpotTrafficCalculationErr::Division),
}
// traffic * (1 + queue_util_diff) + div_by_two
if positive {
let new_traffic = queue_util_diff
.saturating_add(div_by_two)
.saturating_add(One::one())
.saturating_mul(traffic);
Ok(new_traffic.max(<T as Config>::TrafficDefaultValue::get()))
} else {
let new_traffic = queue_util_diff.saturating_sub(div_by_two).saturating_mul(traffic);
Ok(new_traffic.max(<T as Config>::TrafficDefaultValue::get()))
}
}
/// Adds an order to the on demand queue.
///
/// Parameters:
/// - `location`: Whether to push this entry to the back or the front of the queue. Pushing an
/// entry to the front of the queue is only used when the scheduler wants to push back an
/// entry it has already popped.
fn add_on_demand_order(
queue_status: &mut QueueStatusType,
para_id: ParaId,
location: QueuePushDirection,
) {
let idx = match location {
QueuePushDirection::Back => queue_status.push_back(),
QueuePushDirection::Front => queue_status.push_front(),
};
let affinity = ParaIdAffinity::<T>::get(para_id);
let order = EnqueuedOrder::new(idx, para_id);
#[cfg(test)]
log::debug!(target: LOG_TARGET, "add_on_demand_order, order: {:?}, affinity: {:?}, direction: {:?}", order, affinity, location);
match affinity {
None => FreeEntries::<T>::mutate(|entries| entries.push(order)),
Some(affinity) =>
AffinityEntries::<T>::mutate(affinity.core_index, |entries| entries.push(order)),
}
}
/// Decrease core affinity for para and update queue
///
/// if affinity dropped to 0, moving entries back to `FreeEntries`.
fn decrease_affinity_update_queue(para_id: ParaId, core_index: CoreIndex) {
let affinity = Pallet::<T>::decrease_affinity(para_id, core_index);
#[cfg(not(test))]
debug_assert_ne!(
affinity, None,
"Decreased affinity for a para that has not been served on a core?"
);
if affinity != Some(0) {
return;
}
// No affinity more for entries on this core, free any entries:
//
// This is necessary to ensure them being served as the core might no longer exist at all.
AffinityEntries::<T>::mutate(core_index, |affinity_entries| {
FreeEntries::<T>::mutate(|free_entries| {
let (mut freed, affinities): (BinaryHeap<_>, BinaryHeap<_>) =
take(affinity_entries).into_iter().partition(|e| e.para_id == para_id);
free_entries.append(&mut freed);
*affinity_entries = affinities;
})
});
}
/// Decreases the affinity of a `ParaId` to a specified `CoreIndex`.
///
/// Subtracts from the count of the `CoreAffinityCount` if an entry is found and the core_index
/// matches. When the count reaches 0, the entry is removed.
/// A non-existent entry is a no-op.
///
/// Returns: The new affinity of the para on that core. `None` if there is no affinity on this
/// core.
fn decrease_affinity(para_id: ParaId, core_index: CoreIndex) -> Option<u32> {
ParaIdAffinity::<T>::mutate(para_id, |maybe_affinity| {
let affinity = maybe_affinity.as_mut()?;
if affinity.core_index == core_index {
let new_count = affinity.count.saturating_sub(1);
if new_count > 0 {
*maybe_affinity = Some(CoreAffinityCount { core_index, count: new_count });
} else {
*maybe_affinity = None;
}
return Some(new_count);
} else {
None
}
})
}
/// Increases the affinity of a `ParaId` to a specified `CoreIndex`.
/// Adds to the count of the `CoreAffinityCount` if an entry is found and the core_index
/// matches. A non-existent entry will be initialized with a count of 1 and uses the supplied
/// `CoreIndex`.
fn increase_affinity(para_id: ParaId, core_index: CoreIndex) {
ParaIdAffinity::<T>::mutate(para_id, |maybe_affinity| match maybe_affinity {
Some(affinity) =>
if affinity.core_index == core_index {
*maybe_affinity = Some(CoreAffinityCount {
core_index,
count: affinity.count.saturating_add(1),
});
},
None => {
*maybe_affinity = Some(CoreAffinityCount { core_index, count: 1 });
},
})
}
/// Collect the revenue from the `when` blockheight
pub fn claim_revenue_until(when: BlockNumberFor<T>) -> BalanceOf<T> {
let now = <frame_system::Pallet<T>>::block_number();
let mut amount: BalanceOf<T> = BalanceOf::<T>::zero();
Revenue::<T>::mutate(|revenue| {
while !revenue.is_empty() {
let index = (revenue.len() - 1) as u32;
if when > now.saturating_sub(index.into()) {
amount = amount.saturating_add(revenue.pop().defensive_unwrap_or(0u32.into()));
} else {
break;
}
}
});
amount
}
/// Account of the pallet pot, where the funds from instantaneous coretime sale are accumulated.
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
/// Getter for the affinity tracker.
#[cfg(test)]
fn get_affinity_map(para_id: ParaId) -> Option<CoreAffinityCount> {
ParaIdAffinity::<T>::get(para_id)
}
/// Getter for the affinity entries.
#[cfg(test)]
fn get_affinity_entries(core_index: CoreIndex) -> BinaryHeap<EnqueuedOrder> {
AffinityEntries::<T>::get(core_index)
}
/// Getter for the free entries.
#[cfg(test)]
fn get_free_entries() -> BinaryHeap<EnqueuedOrder> {
FreeEntries::<T>::get()
}
#[cfg(feature = "runtime-benchmarks")]
pub fn populate_queue(para_id: ParaId, num: u32) {
QueueStatus::<T>::mutate(|queue_status| {
for _ in 0..num {
Pallet::<T>::add_on_demand_order(queue_status, para_id, QueuePushDirection::Back);
}
});
}
#[cfg(test)]
fn set_queue_status(new_status: QueueStatusType) {
QueueStatus::<T>::set(new_status);
}
#[cfg(test)]
fn get_queue_status() -> QueueStatusType {
QueueStatus::<T>::get()
}
#[cfg(test)]
fn get_traffic_default_value() -> FixedU128 {
<T as Config>::TrafficDefaultValue::get()
}
#[cfg(test)]
fn get_revenue() -> Vec<BalanceOf<T>> {
Revenue::<T>::get().to_vec()
}
}
@@ -0,0 +1,882 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Pezkuwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use crate::{
initializer::SessionChangeNotification,
mock::{
new_test_ext, Balances, OnDemand, Paras, ParasShared, RuntimeOrigin, Scheduler, System,
Test,
},
on_demand::{
self,
mock_helpers::GenesisConfigBuilder,
types::{QueueIndex, ReverseQueueIndex},
Error,
},
paras::{ParaGenesisArgs, ParaKind},
};
use core::cmp::{Ord, Ordering};
use frame_support::{assert_noop, assert_ok};
use pallet_balances::Error as BalancesError;
use pezkuwi_primitives::{BlockNumber, SessionIndex, ValidationCode, ON_DEMAND_MAX_QUEUE_MAX_SIZE};
use sp_runtime::traits::BadOrigin;
fn schedule_blank_para(id: ParaId, parakind: ParaKind) {
let validation_code: ValidationCode = vec![1, 2, 3].into();
assert_ok!(Paras::schedule_para_initialize(
id,
ParaGenesisArgs {
genesis_head: Vec::new().into(),
validation_code: validation_code.clone(),
para_kind: parakind,
}
));
assert_ok!(Paras::add_trusted_validation_code(RuntimeOrigin::root(), validation_code));
}
fn run_to_block(
to: BlockNumber,
new_session: impl Fn(BlockNumber) -> Option<SessionChangeNotification<BlockNumber>>,
) {
while System::block_number() < to {
let b = System::block_number();
Scheduler::initializer_finalize();
Paras::initializer_finalize(b);
if let Some(notification) = new_session(b + 1) {
let mut notification_with_session_index = notification;
// We will make every session change trigger an action queue. Normally this may require
// 2 or more session changes.
if notification_with_session_index.session_index == SessionIndex::default() {
notification_with_session_index.session_index = ParasShared::scheduled_session();
}
Paras::initializer_on_new_session(&notification_with_session_index);
Scheduler::initializer_on_new_session(&notification_with_session_index);
}
System::on_finalize(b);
System::on_initialize(b + 1);
System::set_block_number(b + 1);
Paras::initializer_initialize(b + 1);
Scheduler::initializer_initialize(b + 1);
// Update the spot traffic and revenue on every block.
OnDemand::on_initialize(b + 1);
// In the real runtime this is expected to be called by the `InclusionInherent` pallet.
Scheduler::advance_claim_queue(&Default::default());
}
}
fn place_order_run_to_blocknumber(para_id: ParaId, blocknumber: Option<BlockNumber>) {
let alice = 100u64;
let amt = 10_000_000u128;
Balances::make_free_balance_be(&alice, amt);
if let Some(bn) = blocknumber {
run_to_block(bn, |n| if n == bn { Some(Default::default()) } else { None });
}
#[allow(deprecated)]
OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), amt, para_id).unwrap()
}
fn place_order_run_to_101(para_id: ParaId) {
place_order_run_to_blocknumber(para_id, Some(101));
}
fn place_order(para_id: ParaId) {
place_order_run_to_blocknumber(para_id, None);
}
#[test]
fn spot_traffic_capacity_zero_returns_none() {
match OnDemand::calculate_spot_traffic(
FixedU128::from(u128::MAX),
0u32,
u32::MAX,
Perbill::from_percent(100),
Perbill::from_percent(1),
) {
Ok(_) => panic!("Error"),
Err(e) => assert_eq!(e, SpotTrafficCalculationErr::QueueCapacityIsZero),
};
}
#[test]
fn spot_traffic_queue_size_larger_than_capacity_returns_none() {
match OnDemand::calculate_spot_traffic(
FixedU128::from(u128::MAX),
1u32,
2u32,
Perbill::from_percent(100),
Perbill::from_percent(1),
) {
Ok(_) => panic!("Error"),
Err(e) => assert_eq!(e, SpotTrafficCalculationErr::QueueSizeLargerThanCapacity),
}
}
#[test]
fn spot_traffic_calculation_identity() {
match OnDemand::calculate_spot_traffic(
FixedU128::from_u32(1),
1000,
100,
Perbill::from_percent(10),
Perbill::from_percent(3),
) {
Ok(res) => {
assert_eq!(res, FixedU128::from_u32(1))
},
_ => (),
}
}
#[test]
fn spot_traffic_calculation_u32_max() {
match OnDemand::calculate_spot_traffic(
FixedU128::from_u32(1),
u32::MAX,
u32::MAX,
Perbill::from_percent(100),
Perbill::from_percent(3),
) {
Ok(res) => {
assert_eq!(res, FixedU128::from_u32(1))
},
_ => panic!("Error"),
};
}
#[test]
fn spot_traffic_calculation_u32_traffic_max() {
match OnDemand::calculate_spot_traffic(
FixedU128::from(u128::MAX),
u32::MAX,
u32::MAX,
Perbill::from_percent(1),
Perbill::from_percent(1),
) {
Ok(res) => assert_eq!(res, FixedU128::from(u128::MAX)),
_ => panic!("Error"),
};
}
#[test]
fn sustained_target_increases_spot_traffic() {
let mut traffic = FixedU128::from_u32(1u32);
for _ in 0..50 {
traffic = OnDemand::calculate_spot_traffic(
traffic,
100,
12,
Perbill::from_percent(10),
Perbill::from_percent(100),
)
.unwrap()
}
assert_eq!(traffic, FixedU128::from_inner(2_718_103_312_071_174_015u128))
}
#[test]
fn spot_traffic_can_decrease() {
let traffic = FixedU128::from_u32(100u32);
match OnDemand::calculate_spot_traffic(
traffic,
100u32,
0u32,
Perbill::from_percent(100),
Perbill::from_percent(100),
) {
Ok(new_traffic) => {
assert_eq!(new_traffic, FixedU128::from_inner(50_000_000_000_000_000_000u128))
},
_ => panic!("Error"),
}
}
#[test]
fn spot_traffic_decreases_over_time() {
let mut traffic = FixedU128::from_u32(100u32);
for _ in 0..5 {
traffic = OnDemand::calculate_spot_traffic(
traffic,
100u32,
0u32,
Perbill::from_percent(100),
Perbill::from_percent(100),
)
.unwrap();
println!("{traffic}");
}
assert_eq!(traffic, FixedU128::from_inner(3_125_000_000_000_000_000u128))
}
#[test]
fn spot_traffic_decreases_between_idle_blocks() {
// Testing spot traffic assumptions, but using the mock runtime and default on demand
// configuration values. Ensuring that blocks with no on demand activity (idle)
// decrease traffic.
let para_id = ParaId::from(111);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
// Initialize the parathread and wait for it to be ready.
schedule_blank_para(para_id, ParaKind::Parathread);
assert!(!Paras::is_parathread(para_id));
run_to_block(100, |n| if n == 100 { Some(Default::default()) } else { None });
assert!(Paras::is_parathread(para_id));
// Set the spot traffic to a large number
OnDemand::set_queue_status(QueueStatusType {
traffic: FixedU128::from_u32(10),
..Default::default()
});
assert_eq!(OnDemand::get_queue_status().traffic, FixedU128::from_u32(10));
// Run to block 101 and ensure that the traffic decreases.
run_to_block(101, |n| if n == 100 { Some(Default::default()) } else { None });
assert!(OnDemand::get_queue_status().traffic < FixedU128::from_u32(10));
// Run to block 102 and observe that we've hit the default traffic value.
run_to_block(102, |n| if n == 100 { Some(Default::default()) } else { None });
assert_eq!(OnDemand::get_queue_status().traffic, OnDemand::get_traffic_default_value());
})
}
#[test]
#[allow(deprecated)]
fn place_order_works() {
let alice = 1u64;
let amt = 10_000_000u128;
let para_id = ParaId::from(111);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
// Initialize the parathread and wait for it to be ready.
schedule_blank_para(para_id, ParaKind::Parathread);
assert!(!Paras::is_parathread(para_id));
run_to_block(100, |n| if n == 100 { Some(Default::default()) } else { None });
assert!(Paras::is_parathread(para_id));
// Does not work unsigned
assert_noop!(
OnDemand::place_order_allow_death(RuntimeOrigin::none(), amt, para_id),
BadOrigin
);
// Does not work with max_amount lower than fee
let low_max_amt = 1u128;
assert_noop!(
OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), low_max_amt, para_id,),
Error::<Test>::SpotPriceHigherThanMaxAmount,
);
// Does not work with insufficient balance
assert_noop!(
OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), amt, para_id),
BalancesError::<Test, _>::InsufficientBalance
);
// Works
Balances::make_free_balance_be(&alice, amt);
run_to_block(101, |n| if n == 101 { Some(Default::default()) } else { None });
assert_ok!(OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), amt, para_id));
});
}
#[test]
#[allow(deprecated)]
fn place_order_keep_alive_keeps_alive() {
let alice = 1u64;
let amt = 1u128; // The same as crate::mock's EXISTENTIAL_DEPOSIT
let max_amt = 10_000_000u128;
let para_id = ParaId::from(111);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let config = configuration::ActiveConfig::<Test>::get();
// Initialize the parathread and wait for it to be ready.
schedule_blank_para(para_id, ParaKind::Parathread);
Balances::make_free_balance_be(&alice, amt);
assert!(!Paras::is_parathread(para_id));
run_to_block(100, |n| if n == 100 { Some(Default::default()) } else { None });
assert!(Paras::is_parathread(para_id));
assert_noop!(
OnDemand::place_order_keep_alive(RuntimeOrigin::signed(alice), max_amt, para_id),
BalancesError::<Test, _>::InsufficientBalance
);
Balances::make_free_balance_be(&alice, max_amt);
assert_ok!(OnDemand::place_order_keep_alive(
RuntimeOrigin::signed(alice),
max_amt,
para_id
),);
let queue_status = QueueStatus::<Test>::get();
let spot_price = queue_status.traffic.saturating_mul_int(
config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<Test>>(),
);
assert_eq!(Balances::free_balance(&alice), max_amt.saturating_sub(spot_price));
assert_eq!(
FreeEntries::<Test>::get().pop(),
Some(EnqueuedOrder::new(QueueIndex(0), para_id))
);
});
}
#[test]
fn place_order_with_credits() {
let alice = 1u64;
let initial_credit = 10_000_000u128;
let para_id = ParaId::from(111);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let config = configuration::ActiveConfig::<Test>::get();
// Initialize the parathread and wait for it to be ready.
schedule_blank_para(para_id, ParaKind::Parathread);
OnDemand::credit_account(alice, initial_credit);
assert_eq!(Credits::<Test>::get(alice), initial_credit);
assert!(!Paras::is_parathread(para_id));
run_to_block(100, |n| if n == 100 { Some(Default::default()) } else { None });
assert!(Paras::is_parathread(para_id));
let queue_status = QueueStatus::<Test>::get();
let spot_price = queue_status.traffic.saturating_mul_int(
config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<Test>>(),
);
// Create an order and pay for it with credits.
assert_ok!(OnDemand::place_order_with_credits(
RuntimeOrigin::signed(alice),
initial_credit,
para_id
));
assert_eq!(Credits::<Test>::get(alice), initial_credit.saturating_sub(spot_price));
assert_eq!(
FreeEntries::<Test>::get().pop(),
Some(EnqueuedOrder::new(QueueIndex(0), para_id))
);
// Insufficient credits:
Credits::<Test>::insert(alice, 1u128);
assert_noop!(
OnDemand::place_order_with_credits(
RuntimeOrigin::signed(alice),
1_000_000u128,
para_id
),
Error::<Test>::InsufficientCredits
);
});
}
#[test]
fn pop_assignment_for_core_works() {
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let para_a = ParaId::from(111);
let para_b = ParaId::from(110);
schedule_blank_para(para_a, ParaKind::Parathread);
schedule_blank_para(para_b, ParaKind::Parathread);
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Pop should return none with empty queue
assert_eq!(OnDemand::pop_assignment_for_core(CoreIndex(0)), None);
// Add enough assignments to the order queue.
for _ in 0..2 {
place_order(para_a);
place_order(para_b);
}
// Popped assignments should be for the correct paras and cores
assert_eq!(
OnDemand::pop_assignment_for_core(CoreIndex(0)).map(|a| a.para_id()),
Some(para_a)
);
assert_eq!(
OnDemand::pop_assignment_for_core(CoreIndex(1)).map(|a| a.para_id()),
Some(para_b)
);
assert_eq!(
OnDemand::pop_assignment_for_core(CoreIndex(0)).map(|a| a.para_id()),
Some(para_a)
);
assert_eq!(
OnDemand::pop_assignment_for_core(CoreIndex(1)).map(|a| a.para_id()),
Some(para_b)
);
});
}
#[test]
fn push_back_assignment_works() {
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let para_a = ParaId::from(111);
let para_b = ParaId::from(110);
schedule_blank_para(para_a, ParaKind::Parathread);
schedule_blank_para(para_b, ParaKind::Parathread);
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Add enough assignments to the order queue.
place_order_run_to_101(para_a);
place_order_run_to_101(para_b);
// Pop order a
assert_eq!(OnDemand::pop_assignment_for_core(CoreIndex(0)).unwrap().para_id(), para_a);
// Para a should have affinity for core 0
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 1);
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().core_index, CoreIndex(0));
// Push back order a
OnDemand::push_back_assignment(para_a, CoreIndex(0));
// Para a should have no affinity
assert_eq!(OnDemand::get_affinity_map(para_a).is_none(), true);
// Queue should contain orders a, b. A in front of b.
assert_eq!(OnDemand::pop_assignment_for_core(CoreIndex(0)).unwrap().para_id(), para_a);
assert_eq!(OnDemand::pop_assignment_for_core(CoreIndex(0)).unwrap().para_id(), para_b);
});
}
#[test]
fn affinity_prohibits_parallel_scheduling() {
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let para_a = ParaId::from(111);
let para_b = ParaId::from(222);
schedule_blank_para(para_a, ParaKind::Parathread);
schedule_blank_para(para_b, ParaKind::Parathread);
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// There should be no affinity before starting.
assert!(OnDemand::get_affinity_map(para_a).is_none());
assert!(OnDemand::get_affinity_map(para_b).is_none());
// Add 2 assignments for para_a for every para_b.
place_order_run_to_101(para_a);
place_order_run_to_101(para_a);
place_order_run_to_101(para_b);
// Approximate having 1 core.
for _ in 0..3 {
assert!(OnDemand::pop_assignment_for_core(CoreIndex(0)).is_some());
}
assert!(OnDemand::pop_assignment_for_core(CoreIndex(0)).is_none());
// Affinity on one core is meaningless.
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 2);
assert_eq!(OnDemand::get_affinity_map(para_b).unwrap().count, 1);
assert_eq!(
OnDemand::get_affinity_map(para_a).unwrap().core_index,
OnDemand::get_affinity_map(para_b).unwrap().core_index,
);
// Clear affinity
OnDemand::report_processed(para_a, 0.into());
OnDemand::report_processed(para_a, 0.into());
OnDemand::report_processed(para_b, 0.into());
// Add 2 assignments for para_a for every para_b.
place_order_run_to_101(para_a);
place_order_run_to_101(para_a);
place_order_run_to_101(para_b);
// Approximate having 3 cores. CoreIndex 2 should be unable to obtain an assignment
for _ in 0..3 {
OnDemand::pop_assignment_for_core(CoreIndex(0));
OnDemand::pop_assignment_for_core(CoreIndex(1));
assert!(OnDemand::pop_assignment_for_core(CoreIndex(2)).is_none());
}
// Affinity should be the same as before, but on different cores.
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 2);
assert_eq!(OnDemand::get_affinity_map(para_b).unwrap().count, 1);
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().core_index, CoreIndex(0));
assert_eq!(OnDemand::get_affinity_map(para_b).unwrap().core_index, CoreIndex(1));
// Clear affinity
OnDemand::report_processed(para_a, CoreIndex(0));
OnDemand::report_processed(para_a, CoreIndex(0));
OnDemand::report_processed(para_b, CoreIndex(1));
// There should be no affinity after clearing.
assert!(OnDemand::get_affinity_map(para_a).is_none());
assert!(OnDemand::get_affinity_map(para_b).is_none());
});
}
#[test]
fn affinity_changes_work() {
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let para_a = ParaId::from(111);
let core_index = CoreIndex(0);
schedule_blank_para(para_a, ParaKind::Parathread);
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// There should be no affinity before starting.
assert!(OnDemand::get_affinity_map(para_a).is_none());
// Add enough assignments to the order queue.
for _ in 0..10 {
place_order_run_to_101(para_a);
}
// There should be no affinity before the scheduler pops.
assert!(OnDemand::get_affinity_map(para_a).is_none());
OnDemand::pop_assignment_for_core(core_index);
// Affinity count is 1 after popping.
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 1);
OnDemand::report_processed(para_a, 0.into());
OnDemand::pop_assignment_for_core(core_index);
// Affinity count is 1 after popping with a previous para.
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 1);
for _ in 0..3 {
OnDemand::pop_assignment_for_core(core_index);
}
// Affinity count is 4 after popping 3 times without a previous para.
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 4);
for _ in 0..5 {
OnDemand::report_processed(para_a, 0.into());
assert!(OnDemand::pop_assignment_for_core(core_index).is_some());
}
// Affinity count should still be 4 but queue should be empty.
assert!(OnDemand::pop_assignment_for_core(core_index).is_none());
assert_eq!(OnDemand::get_affinity_map(para_a).unwrap().count, 4);
// Pop 4 times and get to exactly 0 (None) affinity.
for _ in 0..4 {
OnDemand::report_processed(para_a, 0.into());
assert!(OnDemand::pop_assignment_for_core(core_index).is_none());
}
assert!(OnDemand::get_affinity_map(para_a).is_none());
// Decreasing affinity beyond 0 should still be None.
OnDemand::report_processed(para_a, 0.into());
assert!(OnDemand::pop_assignment_for_core(core_index).is_none());
assert!(OnDemand::get_affinity_map(para_a).is_none());
});
}
#[test]
fn new_affinity_for_a_core_must_come_from_free_entries() {
// If affinity count for a core was zero before, and is 1 now, then the entry
// must have come from free_entries.
let teyrchains =
vec![ParaId::from(111), ParaId::from(222), ParaId::from(333), ParaId::from(444)];
let core_indices = vec![CoreIndex(0), CoreIndex(1), CoreIndex(2), CoreIndex(3)];
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
teyrchains.iter().for_each(|chain| {
schedule_blank_para(*chain, ParaKind::Parathread);
});
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Place orders for all chains.
teyrchains.iter().for_each(|chain| {
place_order_run_to_101(*chain);
});
// There are 4 entries in free_entries.
let start_free_entries = OnDemand::get_free_entries().len();
assert_eq!(start_free_entries, 4);
// Pop assignments on all cores.
core_indices.iter().enumerate().for_each(|(n, core_index)| {
// There is no affinity on the core prior to popping.
assert!(OnDemand::get_affinity_entries(*core_index).is_empty());
// There's always an order to be popped for each core.
let free_entries = OnDemand::get_free_entries();
let next_order = free_entries.peek();
// There is no affinity on the paraid prior to popping.
assert!(OnDemand::get_affinity_map(next_order.unwrap().para_id).is_none());
match OnDemand::pop_assignment_for_core(*core_index) {
Some(assignment) => {
// The popped assignment came from free entries.
assert_eq!(start_free_entries - 1 - n, OnDemand::get_free_entries().len());
// The popped assignment has the same para id as the next order.
assert_eq!(assignment.para_id(), next_order.unwrap().para_id);
},
None => panic!("Should not happen"),
}
});
// All entries have been removed from free_entries.
assert!(OnDemand::get_free_entries().is_empty());
// All chains have an affinity count of 1.
teyrchains.iter().for_each(|chain| {
assert_eq!(OnDemand::get_affinity_map(*chain).unwrap().count, 1);
});
});
}
#[test]
#[should_panic]
fn queue_index_ordering_is_unsound_over_max_size() {
// NOTE: Unsoundness proof. If the number goes sufficiently over the max_queue_max_size
// the overflow will cause an opposite comparison to what would be expected.
let max_num = u32::MAX - ON_DEMAND_MAX_QUEUE_MAX_SIZE;
// 0 < some large number.
assert_eq!(QueueIndex(0).cmp(&QueueIndex(max_num + 1)), Ordering::Less);
}
#[test]
fn queue_index_ordering_works() {
// The largest accepted queue size.
let max_num = ON_DEMAND_MAX_QUEUE_MAX_SIZE;
// 0 == 0
assert_eq!(QueueIndex(0).cmp(&QueueIndex(0)), Ordering::Equal);
// 0 < 1
assert_eq!(QueueIndex(0).cmp(&QueueIndex(1)), Ordering::Less);
// 1 > 0
assert_eq!(QueueIndex(1).cmp(&QueueIndex(0)), Ordering::Greater);
// 0 < max_num
assert_eq!(QueueIndex(0).cmp(&QueueIndex(max_num)), Ordering::Less);
// 0 > max_num + 1
assert_eq!(QueueIndex(0).cmp(&QueueIndex(max_num + 1)), Ordering::Less);
// Ordering within the bounds of ON_DEMAND_MAX_QUEUE_MAX_SIZE works.
let mut v = vec![3, 6, 2, 1, 5, 4];
v.sort_by_key(|&num| QueueIndex(num));
assert_eq!(v, vec![1, 2, 3, 4, 5, 6]);
v = vec![max_num, 4, 5, 1, 6];
v.sort_by_key(|&num| QueueIndex(num));
assert_eq!(v, vec![1, 4, 5, 6, max_num]);
// Ordering with an element outside of the bounds of the max size also works.
v = vec![max_num + 2, 0, 6, 2, 1, 5, 4];
v.sort_by_key(|&num| QueueIndex(num));
assert_eq!(v, vec![0, 1, 2, 4, 5, 6, max_num + 2]);
// Numbers way above the max size will overflow
v = vec![u32::MAX - 1, u32::MAX, 6, 2, 1, 5, 4];
v.sort_by_key(|&num| QueueIndex(num));
assert_eq!(v, vec![u32::MAX - 1, u32::MAX, 1, 2, 4, 5, 6]);
}
#[test]
fn reverse_queue_index_does_reverse() {
let mut v = vec![1, 2, 3, 4, 5, 6];
// Basic reversal of a vector.
v.sort_by_key(|&num| ReverseQueueIndex(num));
assert_eq!(v, vec![6, 5, 4, 3, 2, 1]);
// Example from rust docs on `Reverse`. Should work identically.
v.sort_by_key(|&num| (num > 3, ReverseQueueIndex(num)));
assert_eq!(v, vec![3, 2, 1, 6, 5, 4]);
let mut v2 = vec![1, 2, u32::MAX];
v2.sort_by_key(|&num| ReverseQueueIndex(num));
assert_eq!(v2, vec![2, 1, u32::MAX]);
}
#[test]
fn queue_status_size_fn_works() {
// Add orders to the on demand queue, and make sure that they are properly represented
// by the QueueStatusType::size fn.
let teyrchains = vec![ParaId::from(111), ParaId::from(222), ParaId::from(333)];
let core_indices = vec![CoreIndex(0), CoreIndex(1)];
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
teyrchains.iter().for_each(|chain| {
schedule_blank_para(*chain, ParaKind::Parathread);
});
assert_eq!(OnDemand::get_queue_status().size(), 0);
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Place orders for all chains.
teyrchains.iter().for_each(|chain| {
// 2 per chain for a total of 6
place_order_run_to_101(*chain);
place_order_run_to_101(*chain);
});
// 6 orders in free entries
assert_eq!(OnDemand::get_free_entries().len(), 6);
// 6 orders via queue status size
assert_eq!(
OnDemand::get_free_entries().len(),
OnDemand::get_queue_status().size() as usize
);
core_indices.iter().for_each(|core_index| {
OnDemand::pop_assignment_for_core(*core_index);
});
// There should be 2 orders in the scheduler's claimqueue,
// 2 in assorted AffinityMaps and 2 in free.
// ParaId 111
assert_eq!(OnDemand::get_affinity_entries(core_indices[0]).len(), 1);
// ParaId 222
assert_eq!(OnDemand::get_affinity_entries(core_indices[1]).len(), 1);
// Free entries are from ParaId 333
assert_eq!(OnDemand::get_free_entries().len(), 2);
// For a total size of 4.
assert_eq!(OnDemand::get_queue_status().size(), 4)
});
}
#[test]
fn revenue_information_fetching_works() {
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let para_a = ParaId::from(111);
schedule_blank_para(para_a, ParaKind::Parathread);
// Mock assigner sets max revenue history to 10.
run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None });
let revenue = OnDemand::claim_revenue_until(10);
// No revenue should be recorded.
assert_eq!(revenue, 0);
// Place one order
place_order_run_to_blocknumber(para_a, Some(11));
let revenue = OnDemand::get_revenue();
let amt = OnDemand::claim_revenue_until(11);
// Revenue until the current block is still zero as "until" is non-inclusive
assert_eq!(amt, 0);
let amt = OnDemand::claim_revenue_until(12);
// Revenue for a single order should be recorded and shouldn't have been pruned by the
// previous call
assert_eq!(amt, revenue[0]);
run_to_block(12, |n| if n == 12 { Some(Default::default()) } else { None });
let revenue = OnDemand::claim_revenue_until(13);
// No revenue should be recorded.
assert_eq!(revenue, 0);
// Place many orders
place_order(para_a);
place_order(para_a);
run_to_block(13, |n| if n == 13 { Some(Default::default()) } else { None });
place_order(para_a);
run_to_block(14, |n| if n == 14 { Some(Default::default()) } else { None });
let revenue = OnDemand::claim_revenue_until(15);
// All 3 orders should be accounted for.
assert_eq!(revenue, 30_000);
// Place one order
place_order_run_to_blocknumber(para_a, Some(16));
let revenue = OnDemand::claim_revenue_until(15);
// Order is not in range of the revenue_until call
assert_eq!(revenue, 0);
run_to_block(20, |n| if n == 20 { Some(Default::default()) } else { None });
let revenue = OnDemand::claim_revenue_until(21);
assert_eq!(revenue, 10_000);
// Make sure overdue revenue is accumulated
for i in 21..=35 {
run_to_block(i, |n| if n % 10 == 0 { Some(Default::default()) } else { None });
place_order(para_a);
}
let revenue = OnDemand::claim_revenue_until(36);
assert_eq!(revenue, 150_000);
});
}
#[test]
fn pot_account_is_immortal() {
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let para_a = ParaId::from(111);
let pot = OnDemand::account_id();
assert!(!System::account_exists(&pot));
schedule_blank_para(para_a, ParaKind::Parathread);
// Mock assigner sets max revenue history to 10.
run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None });
place_order_run_to_blocknumber(para_a, Some(12));
let purchase_revenue = Balances::free_balance(&pot);
assert!(purchase_revenue > 0);
run_to_block(15, |_| None);
let _imb = <Test as on_demand::Config>::Currency::withdraw(
&pot,
purchase_revenue,
WithdrawReasons::FEE,
ExistenceRequirement::AllowDeath,
);
assert_eq!(Balances::free_balance(&pot), 0);
assert!(System::account_exists(&pot));
assert_eq!(System::providers(&pot), 1);
// One more cycle to make sure providers are not increased on every transition from zero
run_to_block(20, |n| if n == 20 { Some(Default::default()) } else { None });
place_order_run_to_blocknumber(para_a, Some(22));
let purchase_revenue = Balances::free_balance(&pot);
assert!(purchase_revenue > 0);
run_to_block(25, |_| None);
let _imb = <Test as on_demand::Config>::Currency::withdraw(
&pot,
purchase_revenue,
WithdrawReasons::FEE,
ExistenceRequirement::AllowDeath,
);
assert_eq!(Balances::free_balance(&pot), 0);
assert!(System::account_exists(&pot));
assert_eq!(System::providers(&pot), 1);
});
}
@@ -0,0 +1,238 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Pezkuwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
//! On demand module types.
use super::{alloc, pallet::Config};
use alloc::collections::BinaryHeap;
use core::cmp::{Ord, Ordering, PartialOrd};
use frame_support::{
pallet_prelude::{Decode, Encode, RuntimeDebug, TypeInfo},
traits::Currency,
};
use pezkuwi_primitives::{CoreIndex, Id as ParaId, ON_DEMAND_MAX_QUEUE_MAX_SIZE};
use sp_runtime::FixedU128;
/// Shorthand for the Balance type the runtime is using.
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
/// Meta data for full queue.
///
/// This includes elements with affinity and free entries.
///
/// The actual queue is implemented via multiple priority queues. One for each core, for entries
/// which currently have a core affinity and one free queue, with entries without any affinity yet.
///
/// The design aims to have most queue accessess be O(1) or O(log(N)). Absolute worst case is O(N).
/// Importantly this includes all accessess that happen in a single block. Even with 50 cores, the
/// total complexity of all operations in the block should maintain above complexities. In
/// particular O(N) stays O(N), it should never be O(N*cores).
///
/// More concrete rundown on complexity:
///
/// - insert: O(1) for placing an order, O(log(N)) for push backs.
/// - pop_assignment_for_core: O(log(N)), O(N) worst case: Can only happen for one core, next core
/// is already less work.
/// - report_processed & push back: If affinity dropped to 0, then O(N) in the worst case. Again
/// this divides per core.
///
/// Reads still exist, also improved slightly, but worst case we fetch all entries.
#[derive(Encode, Decode, TypeInfo)]
pub struct QueueStatusType {
/// Last calculated traffic value.
pub traffic: FixedU128,
/// The next index to use.
pub next_index: QueueIndex,
/// Smallest index still in use.
///
/// In case of a completely empty queue (free + affinity queues), `next_index - smallest_index
/// == 0`.
pub smallest_index: QueueIndex,
/// Indices that have been freed already.
///
/// But have a hole to `smallest_index`, so we can not yet bump `smallest_index`. This binary
/// heap is roughly bounded in the number of on demand cores:
///
/// For a single core, elements will always be processed in order. With each core added, a
/// level of out of order execution is added.
pub freed_indices: BinaryHeap<ReverseQueueIndex>,
}
impl Default for QueueStatusType {
fn default() -> QueueStatusType {
QueueStatusType {
traffic: FixedU128::default(),
next_index: QueueIndex(0),
smallest_index: QueueIndex(0),
freed_indices: BinaryHeap::new(),
}
}
}
impl QueueStatusType {
/// How many orders are queued in total?
///
/// This includes entries which have core affinity.
pub fn size(&self) -> u32 {
self.next_index
.0
.overflowing_sub(self.smallest_index.0)
.0
.saturating_sub(self.freed_indices.len() as u32)
}
/// Get current next index
///
/// to use for an element newly pushed to the back of the queue.
pub fn push_back(&mut self) -> QueueIndex {
let QueueIndex(next_index) = self.next_index;
self.next_index = QueueIndex(next_index.overflowing_add(1).0);
QueueIndex(next_index)
}
/// Push something to the front of the queue
pub fn push_front(&mut self) -> QueueIndex {
self.smallest_index = QueueIndex(self.smallest_index.0.overflowing_sub(1).0);
self.smallest_index
}
/// The given index is no longer part of the queue.
///
/// This updates `smallest_index` if need be.
pub fn consume_index(&mut self, removed_index: QueueIndex) {
if removed_index != self.smallest_index {
self.freed_indices.push(removed_index.reverse());
return;
}
let mut index = self.smallest_index.0.overflowing_add(1).0;
// Even more to advance?
while self.freed_indices.peek() == Some(&ReverseQueueIndex(index)) {
index = index.overflowing_add(1).0;
self.freed_indices.pop();
}
self.smallest_index = QueueIndex(index);
}
}
/// Type used for priority indices.
// NOTE: The `Ord` implementation for this type is unsound in the general case.
// Do not use it for anything but it's intended purpose.
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq, Copy)]
pub struct QueueIndex(pub u32);
/// QueueIndex with reverse ordering.
///
/// Same as `Reverse(QueueIndex)`, but with all the needed traits implemented.
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq, Copy)]
pub struct ReverseQueueIndex(pub u32);
impl QueueIndex {
fn reverse(self) -> ReverseQueueIndex {
ReverseQueueIndex(self.0)
}
}
impl Ord for QueueIndex {
fn cmp(&self, other: &Self) -> Ordering {
let diff = self.0.overflowing_sub(other.0).0;
if diff == 0 {
Ordering::Equal
} else if diff <= ON_DEMAND_MAX_QUEUE_MAX_SIZE {
Ordering::Greater
} else {
Ordering::Less
}
}
}
impl PartialOrd for QueueIndex {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ReverseQueueIndex {
fn cmp(&self, other: &Self) -> Ordering {
QueueIndex(other.0).cmp(&QueueIndex(self.0))
}
}
impl PartialOrd for ReverseQueueIndex {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(&other))
}
}
/// Internal representation of an order after it has been enqueued already.
///
/// This data structure is provided for a min BinaryHeap (Ord compares in reverse order with regards
/// to its elements)
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq)]
pub struct EnqueuedOrder {
pub para_id: ParaId,
pub idx: QueueIndex,
}
impl EnqueuedOrder {
pub fn new(idx: QueueIndex, para_id: ParaId) -> Self {
Self { idx, para_id }
}
}
impl PartialOrd for EnqueuedOrder {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match other.idx.partial_cmp(&self.idx) {
Some(Ordering::Equal) => other.para_id.partial_cmp(&self.para_id),
o => o,
}
}
}
impl Ord for EnqueuedOrder {
fn cmp(&self, other: &Self) -> Ordering {
match other.idx.cmp(&self.idx) {
Ordering::Equal => other.para_id.cmp(&self.para_id),
o => o,
}
}
}
/// Keeps track of how many assignments a scheduler currently has at a specific `CoreIndex` for a
/// specific `ParaId`.
#[derive(Encode, Decode, Default, Clone, Copy, TypeInfo)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug))]
pub struct CoreAffinityCount {
pub core_index: CoreIndex,
pub count: u32,
}
/// An indicator as to which end of the `OnDemandQueue` an assignment will be placed.
#[cfg_attr(test, derive(RuntimeDebug))]
pub enum QueuePushDirection {
Back,
Front,
}
/// Errors that can happen during spot traffic calculation.
#[derive(PartialEq, RuntimeDebug)]
pub enum SpotTrafficCalculationErr {
/// The order queue capacity is at 0.
QueueCapacityIsZero,
/// The queue size is larger than the queue capacity.
QueueSizeLargerThanCapacity,
/// Arithmetic error during division, either division by 0 or over/underflow.
Division,
}