Coretime Feature branch (relay chain) (#1694)

Also fixes: https://github.com/paritytech/polkadot-sdk/issues/1417

- [x] CoreIndex -> AssignmentProvider mapping will be able to change any
time.
- [x] Implement
- [x] Provide Migrations
- [x] Add and fix tests
- [x] Implement bulk assigner logic
- [x] bulk assigner tests
- [x] Port over current assigner to use bulk designer (+ share on-demand
with bulk): top-level assigner has core ranges: legacy, bulk
- [x] Adjust migrations to reflect new assigner structure
- [x] Move migration code to Assignment code directly and make it
recursive (make it possible to skip releases) -> follow up ticket.
- [x] Test migrations
- [x] Add migration PR to runtimes repo -> follow up ticket.
- [x] Wire up with actual UMP messages
- [x] Write PR docs

---------

Co-authored-by: eskimor <eskimor@no-such-url.com>
Co-authored-by: Bradley Olson <34992650+BradleyOlson64@users.noreply.github.com>
Co-authored-by: BradleyOlson64 <lotrftw9@gmail.com>
Co-authored-by: Anton Vilhelm Ásgeirsson <antonva@users.noreply.github.com>
Co-authored-by: antonva <anton.asgeirsson@parity.io>
Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: Marcin S. <marcin@realemail.net>
Co-authored-by: Bastian Köcher <info@kchr.de>
Co-authored-by: command-bot <>
This commit is contained in:
eskimor
2023-12-21 19:06:58 +01:00
committed by GitHub
parent 18d53dbf91
commit 69434d9a32
71 changed files with 4059 additions and 1213 deletions
-112
View File
@@ -1,112 +0,0 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! The Polkadot multiplexing assignment provider.
//! Provides blockspace assignments for both bulk and on demand parachains.
use frame_system::pallet_prelude::BlockNumberFor;
use primitives::{CoreIndex, Id as ParaId};
use crate::{
configuration, paras,
scheduler::common::{Assignment, AssignmentProvider, AssignmentProviderConfig},
};
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + configuration::Config + paras::Config {
type ParachainsAssignmentProvider: AssignmentProvider<BlockNumberFor<Self>>;
type OnDemandAssignmentProvider: AssignmentProvider<BlockNumberFor<Self>>;
}
}
// Aliases to make the impl more readable.
type ParachainAssigner<T> = <T as Config>::ParachainsAssignmentProvider;
type OnDemandAssigner<T> = <T as Config>::OnDemandAssignmentProvider;
impl<T: Config> Pallet<T> {
// Helper fn for the AssignmentProvider implementation.
// Assumes that the first allocation of cores is to bulk parachains.
// This function will return false if there are no cores assigned to the bulk parachain
// assigner.
fn is_bulk_core(core_idx: &CoreIndex) -> bool {
let parachain_cores =
<ParachainAssigner<T> as AssignmentProvider<BlockNumberFor<T>>>::session_core_count();
core_idx.0 < parachain_cores
}
}
impl<T: Config> AssignmentProvider<BlockNumberFor<T>> for Pallet<T> {
fn session_core_count() -> u32 {
let parachain_cores =
<ParachainAssigner<T> as AssignmentProvider<BlockNumberFor<T>>>::session_core_count();
let on_demand_cores =
<OnDemandAssigner<T> as AssignmentProvider<BlockNumberFor<T>>>::session_core_count();
parachain_cores.saturating_add(on_demand_cores)
}
/// Pops an `Assignment` from a specified `CoreIndex`
fn pop_assignment_for_core(
core_idx: CoreIndex,
concluded_para: Option<ParaId>,
) -> Option<Assignment> {
if Pallet::<T>::is_bulk_core(&core_idx) {
<ParachainAssigner<T> as AssignmentProvider<BlockNumberFor<T>>>::pop_assignment_for_core(
core_idx,
concluded_para,
)
} else {
<OnDemandAssigner<T> as AssignmentProvider<BlockNumberFor<T>>>::pop_assignment_for_core(
core_idx,
concluded_para,
)
}
}
fn push_assignment_for_core(core_idx: CoreIndex, assignment: Assignment) {
if Pallet::<T>::is_bulk_core(&core_idx) {
<ParachainAssigner<T> as AssignmentProvider<BlockNumberFor<T>>>::push_assignment_for_core(
core_idx, assignment,
)
} else {
<OnDemandAssigner<T> as AssignmentProvider<BlockNumberFor<T>>>::push_assignment_for_core(
core_idx, assignment,
)
}
}
fn get_provider_config(core_idx: CoreIndex) -> AssignmentProviderConfig<BlockNumberFor<T>> {
if Pallet::<T>::is_bulk_core(&core_idx) {
<ParachainAssigner<T> as AssignmentProvider<BlockNumberFor<T>>>::get_provider_config(
core_idx,
)
} else {
<OnDemandAssigner<T> as AssignmentProvider<BlockNumberFor<T>>>::get_provider_config(
core_idx,
)
}
}
}
@@ -0,0 +1,87 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. 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 sp_runtime::Perbill;
use 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<primitives::Id>,
}
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.coretime_cores = self.on_demand_cores;
config.on_demand_base_fee = self.on_demand_base_fee;
config.on_demand_fee_variability = self.on_demand_fee_variability;
config.on_demand_queue_max_size = self.on_demand_max_queue_size;
config.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,496 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! The parachain coretime assignment module.
//!
//! Handles scheduling of assignments coming from the coretime/broker chain. For on-demand
//! assignments it relies on the separate on-demand assignment provider, where it forwards requests
//! to.
//!
//! `CoreDescriptor` contains pointers to the begin and the end of a list of schedules, together
//! with the currently active assignments.
mod mock_helpers;
#[cfg(test)]
mod tests;
use crate::{
assigner_on_demand, configuration,
paras::AssignCoretime,
scheduler::common::{Assignment, AssignmentProvider, AssignmentProviderConfig},
ParaId,
};
use frame_support::{defensive, pallet_prelude::*};
use frame_system::pallet_prelude::*;
use pallet_broker::CoreAssignment;
use primitives::CoreIndex;
use sp_runtime::traits::{One, Saturating};
use sp_std::prelude::*;
pub use pallet::*;
/// Fraction expressed as a nominator with an assumed denominator of 57,600.
#[derive(RuntimeDebug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, TypeInfo)]
pub struct PartsOf57600(u16);
impl PartsOf57600 {
pub const ZERO: Self = Self(0);
pub const FULL: Self = Self(57600);
pub fn new_saturating(v: u16) -> Self {
Self::ZERO.saturating_add(Self(v))
}
pub fn is_full(&self) -> bool {
*self == Self::FULL
}
pub fn saturating_add(self, rhs: Self) -> Self {
let inner = self.0.saturating_add(rhs.0);
if inner > 57600 {
Self(57600)
} else {
Self(inner)
}
}
pub fn saturating_sub(self, rhs: Self) -> Self {
Self(self.0.saturating_sub(rhs.0))
}
pub fn checked_add(self, rhs: Self) -> Option<Self> {
let inner = self.0.saturating_add(rhs.0);
if inner > 57600 {
None
} else {
Some(Self(inner))
}
}
}
/// Assignments as they are scheduled by block number
///
/// for a particular core.
#[derive(Encode, Decode, TypeInfo)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug))]
struct Schedule<N> {
// Original assignments
assignments: Vec<(CoreAssignment, PartsOf57600)>,
/// When do our assignments become invalid, if at all?
///
/// If this is `Some`, then this `CoreState` will be dropped at that block number. If this is
/// `None`, then we will keep serving our core assignments in a circle until a new set of
/// assignments is scheduled.
end_hint: Option<N>,
/// The next queued schedule for this core.
///
/// Schedules are forming a queue.
next_schedule: Option<N>,
}
/// Descriptor for a core.
///
/// Contains pointers to first and last schedule into `CoreSchedules` for that core and keeps track
/// of the currently active work as well.
#[derive(Encode, Decode, TypeInfo, Default)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug, Clone))]
struct CoreDescriptor<N> {
/// Meta data about the queued schedules for this core.
queue: Option<QueueDescriptor<N>>,
/// Currently performed work.
current_work: Option<WorkState<N>>,
}
/// Pointers into `CoreSchedules` for a particular core.
///
/// Schedules in `CoreSchedules` form a queue. `Schedule::next_schedule` always pointing to the next
/// item.
#[derive(Encode, Decode, TypeInfo, Copy, Clone)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug))]
struct QueueDescriptor<N> {
/// First scheduled item, that is not yet active.
first: N,
/// Last scheduled item.
last: N,
}
#[derive(Encode, Decode, TypeInfo)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug, Clone))]
struct WorkState<N> {
/// Assignments with current state.
///
/// Assignments and book keeping on how much has been served already. We keep track of serviced
/// assignments in order to adhere to the specified ratios.
assignments: Vec<(CoreAssignment, AssignmentState)>,
/// When do our assignments become invalid if at all?
///
/// If this is `Some`, then this `CoreState` will be dropped at that block number. If this is
/// `None`, then we will keep serving our core assignments in a circle until a new set of
/// assignments is scheduled.
end_hint: Option<N>,
/// Position in the assignments we are currently in.
///
/// Aka which core assignment will be popped next on
/// `AssignmentProvider::pop_assignment_for_core`.
pos: u16,
/// Step width
///
/// How much we subtract from `AssignmentState::remaining` for a core served.
step: PartsOf57600,
}
#[derive(Encode, Decode, TypeInfo)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug, Clone, Copy))]
struct AssignmentState {
/// Ratio of the core this assignment has.
///
/// As initially received via `assign_core`.
ratio: PartsOf57600,
/// How many parts are remaining in this round?
///
/// At the end of each round (in preparation for the next), ratio will be added to remaining.
/// Then every time we get scheduled we subtract a core worth of points. Once we reach 0 or a
/// number lower than what a core is worth (`CoreState::step` size), we move on to the next
/// item in the `Vec`.
///
/// The first round starts with remaining = ratio.
remaining: PartsOf57600,
}
impl<N> From<Schedule<N>> for WorkState<N> {
fn from(schedule: Schedule<N>) -> Self {
let Schedule { assignments, end_hint, next_schedule: _ } = schedule;
let step =
if let Some(min_step_assignment) = assignments.iter().min_by(|a, b| a.1.cmp(&b.1)) {
min_step_assignment.1
} else {
// Assignments empty, should not exist. In any case step size does not matter here:
log::debug!("assignments of a `Schedule` should never be empty.");
PartsOf57600(1)
};
let assignments = assignments
.into_iter()
.map(|(a, ratio)| (a, AssignmentState { ratio, remaining: ratio }))
.collect();
Self { assignments, end_hint, pos: 0, step }
}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config:
frame_system::Config + configuration::Config + assigner_on_demand::Config
{
}
/// Scheduled assignment sets.
///
/// Assignments as of the given block number. They will go into state once the block number is
/// reached (and replace whatever was in there before).
#[pallet::storage]
pub(super) type CoreSchedules<T: Config> = StorageMap<
_,
Twox256,
(BlockNumberFor<T>, CoreIndex),
Schedule<BlockNumberFor<T>>,
OptionQuery,
>;
/// Assignments which are currently active.
///
/// They will be picked from `PendingAssignments` once we reach the scheduled block number in
/// `PendingAssignments`.
#[pallet::storage]
pub(super) type CoreDescriptors<T: Config> = StorageMap<
_,
Twox256,
CoreIndex,
CoreDescriptor<BlockNumberFor<T>>,
ValueQuery,
GetDefault,
>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::error]
pub enum Error<T> {
AssignmentsEmpty,
/// Assignments together exceeded 57600.
OverScheduled,
/// Assignments together less than 57600
UnderScheduled,
/// assign_core is only allowed to append new assignments at the end of already existing
/// ones.
DisallowedInsert,
/// Tried to insert a schedule for the same core and block number as an existing schedule
DuplicateInsert,
/// Tried to add an unsorted set of assignments
AssignmentsNotSorted,
}
}
impl<T: Config> AssignmentProvider<BlockNumberFor<T>> for Pallet<T> {
fn pop_assignment_for_core(core_idx: CoreIndex) -> Option<Assignment> {
let now = <frame_system::Pallet<T>>::block_number();
CoreDescriptors::<T>::mutate(core_idx, |core_state| {
Self::ensure_workload(now, core_idx, core_state);
let work_state = core_state.current_work.as_mut()?;
// Wrap around:
work_state.pos = work_state.pos % work_state.assignments.len() as u16;
let (a_type, a_state) = &mut work_state
.assignments
.get_mut(work_state.pos as usize)
.expect("We limited pos to the size of the vec one line above. qed");
// advance for next pop:
a_state.remaining = a_state.remaining.saturating_sub(work_state.step);
if a_state.remaining < work_state.step {
// Assignment exhausted, need to move to the next and credit remaining for
// next round.
work_state.pos += 1;
// Reset to ratio + still remaining "credits":
a_state.remaining = a_state.remaining.saturating_add(a_state.ratio);
}
match a_type {
CoreAssignment::Idle => None,
CoreAssignment::Pool =>
assigner_on_demand::Pallet::<T>::pop_assignment_for_core(core_idx),
CoreAssignment::Task(para_id) => Some(Assignment::Bulk((*para_id).into())),
}
})
}
fn report_processed(assignment: Assignment) {
match assignment {
Assignment::Pool { para_id, core_index } =>
assigner_on_demand::Pallet::<T>::report_processed(para_id, core_index),
Assignment::Bulk(_) => {},
}
}
/// Push an assignment back to the front of the queue.
///
/// The assignment has not been processed yet. Typically used on session boundaries.
/// Parameters:
/// - `assignment`: The on demand assignment.
fn push_back_assignment(assignment: Assignment) {
match assignment {
Assignment::Pool { para_id, core_index } =>
assigner_on_demand::Pallet::<T>::push_back_assignment(para_id, core_index),
Assignment::Bulk(_) => {
// Session changes are rough. We just drop assignments that did not make it on a
// session boundary. This seems sensible as bulk is region based. Meaning, even if
// we made the effort catching up on those dropped assignments, this would very
// likely lead to other assignments not getting served at the "end" (when our
// assignment set gets replaced).
},
}
}
fn get_provider_config(_core_idx: CoreIndex) -> AssignmentProviderConfig<BlockNumberFor<T>> {
let config = <configuration::Pallet<T>>::config();
AssignmentProviderConfig {
max_availability_timeouts: config.on_demand_retries,
ttl: config.on_demand_ttl,
}
}
#[cfg(any(feature = "runtime-benchmarks", test))]
fn get_mock_assignment(_: CoreIndex, para_id: primitives::Id) -> Assignment {
// Given that we are not tracking anything in `Bulk` assignments, it is safe to always
// return a bulk assignment.
Assignment::Bulk(para_id)
}
fn session_core_count() -> u32 {
let config = <configuration::Pallet<T>>::config();
config.coretime_cores
}
}
impl<T: Config> Pallet<T> {
/// Ensure given workload for core is up to date.
fn ensure_workload(
now: BlockNumberFor<T>,
core_idx: CoreIndex,
descriptor: &mut CoreDescriptor<BlockNumberFor<T>>,
) {
// Workload expired?
if descriptor
.current_work
.as_ref()
.and_then(|w| w.end_hint)
.map_or(false, |e| e <= now)
{
descriptor.current_work = None;
}
let Some(queue) = descriptor.queue else {
// No queue.
return
};
let mut next_scheduled = queue.first;
if next_scheduled > now {
// Not yet ready.
return
}
// Update is needed:
let update = loop {
let Some(update) = CoreSchedules::<T>::take((next_scheduled, core_idx)) else {
break None
};
// Still good?
if update.end_hint.map_or(true, |e| e > now) {
break Some(update)
}
// Move on if possible:
if let Some(n) = update.next_schedule {
next_scheduled = n;
} else {
break None
}
};
let new_first = update.as_ref().and_then(|u| u.next_schedule);
descriptor.current_work = update.map(Into::into);
descriptor.queue = new_first.map(|new_first| {
QueueDescriptor {
first: new_first,
// `last` stays unaffected, if not empty:
last: queue.last,
}
});
}
/// Append another assignment for a core.
///
/// Important only appending is allowed. Meaning, all already existing assignments must have a
/// begin smaller than the one passed here. This restriction exists, because it makes the
/// insertion O(1) and the author could not think of a reason, why this restriction should be
/// causing any problems. Inserting arbitrarily causes a `DispatchError::DisallowedInsert`
/// error. This restriction could easily be lifted if need be and in fact an implementation is
/// available
/// [here](https://github.com/paritytech/polkadot-sdk/pull/1694/commits/c0c23b01fd2830910cde92c11960dad12cdff398#diff-0c85a46e448de79a5452395829986ee8747e17a857c27ab624304987d2dde8baR386).
/// The problem is that insertion complexity then depends on the size of the existing queue,
/// which makes determining weights hard and could lead to issues like overweight blocks (at
/// least in theory).
pub fn assign_core(
core_idx: CoreIndex,
begin: BlockNumberFor<T>,
assignments: Vec<(CoreAssignment, PartsOf57600)>,
end_hint: Option<BlockNumberFor<T>>,
) -> Result<(), DispatchError> {
// There should be at least one assignment.
ensure!(!assignments.is_empty(), Error::<T>::AssignmentsEmpty);
// Checking for sort and unique manually, since we don't have access to iterator tools.
// This way of checking uniqueness only works since we also check sortedness.
assignments.iter().map(|x| &x.0).try_fold(None, |prev, cur| {
if prev.map_or(false, |p| p >= cur) {
Err(Error::<T>::AssignmentsNotSorted)
} else {
Ok(Some(cur))
}
})?;
// Check that the total parts between all assignments are equal to 57600
let parts_sum = assignments
.iter()
.map(|assignment| assignment.1)
.try_fold(PartsOf57600::ZERO, |sum, parts| {
sum.checked_add(parts).ok_or(Error::<T>::OverScheduled)
})?;
ensure!(parts_sum.is_full(), Error::<T>::UnderScheduled);
CoreDescriptors::<T>::mutate(core_idx, |core_descriptor| {
let new_queue = match core_descriptor.queue {
Some(queue) => {
ensure!(begin > queue.last, Error::<T>::DisallowedInsert);
CoreSchedules::<T>::try_mutate((queue.last, core_idx), |schedule| {
if let Some(schedule) = schedule.as_mut() {
debug_assert!(schedule.next_schedule.is_none(), "queue.end was supposed to be the end, so the next item must be `None`!");
schedule.next_schedule = Some(begin);
} else {
defensive!("Queue end entry does not exist?");
}
CoreSchedules::<T>::try_mutate((begin, core_idx), |schedule| {
// It should already be impossible to overwrite an existing schedule due
// to strictly increasing block number. But we check here for safety and
// in case the design changes.
ensure!(schedule.is_none(), Error::<T>::DuplicateInsert);
*schedule =
Some(Schedule { assignments, end_hint, next_schedule: None });
Ok::<(), DispatchError>(())
})?;
Ok::<(), DispatchError>(())
})?;
QueueDescriptor { first: queue.first, last: begin }
},
None => {
// Queue empty, just insert:
CoreSchedules::<T>::insert(
(begin, core_idx),
Schedule { assignments, end_hint, next_schedule: None },
);
QueueDescriptor { first: begin, last: begin }
},
};
core_descriptor.queue = Some(new_queue);
Ok(())
})
}
}
impl<T: Config> AssignCoretime for Pallet<T> {
fn assign_coretime(id: ParaId) -> DispatchResult {
let current_block = frame_system::Pallet::<T>::block_number();
// Add a new core and assign the para to it.
let mut config = <configuration::Pallet<T>>::config();
let core = config.coretime_cores;
config.coretime_cores.saturating_inc();
// `assign_coretime` is only called at genesis or by root, so setting the active
// config here is fine.
configuration::Pallet::<T>::force_set_active_config(config);
let begin = current_block + One::one();
let assignment = vec![(pallet_broker::CoreAssignment::Task(id.into()), PartsOf57600::FULL)];
Pallet::<T>::assign_core(CoreIndex(core), begin, assignment, None)
}
}
@@ -0,0 +1,817 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use crate::{
assigner_coretime::{mock_helpers::GenesisConfigBuilder, pallet::Error, Schedule},
initializer::SessionChangeNotification,
mock::{
new_test_ext, Balances, CoretimeAssigner, OnDemandAssigner, Paras, ParasShared,
RuntimeOrigin, Scheduler, System, Test,
},
paras::{ParaGenesisArgs, ParaKind},
scheduler::common::Assignment,
};
use frame_support::{assert_noop, assert_ok, pallet_prelude::*, traits::Currency};
use pallet_broker::TaskId;
use primitives::{BlockNumber, Id as ParaId, SessionIndex, ValidationCode};
use sp_std::collections::btree_map::BTreeMap;
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);
// In the real runtime this is expected to be called by the `InclusionInherent` pallet.
Scheduler::free_cores_and_fill_claimqueue(BTreeMap::new(), b + 1);
}
}
fn default_test_assignments() -> Vec<(CoreAssignment, PartsOf57600)> {
vec![(CoreAssignment::Idle, PartsOf57600::FULL)]
}
fn default_test_schedule() -> Schedule<BlockNumberFor<Test>> {
Schedule { assignments: default_test_assignments(), end_hint: None, next_schedule: None }
}
#[test]
// Should create new QueueDescriptor and add new schedule to CoreSchedules
fn assign_core_works_with_no_prior_schedule() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Call assign_core
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
default_test_assignments(),
None,
));
// Check CoreSchedules
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(11u32), core_idx)),
Some(default_test_schedule())
);
// Check QueueDescriptor
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.queue
.as_ref()
.and_then(|q| Some(q.first)),
Some(BlockNumberFor::<Test>::from(11u32))
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx).queue.as_ref().and_then(|q| Some(q.last)),
Some(BlockNumberFor::<Test>::from(11u32))
);
});
}
#[test]
fn end_hint_is_properly_honored() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
vec![(CoreAssignment::Task(1), PartsOf57600::FULL)],
Some(15u32),
));
assert!(
CoretimeAssigner::pop_assignment_for_core(core_idx).is_none(),
"No assignment yet in effect"
);
run_to_block(11, |_| None);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(1.into())),
"Assignment should now be present"
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(1.into())),
"Nothing changed, assignment should still be present"
);
run_to_block(15, |_| None);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
None,
"Assignment should now be gone"
);
// Insert assignment that is already dead:
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
vec![(CoreAssignment::Task(1), PartsOf57600::FULL)],
Some(15u32),
));
// Core should still be empty:
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
None,
"Assignment should now be gone"
);
});
}
#[test]
// Should update last in QueueDescriptor and add new schedule to CoreSchedules
fn assign_core_works_with_prior_schedule() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
let default_with_next_schedule =
Schedule { next_schedule: Some(15u32), ..default_test_schedule() };
// Call assign_core twice
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(15u32),
default_test_assignments(),
None,
));
// Check CoreSchedules for two entries
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(11u32), core_idx)),
Some(default_with_next_schedule)
);
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(15u32), core_idx)),
Some(default_test_schedule())
);
// Check QueueDescriptor
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.queue
.as_ref()
.and_then(|q| Some(q.first)),
Some(BlockNumberFor::<Test>::from(11u32))
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx).queue.as_ref().and_then(|q| Some(q.last)),
Some(BlockNumberFor::<Test>::from(15u32))
);
});
}
#[test]
// Invariants: We assume that CoreSchedules is append only and consumed. In other words new
// schedules inserted for a core must have a higher block number than all of the already existing
// schedules.
fn assign_core_enforces_higher_block_number() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Call assign core twice to establish some schedules
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(12u32),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(15u32),
default_test_assignments(),
None,
));
// Call assign core with block number before QueueDescriptor first, expecting an error
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
default_test_assignments(),
None,
),
Error::<Test>::DisallowedInsert
);
// Call assign core with block number between already scheduled assignments, expecting an
// error
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(13u32),
default_test_assignments(),
None,
),
Error::<Test>::DisallowedInsert
);
});
}
#[test]
fn assign_core_enforces_well_formed_schedule() {
let para_id = ParaId::from(1u32);
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
let empty_assignments: Vec<(CoreAssignment, PartsOf57600)> = vec![];
let overscheduled = vec![
(CoreAssignment::Pool, PartsOf57600::FULL),
(CoreAssignment::Task(para_id.into()), PartsOf57600::FULL),
];
let underscheduled = vec![(CoreAssignment::Pool, PartsOf57600(30000))];
let not_unique = vec![
(CoreAssignment::Pool, PartsOf57600::FULL / 2),
(CoreAssignment::Pool, PartsOf57600::FULL / 2),
];
let not_sorted = vec![
(CoreAssignment::Task(para_id.into()), PartsOf57600(19200)),
(CoreAssignment::Pool, PartsOf57600(19200)),
(CoreAssignment::Idle, PartsOf57600(19200)),
];
// Attempting assign_core with malformed assignments such that all error cases
// are tested
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
empty_assignments,
None,
),
Error::<Test>::AssignmentsEmpty
);
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
overscheduled,
None,
),
Error::<Test>::OverScheduled
);
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
underscheduled,
None,
),
Error::<Test>::UnderScheduled
);
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
not_unique,
None,
),
Error::<Test>::AssignmentsNotSorted
);
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
not_sorted,
None,
),
Error::<Test>::AssignmentsNotSorted
);
});
}
#[test]
fn next_schedule_always_points_to_next_work_plan_item() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
let start_1 = 15u32;
let start_2 = 20u32;
let start_3 = 25u32;
let start_4 = 30u32;
let start_5 = 35u32;
let expected_schedule_3 =
Schedule { next_schedule: Some(start_4), ..default_test_schedule() };
let expected_schedule_4 =
Schedule { next_schedule: Some(start_5), ..default_test_schedule() };
let expected_schedule_5 = default_test_schedule();
// Call assign_core for each of five schedules
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_1),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_2),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_3),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_4),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_5),
default_test_assignments(),
None,
));
// Rotate through the first two schedules
run_to_block(start_1, |n| if n == start_1 { Some(Default::default()) } else { None });
CoretimeAssigner::pop_assignment_for_core(core_idx);
run_to_block(start_2, |n| if n == start_2 { Some(Default::default()) } else { None });
CoretimeAssigner::pop_assignment_for_core(core_idx);
// Use saved starting block numbers to check that schedules chain
// together correctly
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(start_3), core_idx)),
Some(expected_schedule_3)
);
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(start_4), core_idx)),
Some(expected_schedule_4)
);
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(start_5), core_idx)),
Some(expected_schedule_5)
);
// Check QueueDescriptor
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.queue
.as_ref()
.and_then(|q| Some(q.first)),
Some(start_3)
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx).queue.as_ref().and_then(|q| Some(q.last)),
Some(start_5)
);
});
}
#[test]
fn ensure_workload_works() {
let core_idx = CoreIndex(0);
let test_assignment_state =
AssignmentState { ratio: PartsOf57600::FULL, remaining: PartsOf57600::FULL };
let empty_descriptor: CoreDescriptor<BlockNumberFor<Test>> =
CoreDescriptor { queue: None, current_work: None };
let assignments_queued_descriptor = CoreDescriptor {
queue: Some(QueueDescriptor {
first: BlockNumberFor::<Test>::from(11u32),
last: BlockNumberFor::<Test>::from(11u32),
}),
current_work: None,
};
let assignments_active_descriptor = CoreDescriptor {
queue: None,
current_work: Some(WorkState {
assignments: vec![(CoreAssignment::Pool, test_assignment_state)],
end_hint: Some(BlockNumberFor::<Test>::from(15u32)),
pos: 0,
step: PartsOf57600::FULL,
}),
};
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let mut core_descriptor: CoreDescriptor<BlockNumberFor<Test>> = empty_descriptor.clone();
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Case 1: No new schedule in CoreSchedules for core
CoretimeAssigner::ensure_workload(10u32, core_idx, &mut core_descriptor);
assert_eq!(core_descriptor, empty_descriptor);
// Case 2: New schedule exists in CoreSchedules for core, but new
// schedule start is not yet reached.
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
vec![(CoreAssignment::Pool, PartsOf57600::FULL)],
Some(BlockNumberFor::<Test>::from(15u32)),
));
// Propagate changes from storage to Core_Descriptor handle. Normally
// pop_assignment_for_core would handle this.
core_descriptor = CoreDescriptors::<Test>::get(core_idx);
CoretimeAssigner::ensure_workload(10u32, core_idx, &mut core_descriptor);
assert_eq!(core_descriptor, assignments_queued_descriptor);
// Case 3: Next schedule exists in CoreSchedules for core. Next starting
// block has been reached. Swaps new WorkState into CoreDescriptors from
// CoreSchedules.
CoretimeAssigner::ensure_workload(11u32, core_idx, &mut core_descriptor);
assert_eq!(core_descriptor, assignments_active_descriptor);
// Case 4: end_hint reached but new schedule start not yet reached. WorkState in
// CoreDescriptor is cleared
CoretimeAssigner::ensure_workload(15u32, core_idx, &mut core_descriptor);
assert_eq!(core_descriptor, empty_descriptor);
});
}
#[test]
fn pop_assignment_for_core_works() {
let para_id = ParaId::from(1);
let core_idx = CoreIndex(0);
let alice = 1u64;
let amt = 10_000_000u128;
let assignments_pool = vec![(CoreAssignment::Pool, PartsOf57600::FULL)];
let assignments_task = vec![(CoreAssignment::Task(para_id.into()), PartsOf57600::FULL)];
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
// Initialize the parathread, wait for it to be ready, then add an
// on demand order to later pop with our Coretime assigner.
schedule_blank_para(para_id, ParaKind::Parathread);
Balances::make_free_balance_be(&alice, amt);
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
assert_ok!(OnDemandAssigner::place_order_allow_death(
RuntimeOrigin::signed(alice),
amt,
para_id
));
// Case 1: Assignment idle
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
default_test_assignments(), // Default is Idle
None,
));
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
assert_eq!(CoretimeAssigner::pop_assignment_for_core(core_idx), None);
// Case 2: Assignment pool
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(21u32),
assignments_pool,
None,
));
run_to_block(21, |n| if n == 21 { Some(Default::default()) } else { None });
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Pool { para_id, core_index: 0.into() })
);
// Case 3: Assignment task
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(31u32),
assignments_task,
None,
));
run_to_block(31, |n| if n == 31 { Some(Default::default()) } else { None });
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(para_id))
);
});
}
#[test]
fn assignment_proportions_in_core_state_work() {
let core_idx = CoreIndex(0);
let task_1 = TaskId::from(1u32);
let task_2 = TaskId::from(2u32);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Task 1 gets 2/3 core usage, while task 2 gets 1/3
let test_assignments = vec![
(CoreAssignment::Task(task_1), PartsOf57600::FULL / 3 * 2),
(CoreAssignment::Task(task_2), PartsOf57600::FULL / 3),
];
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
test_assignments,
None,
));
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Case 1: Current assignment remaining >= step after pop
{
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.pos)),
Some(0u16)
);
// Consumed step should be 1/3 of core parts, leaving 1/3 remaining
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.assignments[0].1.remaining)),
Some(PartsOf57600::FULL / 3)
);
}
// Case 2: Current assignment remaning < step after pop
{
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
// Pos should have incremented, as assignment had remaining < step
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.pos)),
Some(1u16)
);
// Remaining should have started at 1/3 of core work parts. We then subtract
// step (1/3) and add back ratio (2/3), leaving us with 2/3 of core work parts.
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.assignments[0].1.remaining)),
Some(PartsOf57600::FULL / 3 * 2)
);
}
// Final check, task 2's turn to be served
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
});
}
#[test]
fn equal_assignments_served_equally() {
let core_idx = CoreIndex(0);
let task_1 = TaskId::from(1u32);
let task_2 = TaskId::from(2u32);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Tasks 1 and 2 get equal work parts
let test_assignments = vec![
(CoreAssignment::Task(task_1), PartsOf57600::FULL / 2),
(CoreAssignment::Task(task_2), PartsOf57600::FULL / 2),
];
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
test_assignments,
None,
));
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Test that popped assignments alternate between tasks 1 and 2
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
});
}
#[test]
// Checks that core is shared fairly, even in case of `ratio` not being
// divisible by `step` (over multiple rounds).
fn assignment_proportions_indivisible_by_step_work() {
let core_idx = CoreIndex(0);
let task_1 = TaskId::from(1u32);
let ratio_1 = PartsOf57600::FULL / 5 * 3;
let ratio_2 = PartsOf57600::FULL / 5 * 2;
let task_2 = TaskId::from(2u32);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Task 1 gets 3/5 core usage, while task 2 gets 2/5. That way
// step is set to 2/5 and task 1 is indivisible by step.
let test_assignments =
vec![(CoreAssignment::Task(task_1), ratio_1), (CoreAssignment::Task(task_2), ratio_2)];
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
test_assignments,
None,
));
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Pop 5 assignments. Should Result in the the following work ordering:
// 1, 2, 1, 1, 2. The remaining parts for each assignment should be same
// at the end as in the beginning.
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
// Remaining should equal ratio for both assignments.
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.assignments[0].1.remaining)),
Some(ratio_1)
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.assignments[1].1.remaining)),
Some(ratio_2)
);
});
}
#[cfg(test)]
impl std::ops::Div<u16> for PartsOf57600 {
type Output = Self;
fn div(self, rhs: u16) -> Self::Output {
if rhs == 0 {
panic!("Cannot divide by zero!");
}
Self(self.0 / rhs)
}
}
#[cfg(test)]
impl std::ops::Mul<u16> for PartsOf57600 {
type Output = Self;
fn mul(self, rhs: u16) -> Self {
Self(self.0 * rhs)
}
}
#[test]
fn parts_of_57600_ops() {
assert!(PartsOf57600::new_saturating(57601).is_full());
assert!(PartsOf57600::FULL.saturating_add(PartsOf57600(1)).is_full());
assert_eq!(PartsOf57600::ZERO.saturating_sub(PartsOf57600(1)), PartsOf57600::ZERO);
assert_eq!(PartsOf57600::FULL.checked_add(PartsOf57600(0)), Some(PartsOf57600::FULL));
assert_eq!(PartsOf57600::FULL.checked_add(PartsOf57600(1)), None);
}
@@ -43,7 +43,7 @@ where
{
ParasShared::<T>::set_session_index(SESSION_INDEX);
let mut config = HostConfiguration::default();
config.on_demand_cores = 1;
config.coretime_cores = 1;
ConfigurationPallet::<T>::force_set_active_config(config);
let mut parachains = ParachainsCache::new();
ParasPallet::<T>::initialize_para_now(
@@ -70,11 +70,10 @@ mod benchmarks {
let para_id = ParaId::from(111u32);
init_parathread::<T>(para_id);
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
let assignment = Assignment::new(para_id);
let order = EnqueuedOrder::new(para_id);
for _ in 0..s {
Pallet::<T>::add_on_demand_assignment(assignment.clone(), QueuePushDirection::Back)
.unwrap();
Pallet::<T>::add_on_demand_order(order.clone(), QueuePushDirection::Back).unwrap();
}
#[extrinsic_call]
@@ -88,11 +87,10 @@ mod benchmarks {
let para_id = ParaId::from(111u32);
init_parathread::<T>(para_id);
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
let assignment = Assignment::new(para_id);
let order = EnqueuedOrder::new(para_id);
for _ in 0..s {
Pallet::<T>::add_on_demand_assignment(assignment.clone(), QueuePushDirection::Back)
.unwrap();
Pallet::<T>::add_on_demand_order(order.clone(), QueuePushDirection::Back).unwrap();
}
#[extrinsic_call]
@@ -27,7 +27,7 @@ use crate::{
use primitives::{Balance, HeadData, ValidationCode};
pub fn default_genesis_config() -> MockGenesisConfig {
fn default_genesis_config() -> MockGenesisConfig {
MockGenesisConfig {
configuration: crate::configuration::GenesisConfig {
config: crate::configuration::HostConfiguration { ..Default::default() },
@@ -63,7 +63,7 @@ impl GenesisConfigBuilder {
pub(super) fn build(self) -> MockGenesisConfig {
let mut genesis = default_genesis_config();
let config = &mut genesis.configuration.config;
config.on_demand_cores = self.on_demand_cores;
config.coretime_cores = self.on_demand_cores;
config.on_demand_base_fee = self.on_demand_base_fee;
config.on_demand_fee_variability = self.on_demand_fee_variability;
config.on_demand_queue_max_size = self.on_demand_max_queue_size;
@@ -32,10 +32,7 @@ mod mock_helpers;
#[cfg(test)]
mod tests;
use crate::{
configuration, paras,
scheduler::common::{Assignment, AssignmentProvider, AssignmentProviderConfig},
};
use crate::{configuration, paras, scheduler::common::Assignment};
use frame_support::{
pallet_prelude::*,
@@ -79,7 +76,7 @@ impl WeightInfo for TestWeightInfo {
/// 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, Debug))]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug))]
pub struct CoreAffinityCount {
core_idx: CoreIndex,
count: u32,
@@ -107,6 +104,18 @@ pub enum SpotTrafficCalculationErr {
Division,
}
/// Internal representation of an order after it has been enqueued already.
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone)]
pub(super) struct EnqueuedOrder {
pub para_id: ParaId,
}
impl EnqueuedOrder {
pub fn new(para_id: ParaId) -> Self {
Self { para_id }
}
}
#[frame_support::pallet]
pub mod pallet {
@@ -140,7 +149,7 @@ pub mod pallet {
/// Creates an empty on demand queue if one isn't present in storage already.
#[pallet::type_value]
pub fn OnDemandQueueOnEmpty<T: Config>() -> VecDeque<Assignment> {
pub(super) fn OnDemandQueueOnEmpty<T: Config>() -> VecDeque<EnqueuedOrder> {
VecDeque::new()
}
@@ -153,8 +162,8 @@ pub mod pallet {
/// The order storage entry. Uses a VecDeque to be able to push to the front of the
/// queue from the scheduler on session boundaries.
#[pallet::storage]
pub type OnDemandQueue<T: Config> =
StorageValue<_, VecDeque<Assignment>, ValueQuery, OnDemandQueueOnEmpty<T>>;
pub(super) type OnDemandQueue<T: Config> =
StorageValue<_, VecDeque<EnqueuedOrder>, ValueQuery, OnDemandQueueOnEmpty<T>>;
/// 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
@@ -182,9 +191,6 @@ pub mod pallet {
/// The current spot price is higher than the max amount specified in the `place_order`
/// call, making it invalid.
SpotPriceHigherThanMaxAmount,
/// There are no on demand cores available. `place_order` will not add anything to the
/// queue.
NoOnDemandCores,
}
#[pallet::hooks]
@@ -248,7 +254,6 @@ pub mod pallet {
/// - `InvalidParaId`
/// - `QueueFull`
/// - `SpotPriceHigherThanMaxAmount`
/// - `NoOnDemandCores`
///
/// Events:
/// - `SpotOrderPlaced`
@@ -276,7 +281,6 @@ pub mod pallet {
/// - `InvalidParaId`
/// - `QueueFull`
/// - `SpotPriceHigherThanMaxAmount`
/// - `NoOnDemandCores`
///
/// Events:
/// - `SpotOrderPlaced`
@@ -311,7 +315,6 @@ where
/// - `InvalidParaId`
/// - `QueueFull`
/// - `SpotPriceHigherThanMaxAmount`
/// - `NoOnDemandCores`
///
/// Events:
/// - `SpotOrderPlaced`
@@ -323,9 +326,6 @@ where
) -> DispatchResult {
let config = <configuration::Pallet<T>>::config();
// Are there any schedulable cores in this session
ensure!(config.on_demand_cores > 0, Error::<T>::NoOnDemandCores);
// Traffic always falls back to 1.0
let traffic = SpotTraffic::<T>::get();
@@ -344,17 +344,15 @@ where
existence_requirement,
)?;
let assignment = Assignment::new(para_id);
let order = EnqueuedOrder::new(para_id);
let res = Pallet::<T>::add_on_demand_assignment(assignment, QueuePushDirection::Back);
let res = Pallet::<T>::add_on_demand_order(order, QueuePushDirection::Back);
match res {
Ok(_) => {
Pallet::<T>::deposit_event(Event::<T>::OnDemandOrderPlaced { para_id, spot_price });
return Ok(())
},
Err(err) => return Err(err),
if res.is_ok() {
Pallet::<T>::deposit_event(Event::<T>::OnDemandOrderPlaced { para_id, spot_price });
}
res
}
/// The spot price multiplier. This is based on the transaction fee calculations defined in:
@@ -428,10 +426,10 @@ where
}
}
/// Adds an assignment to the on demand queue.
/// Adds an order to the on demand queue.
///
/// Paramenters:
/// - `assignment`: The on demand assignment to add to the queue.
/// - `order`: The `EnqueuedOrder` to add to the queue.
/// - `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.
@@ -441,12 +439,12 @@ where
/// Errors:
/// - `InvalidParaId`
/// - `QueueFull`
pub fn add_on_demand_assignment(
assignment: Assignment,
fn add_on_demand_order(
order: EnqueuedOrder,
location: QueuePushDirection,
) -> Result<(), DispatchError> {
// Only parathreads are valid paraids for on the go parachains.
ensure!(<paras::Pallet<T>>::is_parathread(assignment.para_id), Error::<T>::InvalidParaId);
ensure!(<paras::Pallet<T>>::is_parathread(order.para_id), Error::<T>::InvalidParaId);
let config = <configuration::Pallet<T>>::config();
@@ -454,8 +452,8 @@ where
// Abort transaction if queue is too large
ensure!(Self::queue_size() < config.on_demand_queue_max_size, Error::<T>::QueueFull);
match location {
QueuePushDirection::Back => queue.push_back(assignment),
QueuePushDirection::Front => queue.push_front(assignment),
QueuePushDirection::Back => queue.push_back(order),
QueuePushDirection::Front => queue.push_front(order),
};
Ok(())
})
@@ -480,7 +478,8 @@ where
}
/// Getter for the order queue.
pub fn get_queue() -> VecDeque<Assignment> {
#[cfg(test)]
fn get_queue() -> VecDeque<EnqueuedOrder> {
OnDemandQueue::<T>::get()
}
@@ -528,12 +527,7 @@ where
}
}
impl<T: Config> AssignmentProvider<BlockNumberFor<T>> for Pallet<T> {
fn session_core_count() -> u32 {
let config = <configuration::Pallet<T>>::config();
config.on_demand_cores
}
impl<T: Config> Pallet<T> {
/// Take the next queued entry that is available for a given core index.
/// Invalidates and removes orders with a `para_id` that is not `ParaLifecycle::Parathread`
/// but only in [0..P] range slice of the order queue, where P is the element that is
@@ -541,20 +535,8 @@ impl<T: Config> AssignmentProvider<BlockNumberFor<T>> for Pallet<T> {
///
/// Parameters:
/// - `core_idx`: The core index
/// - `previous_paraid`: Which paraid was previously processed on the requested core. Is None if
/// nothing was processed on the core.
fn pop_assignment_for_core(
core_idx: CoreIndex,
previous_para: Option<ParaId>,
) -> Option<Assignment> {
// Only decrease the affinity of the previous para if it exists.
// A nonexistant `ParaId` indicates that the scheduler has not processed any
// `ParaId` this session.
if let Some(previous_para_id) = previous_para {
Pallet::<T>::decrease_affinity(previous_para_id, core_idx)
}
let mut queue: VecDeque<Assignment> = OnDemandQueue::<T>::get();
pub fn pop_assignment_for_core(core_idx: CoreIndex) -> Option<Assignment> {
let mut queue: VecDeque<EnqueuedOrder> = OnDemandQueue::<T>::get();
let mut invalidated_para_id_indexes: Vec<usize> = vec![];
@@ -591,28 +573,28 @@ impl<T: Config> AssignmentProvider<BlockNumberFor<T>> for Pallet<T> {
// Write changes to storage.
OnDemandQueue::<T>::set(queue);
popped
popped.map(|p| Assignment::Pool { para_id: p.para_id, core_index: core_idx })
}
/// Push an assignment back to the queue.
/// Typically used on session boundaries.
/// Report that the `para_id` & `core_index` combination was processed.
pub fn report_processed(para_id: ParaId, core_index: CoreIndex) {
Pallet::<T>::decrease_affinity(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.
/// Parameters:
/// - `core_idx`: The core index
/// - `assignment`: The on demand assignment.
fn push_assignment_for_core(core_idx: CoreIndex, assignment: Assignment) {
Pallet::<T>::decrease_affinity(assignment.para_id, core_idx);
pub fn push_back_assignment(para_id: ParaId, core_index: CoreIndex) {
Pallet::<T>::decrease_affinity(para_id, core_index);
// Skip the queue on push backs from scheduler
match Pallet::<T>::add_on_demand_assignment(assignment, QueuePushDirection::Front) {
match Pallet::<T>::add_on_demand_order(
EnqueuedOrder::new(para_id),
QueuePushDirection::Front,
) {
Ok(_) => {},
Err(_) => {},
}
}
fn get_provider_config(_core_idx: CoreIndex) -> AssignmentProviderConfig<BlockNumberFor<T>> {
let config = <configuration::Pallet<T>>::config();
AssignmentProviderConfig {
max_availability_timeouts: config.on_demand_retries,
ttl: config.on_demand_ttl,
}
}
}
@@ -24,7 +24,6 @@ use crate::{
System, Test,
},
paras::{ParaGenesisArgs, ParaKind},
scheduler::common::Assignment,
};
use frame_support::{assert_noop, assert_ok, error::BadOrigin};
use pallet_balances::Error as BalancesError;
@@ -75,7 +74,7 @@ fn run_to_block(
Scheduler::initializer_initialize(b + 1);
// In the real runtime this is expected to be called by the `InclusionInherent` pallet.
Scheduler::update_claimqueue(BTreeMap::new(), b + 1);
Scheduler::free_cores_and_fill_claimqueue(BTreeMap::new(), b + 1);
}
}
@@ -280,9 +279,9 @@ fn place_order_keep_alive_keeps_alive() {
}
#[test]
fn add_on_demand_assignment_works() {
fn add_on_demand_order_works() {
let para_a = ParaId::from(111);
let assignment = Assignment::new(para_a);
let order = EnqueuedOrder::new(para_a);
let mut genesis = GenesisConfigBuilder::default();
genesis.on_demand_max_queue_size = 1;
@@ -292,10 +291,7 @@ fn add_on_demand_assignment_works() {
// `para_a` is not onboarded as a parathread yet.
assert_noop!(
OnDemandAssigner::add_on_demand_assignment(
assignment.clone(),
QueuePushDirection::Back
),
OnDemandAssigner::add_on_demand_order(order.clone(), QueuePushDirection::Back),
Error::<Test>::InvalidParaId
);
@@ -304,14 +300,11 @@ fn add_on_demand_assignment_works() {
assert!(Paras::is_parathread(para_a));
// `para_a` is now onboarded as a valid parathread.
assert_ok!(OnDemandAssigner::add_on_demand_assignment(
assignment.clone(),
QueuePushDirection::Back
));
assert_ok!(OnDemandAssigner::add_on_demand_order(order.clone(), QueuePushDirection::Back));
// Max queue size is 1, queue should be full.
assert_noop!(
OnDemandAssigner::add_on_demand_assignment(assignment, QueuePushDirection::Back),
OnDemandAssigner::add_on_demand_order(order, QueuePushDirection::Back),
Error::<Test>::QueueFull
);
});
@@ -330,29 +323,131 @@ fn spotqueue_push_directions() {
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
let assignment_a = Assignment { para_id: para_a };
let assignment_b = Assignment { para_id: para_b };
let assignment_c = Assignment { para_id: para_c };
let order_a = EnqueuedOrder::new(para_a);
let order_b = EnqueuedOrder::new(para_b);
let order_c = EnqueuedOrder::new(para_c);
assert_ok!(OnDemandAssigner::add_on_demand_assignment(
assignment_a.clone(),
assert_ok!(OnDemandAssigner::add_on_demand_order(
order_a.clone(),
QueuePushDirection::Front
));
assert_ok!(OnDemandAssigner::add_on_demand_assignment(
assignment_b.clone(),
assert_ok!(OnDemandAssigner::add_on_demand_order(
order_b.clone(),
QueuePushDirection::Front
));
assert_ok!(OnDemandAssigner::add_on_demand_assignment(
assignment_c.clone(),
assert_ok!(OnDemandAssigner::add_on_demand_order(
order_c.clone(),
QueuePushDirection::Back
));
assert_eq!(OnDemandAssigner::queue_size(), 3);
assert_eq!(OnDemandAssigner::get_queue(), VecDeque::from(vec![order_b, order_a, order_c]))
});
}
#[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 });
let order_a = EnqueuedOrder::new(para_a);
let order_b = EnqueuedOrder::new(para_b);
let assignment_a = Assignment::Pool { para_id: para_a, core_index: CoreIndex(0) };
let assignment_b = Assignment::Pool { para_id: para_b, core_index: CoreIndex(1) };
// Pop should return none with empty queue
assert_eq!(OnDemandAssigner::pop_assignment_for_core(CoreIndex(0)), None);
// Add enough assignments to the order queue.
for _ in 0..2 {
OnDemandAssigner::add_on_demand_order(order_a.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
OnDemandAssigner::add_on_demand_order(order_b.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
}
// Queue should contain orders a, b, a, b
{
let queue: Vec<EnqueuedOrder> = OnDemandQueue::<Test>::get().into_iter().collect();
assert_eq!(
queue,
vec![order_a.clone(), order_b.clone(), order_a.clone(), order_b.clone()]
);
}
// Popped assignments should be for the correct paras and cores
assert_eq!(
OnDemandAssigner::get_queue(),
VecDeque::from(vec![assignment_b, assignment_a, assignment_c])
)
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0)),
Some(assignment_a.clone())
);
assert_eq!(
OnDemandAssigner::pop_assignment_for_core(CoreIndex(1)),
Some(assignment_b.clone())
);
assert_eq!(
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0)),
Some(assignment_a.clone())
);
// Queue should contain one left over order
{
let queue: Vec<EnqueuedOrder> = OnDemandQueue::<Test>::get().into_iter().collect();
assert_eq!(queue, vec![order_b.clone(),]);
}
});
}
#[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 });
let order_a = EnqueuedOrder::new(para_a);
let order_b = EnqueuedOrder::new(para_b);
// Add enough assignments to the order queue.
OnDemandAssigner::add_on_demand_order(order_a.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
OnDemandAssigner::add_on_demand_order(order_b.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
// Pop order a
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0));
// Para a should have affinity for core 0
assert_eq!(OnDemandAssigner::get_affinity_map(para_a).unwrap().count, 1);
assert_eq!(OnDemandAssigner::get_affinity_map(para_a).unwrap().core_idx, CoreIndex(0));
// Queue should still contain order b
{
let queue: Vec<EnqueuedOrder> = OnDemandQueue::<Test>::get().into_iter().collect();
assert_eq!(queue, vec![order_b.clone()]);
}
// Push back order a
OnDemandAssigner::push_back_assignment(para_a, CoreIndex(0));
// Para a should have no affinity
assert_eq!(OnDemandAssigner::get_affinity_map(para_a).is_none(), true);
// Queue should contain orders a, b. A in front of b.
{
let queue: Vec<EnqueuedOrder> = OnDemandQueue::<Test>::get().into_iter().collect();
assert_eq!(queue, vec![order_a.clone(), order_b.clone()]);
}
});
}
@@ -360,39 +455,38 @@ fn spotqueue_push_directions() {
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);
let order_a = EnqueuedOrder::new(para_a);
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
let assignment_a = Assignment { para_id: para_a };
// There should be no affinity before starting.
assert!(OnDemandAssigner::get_affinity_map(para_a).is_none());
// Add enough assignments to the order queue.
for _ in 0..10 {
OnDemandAssigner::add_on_demand_assignment(
assignment_a.clone(),
QueuePushDirection::Front,
)
.expect("Invalid paraid or queue full");
OnDemandAssigner::add_on_demand_order(order_a.clone(), QueuePushDirection::Front)
.expect("Invalid paraid or queue full");
}
// There should be no affinity before the scheduler pops.
assert!(OnDemandAssigner::get_affinity_map(para_a).is_none());
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), None);
OnDemandAssigner::pop_assignment_for_core(core_index);
// Affinity count is 1 after popping.
assert_eq!(OnDemandAssigner::get_affinity_map(para_a).unwrap().count, 1);
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), Some(para_a));
OnDemandAssigner::report_processed(para_a, 0.into());
OnDemandAssigner::pop_assignment_for_core(core_index);
// Affinity count is 1 after popping with a previous para.
assert_eq!(OnDemandAssigner::get_affinity_map(para_a).unwrap().count, 1);
assert_eq!(OnDemandAssigner::queue_size(), 8);
for _ in 0..3 {
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), None);
OnDemandAssigner::pop_assignment_for_core(core_index);
}
// Affinity count is 4 after popping 3 times without a previous para.
@@ -400,7 +494,8 @@ fn affinity_changes_work() {
assert_eq!(OnDemandAssigner::queue_size(), 5);
for _ in 0..5 {
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), Some(para_a));
OnDemandAssigner::report_processed(para_a, 0.into());
OnDemandAssigner::pop_assignment_for_core(core_index);
}
// Affinity count should still be 4 but queue should be empty.
@@ -409,12 +504,14 @@ fn affinity_changes_work() {
// Pop 4 times and get to exactly 0 (None) affinity.
for _ in 0..4 {
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), Some(para_a));
OnDemandAssigner::report_processed(para_a, 0.into());
OnDemandAssigner::pop_assignment_for_core(core_index);
}
assert!(OnDemandAssigner::get_affinity_map(para_a).is_none());
// Decreasing affinity beyond 0 should still be None.
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), Some(para_a));
OnDemandAssigner::report_processed(para_a, 0.into());
OnDemandAssigner::pop_assignment_for_core(core_index);
assert!(OnDemandAssigner::get_affinity_map(para_a).is_none());
});
}
@@ -430,28 +527,28 @@ fn affinity_prohibits_parallel_scheduling() {
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
let assignment_a = Assignment { para_id: para_a };
let assignment_b = Assignment { para_id: para_b };
let order_a = EnqueuedOrder::new(para_a);
let order_b = EnqueuedOrder::new(para_b);
// There should be no affinity before starting.
assert!(OnDemandAssigner::get_affinity_map(para_a).is_none());
assert!(OnDemandAssigner::get_affinity_map(para_b).is_none());
// Add 2 assignments for para_a for every para_b.
OnDemandAssigner::add_on_demand_assignment(assignment_a.clone(), QueuePushDirection::Back)
OnDemandAssigner::add_on_demand_order(order_a.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
OnDemandAssigner::add_on_demand_assignment(assignment_a.clone(), QueuePushDirection::Back)
OnDemandAssigner::add_on_demand_order(order_a.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
OnDemandAssigner::add_on_demand_assignment(assignment_b.clone(), QueuePushDirection::Back)
OnDemandAssigner::add_on_demand_order(order_b.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
assert_eq!(OnDemandAssigner::queue_size(), 3);
// Approximate having 1 core.
for _ in 0..3 {
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), None);
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0));
}
// Affinity on one core is meaningless.
@@ -463,24 +560,25 @@ fn affinity_prohibits_parallel_scheduling() {
);
// Clear affinity
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), Some(para_a));
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), Some(para_a));
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), Some(para_b));
OnDemandAssigner::report_processed(para_a, 0.into());
OnDemandAssigner::report_processed(para_a, 0.into());
OnDemandAssigner::report_processed(para_b, 0.into());
// Add 2 assignments for para_a for every para_b.
OnDemandAssigner::add_on_demand_assignment(assignment_a.clone(), QueuePushDirection::Back)
OnDemandAssigner::add_on_demand_order(order_a.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
OnDemandAssigner::add_on_demand_assignment(assignment_a.clone(), QueuePushDirection::Back)
OnDemandAssigner::add_on_demand_order(order_a.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
OnDemandAssigner::add_on_demand_assignment(assignment_b.clone(), QueuePushDirection::Back)
OnDemandAssigner::add_on_demand_order(order_b.clone(), QueuePushDirection::Back)
.expect("Invalid paraid or queue full");
// Approximate having 2 cores.
// Approximate having 3 cores. CoreIndex 2 should be unable to obtain an assignment
for _ in 0..3 {
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), None);
OnDemandAssigner::pop_assignment_for_core(CoreIndex(1), None);
OnDemandAssigner::pop_assignment_for_core(CoreIndex(0));
OnDemandAssigner::pop_assignment_for_core(CoreIndex(1));
assert_eq!(None, OnDemandAssigner::pop_assignment_for_core(CoreIndex(2)));
}
// Affinity should be the same as before, but on different cores.
@@ -488,38 +586,23 @@ fn affinity_prohibits_parallel_scheduling() {
assert_eq!(OnDemandAssigner::get_affinity_map(para_b).unwrap().count, 1);
assert_eq!(OnDemandAssigner::get_affinity_map(para_a).unwrap().core_idx, CoreIndex(0));
assert_eq!(OnDemandAssigner::get_affinity_map(para_b).unwrap().core_idx, CoreIndex(1));
});
}
#[test]
fn cannot_place_order_when_no_on_demand_cores() {
let mut genesis = GenesisConfigBuilder::default();
genesis.on_demand_cores = 0;
let para_id = ParaId::from(10);
let alice = 1u64;
let amt = 10_000_000u128;
// Clear affinity
OnDemandAssigner::report_processed(para_a, 0.into());
OnDemandAssigner::report_processed(para_a, 0.into());
OnDemandAssigner::report_processed(para_b, 1.into());
new_test_ext(genesis.build()).execute_with(|| {
schedule_blank_para(para_id, ParaKind::Parathread);
Balances::make_free_balance_be(&alice, amt);
assert!(!Paras::is_parathread(para_id));
run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None });
assert!(Paras::is_parathread(para_id));
assert_noop!(
OnDemandAssigner::place_order_allow_death(RuntimeOrigin::signed(alice), amt, para_id),
Error::<Test>::NoOnDemandCores
);
// There should be no affinity after clearing.
assert!(OnDemandAssigner::get_affinity_map(para_a).is_none());
assert!(OnDemandAssigner::get_affinity_map(para_b).is_none());
});
}
#[test]
fn on_demand_orders_cannot_be_popped_if_lifecycle_changes() {
let para_id = ParaId::from(10);
let assignment = Assignment { para_id };
let core_index = CoreIndex(0);
let order = EnqueuedOrder::new(para_id);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
// Register the para_id as a parathread
@@ -530,17 +613,14 @@ fn on_demand_orders_cannot_be_popped_if_lifecycle_changes() {
assert!(Paras::is_parathread(para_id));
// Add two assignments for a para_id with a valid lifecycle.
assert_ok!(OnDemandAssigner::add_on_demand_assignment(
assignment.clone(),
QueuePushDirection::Back
));
assert_ok!(OnDemandAssigner::add_on_demand_assignment(
assignment.clone(),
QueuePushDirection::Back
));
assert_ok!(OnDemandAssigner::add_on_demand_order(order.clone(), QueuePushDirection::Back));
assert_ok!(OnDemandAssigner::add_on_demand_order(order.clone(), QueuePushDirection::Back));
// First pop is fine
assert!(OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), None) == Some(assignment));
assert!(
OnDemandAssigner::pop_assignment_for_core(core_index) ==
Some(Assignment::Pool { para_id, core_index })
);
// Deregister para
assert_ok!(Paras::schedule_para_cleanup(para_id));
@@ -551,6 +631,7 @@ fn on_demand_orders_cannot_be_popped_if_lifecycle_changes() {
assert!(!Paras::is_parathread(para_id));
// Second pop should be None.
assert!(OnDemandAssigner::pop_assignment_for_core(CoreIndex(0), Some(para_id)) == None);
OnDemandAssigner::report_processed(para_id, core_index);
assert_eq!(OnDemandAssigner::pop_assignment_for_core(core_index), None);
});
}
@@ -17,13 +17,20 @@
//! The bulk (parachain slot auction) blockspace assignment provider.
//! This provider is tightly coupled with the configuration and paras modules.
#[cfg(test)]
mod mock_helpers;
#[cfg(test)]
mod tests;
use frame_system::pallet_prelude::BlockNumberFor;
use primitives::CoreIndex;
use crate::{
configuration, paras,
scheduler::common::{Assignment, AssignmentProvider, AssignmentProviderConfig},
};
use frame_system::pallet_prelude::BlockNumberFor;
pub use pallet::*;
use primitives::{CoreIndex, Id as ParaId};
#[frame_support::pallet]
pub mod pallet {
@@ -38,23 +45,18 @@ pub mod pallet {
}
impl<T: Config> AssignmentProvider<BlockNumberFor<T>> for Pallet<T> {
fn session_core_count() -> u32 {
paras::Parachains::<T>::decode_len().unwrap_or(0) as u32
}
fn pop_assignment_for_core(
core_idx: CoreIndex,
_concluded_para: Option<ParaId>,
) -> Option<Assignment> {
fn pop_assignment_for_core(core_idx: CoreIndex) -> Option<Assignment> {
<paras::Pallet<T>>::parachains()
.get(core_idx.0 as usize)
.copied()
.map(|para_id| Assignment::new(para_id))
.map(Assignment::Bulk)
}
fn report_processed(_: Assignment) {}
/// Bulk assignment has no need to push the assignment back on a session change,
/// this is a no-op in the case of a bulk assignment slot.
fn push_assignment_for_core(_: CoreIndex, _: Assignment) {}
fn push_back_assignment(_: Assignment) {}
fn get_provider_config(_core_idx: CoreIndex) -> AssignmentProviderConfig<BlockNumberFor<T>> {
AssignmentProviderConfig {
@@ -65,4 +67,13 @@ impl<T: Config> AssignmentProvider<BlockNumberFor<T>> for Pallet<T> {
ttl: 10u32.into(),
}
}
#[cfg(any(feature = "runtime-benchmarks", test))]
fn get_mock_assignment(_: CoreIndex, para_id: primitives::Id) -> Assignment {
Assignment::Bulk(para_id)
}
fn session_core_count() -> u32 {
paras::Parachains::<T>::decode_len().unwrap_or(0) as u32
}
}
@@ -0,0 +1,83 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Helper functions for tests
use crate::{
mock::MockGenesisConfig,
paras::{ParaGenesisArgs, ParaKind},
};
use primitives::{Balance, HeadData, ValidationCode};
use sp_runtime::Perbill;
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<primitives::Id>,
}
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.coretime_cores = self.on_demand_cores;
config.on_demand_base_fee = self.on_demand_base_fee;
config.on_demand_fee_variability = self.on_demand_fee_variability;
config.on_demand_queue_max_size = self.on_demand_max_queue_size;
config.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,112 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use crate::{
assigner_parachains::mock_helpers::GenesisConfigBuilder,
initializer::SessionChangeNotification,
mock::{
new_test_ext, ParachainsAssigner, Paras, ParasShared, RuntimeOrigin, Scheduler, System,
},
paras::{ParaGenesisArgs, ParaKind},
};
use frame_support::{assert_ok, pallet_prelude::*};
use primitives::{BlockNumber, Id as ParaId, SessionIndex, ValidationCode};
use sp_std::collections::btree_map::BTreeMap;
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);
// In the real runtime this is expected to be called by the `InclusionInherent` pallet.
Scheduler::free_cores_and_fill_claimqueue(BTreeMap::new(), b + 1);
}
}
// This and the scheduler test schedule_schedules_including_just_freed together
// ensure that next_up_on_available and next_up_on_time_out will always be
// filled with scheduler claims for lease holding parachains. (Removes the need
// for two other scheduler tests)
#[test]
fn parachains_assigner_pop_assignment_is_always_some() {
let core_index = CoreIndex(0);
let para_id = ParaId::from(10);
let expected_assignment = Assignment::Bulk(para_id);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
// Register the para_id as a lease holding parachain
schedule_blank_para(para_id, ParaKind::Parachain);
assert!(!Paras::is_parachain(para_id));
run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None });
assert!(Paras::is_parachain(para_id));
for _ in 0..20 {
assert!(
ParachainsAssigner::pop_assignment_for_core(core_index) ==
Some(expected_assignment.clone())
);
}
run_to_block(20, |n| if n == 20 { Some(Default::default()) } else { None });
for _ in 0..20 {
assert!(
ParachainsAssigner::pop_assignment_for_core(core_index) ==
Some(expected_assignment.clone())
);
}
});
}
+42 -8
View File
@@ -20,7 +20,7 @@ use crate::{
paras_inherent,
scheduler::{
self,
common::{Assignment, AssignmentProviderConfig},
common::{AssignmentProvider, AssignmentProviderConfig},
CoreOccupied, ParasEntry,
},
session_info, shared,
@@ -96,6 +96,8 @@ pub(crate) struct BenchBuilder<T: paras_inherent::Config> {
/// Make every candidate include a code upgrade by setting this to `Some` where the interior
/// value is the byte length of the new code.
code_upgrade: Option<u32>,
/// Specifies whether the claimqueue should be filled.
fill_claimqueue: bool,
_phantom: sp_std::marker::PhantomData<T>,
}
@@ -122,6 +124,7 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
dispute_sessions: Default::default(),
backed_and_concluding_cores: Default::default(),
code_upgrade: None,
fill_claimqueue: true,
_phantom: sp_std::marker::PhantomData::<T>,
}
}
@@ -225,6 +228,13 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
self.max_validators() / self.max_validators_per_core()
}
/// Set whether the claim queue should be filled.
#[cfg(not(feature = "runtime-benchmarks"))]
pub(crate) fn set_fill_claimqueue(mut self, f: bool) -> Self {
self.fill_claimqueue = f;
self
}
/// Get the minimum number of validity votes in order for a backed candidate to be included.
#[cfg(feature = "runtime-benchmarks")]
pub(crate) fn fallback_min_validity_votes() -> u32 {
@@ -643,7 +653,7 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
})
.collect();
DisputeStatementSet { candidate_hash: candidate_hash, session, statements }
DisputeStatementSet { candidate_hash, session, statements }
})
.collect()
}
@@ -663,14 +673,18 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
inclusion::PendingAvailability::<T>::remove_all(None);
// We don't allow a core to have both disputes and be marked fully available at this block.
let cores = self.max_cores();
let max_cores = self.max_cores();
let used_cores =
(self.dispute_sessions.len() + self.backed_and_concluding_cores.len()) as u32;
assert!(used_cores <= cores);
assert!(used_cores <= max_cores);
let fill_claimqueue = self.fill_claimqueue;
// NOTE: there is an n+2 session delay for these actions to take effect.
// We are currently in Session 0, so these changes will take effect in Session 2.
Self::setup_para_ids(used_cores);
configuration::ActiveConfig::<T>::mutate(|c| {
c.coretime_cores = used_cores;
});
let validator_ids = Self::generate_validator_pairs(self.max_validators());
let target_session = SessionIndex::from(self.target_session);
@@ -702,13 +716,33 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
.map(|i| {
let AssignmentProviderConfig { ttl, .. } =
scheduler::Pallet::<T>::assignment_provider_config(CoreIndex(i));
CoreOccupied::Paras(ParasEntry::new(
Assignment::new(ParaId::from(i as u32)),
now + ttl,
))
// Load an assignment into provider so that one is present to pop
let assignment = <T as scheduler::Config>::AssignmentProvider::get_mock_assignment(
CoreIndex(i),
ParaId::from(i),
);
CoreOccupied::Paras(ParasEntry::new(assignment, now + ttl))
})
.collect();
scheduler::AvailabilityCores::<T>::set(cores);
if fill_claimqueue {
// Add items to claim queue as well:
let cores = (0..used_cores)
.into_iter()
.map(|i| {
let AssignmentProviderConfig { ttl, .. } =
scheduler::Pallet::<T>::assignment_provider_config(CoreIndex(i));
// Load an assignment into provider so that one is present to pop
let assignment =
<T as scheduler::Config>::AssignmentProvider::get_mock_assignment(
CoreIndex(i),
ParaId::from(i),
);
(CoreIndex(i), [ParasEntry::new(assignment, now + ttl)].into())
})
.collect();
scheduler::ClaimQueue::<T>::set(cores);
}
Bench::<T> {
data: ParachainsInherentData {
@@ -172,8 +172,8 @@ pub struct HostConfiguration<BlockNumber> {
/// How long to keep code on-chain, in blocks. This should be sufficiently long that disputes
/// have concluded.
pub code_retention_period: BlockNumber,
/// The amount of execution cores to dedicate to on demand execution.
pub on_demand_cores: u32,
/// How many cores are managed by the coretime chain.
pub coretime_cores: u32,
/// The number of retries that a on demand author has to submit their block.
pub on_demand_retries: u32,
/// The maximum queue size of the pay as you go module.
@@ -284,7 +284,7 @@ impl<BlockNumber: Default + From<u32>> Default for HostConfiguration<BlockNumber
max_code_size: Default::default(),
max_pov_size: Default::default(),
max_head_data_size: Default::default(),
on_demand_cores: Default::default(),
coretime_cores: Default::default(),
on_demand_retries: Default::default(),
scheduling_lookahead: 1,
max_validators_per_core: Default::default(),
@@ -665,17 +665,18 @@ pub mod pallet {
})
}
/// Set the number of on demand execution cores.
/// Set the number of coretime execution cores.
///
/// Note that this configuration is managed by the coretime chain. Only manually change
/// this, if you really know what you are doing!
#[pallet::call_index(6)]
#[pallet::weight((
T::WeightInfo::set_config_with_u32(),
DispatchClass::Operational,
))]
pub fn set_on_demand_cores(origin: OriginFor<T>, new: u32) -> DispatchResult {
pub fn set_coretime_cores(origin: OriginFor<T>, new: u32) -> DispatchResult {
ensure_root(origin)?;
Self::schedule_config_update(|config| {
config.on_demand_cores = new;
})
Self::set_coretime_cores_unchecked(new)
}
/// Set the number of retries for a particular on demand.
@@ -1245,6 +1246,17 @@ pub mod pallet {
}
}
impl<T: Config> Pallet<T> {
/// Set coretime cores.
///
/// To be used if authorization is checked otherwise.
pub fn set_coretime_cores_unchecked(new: u32) -> DispatchResult {
Self::schedule_config_update(|config| {
config.coretime_cores = new;
})
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn integrity_test() {
@@ -126,7 +126,7 @@ hrmp_max_parachain_inbound_channels : pre.hrmp_max_parachain_inbound_channe
hrmp_max_parachain_outbound_channels : pre.hrmp_max_parachain_outbound_channels,
hrmp_channel_max_message_size : pre.hrmp_channel_max_message_size,
code_retention_period : pre.code_retention_period,
on_demand_cores : pre.on_demand_cores,
coretime_cores : pre.on_demand_cores,
on_demand_retries : pre.on_demand_retries,
group_rotation_frequency : pre.group_rotation_frequency,
paras_availability_period : pre.paras_availability_period,
@@ -222,7 +222,7 @@ mod tests {
assert_eq!(v11.n_delay_tranches, 25);
assert_eq!(v11.minimum_validation_upgrade_delay, 5);
assert_eq!(v11.group_rotation_frequency, 20);
assert_eq!(v11.on_demand_cores, 0);
assert_eq!(v11.coretime_cores, 0);
assert_eq!(v11.on_demand_base_fee, 10_000_000);
assert_eq!(v11.minimum_backing_votes, LEGACY_MIN_BACKING_VOTES);
assert_eq!(v11.approval_voting_params.max_approval_coalesce_count, 1);
@@ -286,7 +286,7 @@ mod tests {
assert_eq!(v10.hrmp_max_parachain_inbound_channels , v11.hrmp_max_parachain_inbound_channels);
assert_eq!(v10.hrmp_channel_max_message_size , v11.hrmp_channel_max_message_size);
assert_eq!(v10.code_retention_period , v11.code_retention_period);
assert_eq!(v10.on_demand_cores , v11.on_demand_cores);
assert_eq!(v10.coretime_cores , v11.coretime_cores);
assert_eq!(v10.on_demand_retries , v11.on_demand_retries);
assert_eq!(v10.group_rotation_frequency , v11.group_rotation_frequency);
assert_eq!(v10.paras_availability_period , v11.paras_availability_period);
@@ -283,7 +283,7 @@ fn setting_pending_config_members() {
max_code_size: 100_000,
max_pov_size: 1024,
max_head_data_size: 1_000,
on_demand_cores: 2,
coretime_cores: 2,
on_demand_retries: 5,
group_rotation_frequency: 20,
paras_availability_period: 10,
@@ -342,7 +342,7 @@ fn setting_pending_config_members() {
Configuration::set_max_pov_size(RuntimeOrigin::root(), new_config.max_pov_size).unwrap();
Configuration::set_max_head_data_size(RuntimeOrigin::root(), new_config.max_head_data_size)
.unwrap();
Configuration::set_on_demand_cores(RuntimeOrigin::root(), new_config.on_demand_cores)
Configuration::set_coretime_cores(RuntimeOrigin::root(), new_config.coretime_cores)
.unwrap();
Configuration::set_on_demand_retries(RuntimeOrigin::root(), new_config.on_demand_retries)
.unwrap();
@@ -0,0 +1,73 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! On demand assigner pallet benchmarking.
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_benchmarking::v2::*;
use frame_support::traits::OriginTrait;
use pallet_broker::CoreIndex as BrokerCoreIndex;
#[benchmarks]
mod benchmarks {
use super::*;
use assigner_coretime::PartsOf57600;
#[benchmark]
fn request_core_count() {
// Setup
let root_origin = <T as frame_system::Config>::RuntimeOrigin::root();
#[extrinsic_call]
_(
root_origin as <T as frame_system::Config>::RuntimeOrigin,
// random core count
100,
)
}
#[benchmark]
fn assign_core(s: Linear<1, 100>) {
// Setup
let root_origin = <T as frame_system::Config>::RuntimeOrigin::root();
// Use parameterized assignment count
let mut assignments: Vec<(CoreAssignment, PartsOf57600)> = vec![0u16; s as usize - 1]
.into_iter()
.enumerate()
.map(|(index, parts)| {
(CoreAssignment::Task(index as u32), PartsOf57600::new_saturating(parts))
})
.collect();
// Parts must add up to exactly 57600. Here we add all the parts in one assignment, as
// it won't effect the weight and splitting up the parts into even groupings may not
// work for every value `s`.
assignments.push((CoreAssignment::Task(s as u32), PartsOf57600::FULL));
let core_index: BrokerCoreIndex = 0;
#[extrinsic_call]
_(
root_origin as <T as frame_system::Config>::RuntimeOrigin,
core_index,
BlockNumberFor::<T>::from(5u32),
assignments,
Some(BlockNumberFor::<T>::from(20u32)),
)
}
}
@@ -0,0 +1,285 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Migrations for the Coretime pallet.
pub use v_coretime::{GetLegacyLease, MigrateToCoretime};
mod v_coretime {
#[cfg(feature = "try-runtime")]
use crate::scheduler::common::AssignmentProvider;
use crate::{
assigner_coretime, configuration,
coretime::{mk_coretime_call, Config, PartsOf57600, WeightInfo},
paras,
};
#[cfg(feature = "try-runtime")]
use frame_support::ensure;
use frame_support::{
traits::{OnRuntimeUpgrade, PalletInfoAccess, StorageVersion},
weights::Weight,
};
use frame_system::pallet_prelude::BlockNumberFor;
use pallet_broker::{CoreAssignment, CoreMask, ScheduleItem};
#[cfg(feature = "try-runtime")]
use parity_scale_codec::Decode;
#[cfg(feature = "try-runtime")]
use parity_scale_codec::Encode;
use polkadot_parachain_primitives::primitives::IsSystem;
use primitives::{CoreIndex, Id as ParaId};
use sp_arithmetic::traits::SaturatedConversion;
use sp_core::Get;
use sp_runtime::BoundedVec;
#[cfg(feature = "try-runtime")]
use sp_std::vec::Vec;
use sp_std::{iter, prelude::*, result};
use xcm::v3::{
send_xcm, Instruction, Junction, Junctions, MultiLocation, SendError, WeightLimit, Xcm,
};
/// Return information about a legacy lease of a parachain.
pub trait GetLegacyLease<N> {
/// If parachain is a lease holding parachain, return the block at which the lease expires.
fn get_parachain_lease_in_blocks(para: ParaId) -> Option<N>;
}
/// Migrate a chain to use coretime.
///
/// This assumes that the `Coretime` and the `AssignerCoretime` pallets are added at the same
/// time to a runtime.
pub struct MigrateToCoretime<T, SendXcm, LegacyLease>(
sp_std::marker::PhantomData<(T, SendXcm, LegacyLease)>,
);
impl<T: Config, SendXcm: xcm::v3::SendXcm, LegacyLease: GetLegacyLease<BlockNumberFor<T>>>
MigrateToCoretime<T, SendXcm, LegacyLease>
{
fn already_migrated() -> bool {
// We are using the assigner coretime because the coretime pallet doesn't has any
// storage data. But both pallets are introduced at the same time, so this is fine.
let name_hash = assigner_coretime::Pallet::<T>::name_hash();
let mut next_key = name_hash.to_vec();
let storage_version_key = StorageVersion::storage_key::<assigner_coretime::Pallet<T>>();
loop {
match sp_io::storage::next_key(&next_key) {
// StorageVersion is initialized before, so we need to ingore it.
Some(key) if &key == &storage_version_key => {
next_key = key;
},
// If there is any other key with the prefix of the pallet,
// we already have executed the migration.
Some(key) if key.starts_with(&name_hash) => {
log::info!("`MigrateToCoretime` already executed!");
return true
},
// Any other key/no key means that we did not yet have migrated.
None | Some(_) => return false,
}
}
}
}
impl<
T: Config + crate::dmp::Config,
SendXcm: xcm::v3::SendXcm,
LegacyLease: GetLegacyLease<BlockNumberFor<T>>,
> OnRuntimeUpgrade for MigrateToCoretime<T, SendXcm, LegacyLease>
{
fn on_runtime_upgrade() -> Weight {
if Self::already_migrated() {
return Weight::zero()
}
log::info!("Migrating existing parachains to coretime.");
migrate_to_coretime::<T, SendXcm, LegacyLease>()
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::DispatchError> {
if Self::already_migrated() {
return Ok(Vec::new())
}
let legacy_paras = paras::Parachains::<T>::get();
let config = <configuration::Pallet<T>>::config();
let total_core_count = config.coretime_cores + legacy_paras.len() as u32;
let dmp_queue_size =
crate::dmp::Pallet::<T>::dmq_contents(T::BrokerId::get().into()).len() as u32;
let total_core_count = total_core_count as u32;
Ok((total_core_count, dmp_queue_size).encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::DispatchError> {
if state.is_empty() {
return Ok(())
}
log::trace!("Running post_upgrade()");
let (prev_core_count, prev_dmp_queue_size) =
<(u32, u32)>::decode(&mut &state[..]).unwrap();
let dmp_queue_size =
crate::dmp::Pallet::<T>::dmq_contents(T::BrokerId::get().into()).len() as u32;
let new_core_count = assigner_coretime::Pallet::<T>::session_core_count();
ensure!(new_core_count == prev_core_count, "Total number of cores need to not change.");
ensure!(
dmp_queue_size == prev_dmp_queue_size + 1,
"There should have been enqueued one DMP message."
);
Ok(())
}
}
// Migrate to Coretime.
//
// NOTE: Also migrates coretime_cores config value in configuration::ActiveConfig.
fn migrate_to_coretime<
T: Config,
SendXcm: xcm::v3::SendXcm,
LegacyLease: GetLegacyLease<BlockNumberFor<T>>,
>() -> Weight {
let legacy_paras = paras::Pallet::<T>::parachains();
let legacy_count = legacy_paras.len() as u32;
let now = <frame_system::Pallet<T>>::block_number();
for (core, para_id) in legacy_paras.into_iter().enumerate() {
let r = assigner_coretime::Pallet::<T>::assign_core(
CoreIndex(core as u32),
now,
vec![(CoreAssignment::Task(para_id.into()), PartsOf57600::FULL)],
None,
);
if let Err(err) = r {
log::error!(
"Creating assignment for existing para failed: {:?}, error: {:?}",
para_id,
err
);
}
}
let config = <configuration::Pallet<T>>::config();
// coretime_cores was on_demand_cores until now:
for on_demand in 0..config.coretime_cores {
let core = CoreIndex(legacy_count.saturating_add(on_demand as _));
let r = assigner_coretime::Pallet::<T>::assign_core(
core,
now,
vec![(CoreAssignment::Pool, PartsOf57600::FULL)],
None,
);
if let Err(err) = r {
log::error!("Creating assignment for existing on-demand core, failed: {:?}", err);
}
}
let total_cores = config.coretime_cores + legacy_count;
configuration::ActiveConfig::<T>::mutate(|c| {
c.coretime_cores = total_cores;
});
if let Err(err) = migrate_send_assignments_to_coretime_chain::<T, SendXcm, LegacyLease>() {
log::error!("Sending legacy chain data to coretime chain failed: {:?}", err);
}
let single_weight = <T as Config>::WeightInfo::assign_core(1);
single_weight
.saturating_mul(u64::from(legacy_count.saturating_add(config.coretime_cores)))
// Second read from sending assignments to the coretime chain.
.saturating_add(T::DbWeight::get().reads_writes(2, 1))
}
fn migrate_send_assignments_to_coretime_chain<
T: Config,
SendXcm: xcm::v3::SendXcm,
LegacyLease: GetLegacyLease<BlockNumberFor<T>>,
>() -> result::Result<(), SendError> {
let legacy_paras = paras::Pallet::<T>::parachains();
let legacy_paras_count = legacy_paras.len();
let (system_chains, lease_holding): (Vec<_>, Vec<_>) =
legacy_paras.into_iter().partition(IsSystem::is_system);
let reservations = system_chains.into_iter().map(|p| {
let schedule = BoundedVec::truncate_from(vec![ScheduleItem {
mask: CoreMask::complete(),
assignment: CoreAssignment::Task(p.into()),
}]);
mk_coretime_call(crate::coretime::CoretimeCalls::Reserve(schedule))
});
let leases = lease_holding.into_iter().filter_map(|p| {
log::trace!(target: "coretime-migration", "Preparing sending of lease holding para {:?}", p);
let Some(valid_until) = LegacyLease::get_parachain_lease_in_blocks(p) else {
log::error!("Lease holding chain with no lease information?!");
return None
};
let valid_until: u32 = match valid_until.try_into() {
Ok(val) => val,
Err(_) => {
log::error!("Converting block number to u32 failed!");
return None
},
};
// We assume the coretime chain set this parameter to the recommened value in RFC-1:
const TIME_SLICE_PERIOD: u32 = 80;
let round_up = if valid_until % TIME_SLICE_PERIOD > 0 { 1 } else { 0 };
let time_slice = valid_until / TIME_SLICE_PERIOD + TIME_SLICE_PERIOD * round_up;
log::trace!(target: "coretime-migration", "Sending of lease holding para {:?}, valid_until: {:?}, time_slice: {:?}", p, valid_until, time_slice);
Some(mk_coretime_call(crate::coretime::CoretimeCalls::SetLease(p.into(), time_slice)))
});
let core_count: u16 = configuration::Pallet::<T>::config().coretime_cores.saturated_into();
let set_core_count = iter::once(mk_coretime_call(
crate::coretime::CoretimeCalls::NotifyCoreCount(core_count),
));
let pool = (legacy_paras_count..core_count.into()).map(|_| {
let schedule = BoundedVec::truncate_from(vec![ScheduleItem {
mask: CoreMask::complete(),
assignment: CoreAssignment::Pool,
}]);
// Reserved cores will come before lease cores, so cores will change their assignments
// when coretime chain sends us their assign_core calls -> Good test.
mk_coretime_call(crate::coretime::CoretimeCalls::Reserve(schedule))
});
let message_content = iter::once(Instruction::UnpaidExecution {
weight_limit: WeightLimit::Unlimited,
check_origin: None,
})
.chain(reservations)
.chain(pool)
.chain(leases)
.chain(set_core_count)
.collect();
let message = Xcm(message_content);
send_xcm::<SendXcm>(
MultiLocation {
parents: 0,
interior: Junctions::X1(Junction::Parachain(T::BrokerId::get())),
},
message,
)?;
Ok(())
}
}
@@ -0,0 +1,251 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Extrinsics implementing the relay chain side of the Coretime interface.
//!
//! <https://github.com/polkadot-fellows/RFCs/blob/main/text/0005-coretime-interface.md>
use sp_std::{prelude::*, result};
use frame_support::{pallet_prelude::*, traits::Currency};
use frame_system::pallet_prelude::*;
pub use pallet::*;
use pallet_broker::{CoreAssignment, CoreIndex as BrokerCoreIndex};
use primitives::{CoreIndex, Id as ParaId};
use sp_arithmetic::traits::SaturatedConversion;
use xcm::v3::{
send_xcm, Instruction, Junction, Junctions, MultiLocation, OriginKind, SendXcm, Xcm,
};
use crate::{
assigner_coretime::{self, PartsOf57600},
initializer::{OnNewSession, SessionChangeNotification},
origin::{ensure_parachain, Origin},
};
mod benchmarking;
pub mod migration;
pub trait WeightInfo {
fn request_core_count() -> Weight;
//fn request_revenue_info_at() -> Weight;
//fn credit_account() -> Weight;
fn assign_core(s: u32) -> Weight;
}
/// A weight info that is only suitable for testing.
pub struct TestWeightInfo;
impl WeightInfo for TestWeightInfo {
fn request_core_count() -> Weight {
Weight::MAX
}
// TODO: Add real benchmarking functionality for each of these to
// benchmarking.rs, then uncomment here and in trait definition.
/*fn request_revenue_info_at() -> Weight {
Weight::MAX
}
fn credit_account() -> Weight {
Weight::MAX
}*/
fn assign_core(_s: u32) -> Weight {
Weight::MAX
}
}
/// Broker pallet index on the coretime chain. Used to
///
/// construct remote calls. The codec index must correspond to the index of `Broker` in the
/// `construct_runtime` of the coretime chain.
#[derive(Encode, Decode)]
enum BrokerRuntimePallets {
#[codec(index = 50)]
Broker(CoretimeCalls),
}
/// Call encoding for the calls needed from the Broker pallet.
#[derive(Encode, Decode)]
enum CoretimeCalls {
#[codec(index = 1)]
Reserve(pallet_broker::Schedule),
#[codec(index = 3)]
SetLease(pallet_broker::TaskId, pallet_broker::Timeslice),
#[codec(index = 19)]
NotifyCoreCount(u16),
}
#[frame_support::pallet]
pub mod pallet {
use crate::configuration;
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + assigner_coretime::Config {
type RuntimeOrigin: From<<Self as frame_system::Config>::RuntimeOrigin>
+ Into<result::Result<Origin, <Self as Config>::RuntimeOrigin>>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// The runtime's definition of a Currency.
type Currency: Currency<Self::AccountId>;
/// The ParaId of the broker system parachain.
#[pallet::constant]
type BrokerId: Get<u32>;
/// Something that provides the weight of this pallet.
type WeightInfo: WeightInfo;
type SendXcm: SendXcm;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// The broker chain has asked for revenue information for a specific block.
RevenueInfoRequested { when: BlockNumberFor<T> },
/// A core has received a new assignment from the broker chain.
CoreAssigned { core: CoreIndex },
}
#[pallet::error]
pub enum Error<T> {
/// The paraid making the call is not the coretime brokerage system parachain.
NotBroker,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(<T as Config>::WeightInfo::request_core_count())]
#[pallet::call_index(1)]
pub fn request_core_count(origin: OriginFor<T>, count: u16) -> DispatchResult {
// Ignore requests not coming from the broker parachain or root.
Self::ensure_root_or_para(origin, <T as Config>::BrokerId::get().into())?;
configuration::Pallet::<T>::set_coretime_cores_unchecked(u32::from(count))
}
//// TODO Impl me!
////#[pallet::weight(<T as Config>::WeightInfo::request_revenue_info_at())]
//#[pallet::call_index(2)]
//pub fn request_revenue_info_at(
// origin: OriginFor<T>,
// _when: BlockNumberFor<T>,
//) -> DispatchResult {
// // Ignore requests not coming from the broker parachain or root.
// Self::ensure_root_or_para(origin, <T as Config>::BrokerId::get().into())?;
// Ok(())
//}
//// TODO Impl me!
////#[pallet::weight(<T as Config>::WeightInfo::credit_account())]
//#[pallet::call_index(3)]
//pub fn credit_account(
// origin: OriginFor<T>,
// _who: T::AccountId,
// _amount: BalanceOf<T>,
//) -> DispatchResult {
// // Ignore requests not coming from the broker parachain or root.
// Self::ensure_root_or_para(origin, <T as Config>::BrokerId::get().into())?;
// Ok(())
//}
/// Receive instructions from the `ExternalBrokerOrigin`, detailing how a specific core is
/// to be used.
///
/// Parameters:
/// -`origin`: The `ExternalBrokerOrigin`, assumed to be the Broker system parachain.
/// -`core`: The core that should be scheduled.
/// -`begin`: The starting blockheight of the instruction.
/// -`assignment`: How the blockspace should be utilised.
/// -`end_hint`: An optional hint as to when this particular set of instructions will end.
// The broker pallet's `CoreIndex` definition is `u16` but on the relay chain it's `struct
// CoreIndex(u32)`
#[pallet::call_index(4)]
#[pallet::weight(<T as Config>::WeightInfo::assign_core(assignment.len() as u32))]
pub fn assign_core(
origin: OriginFor<T>,
core: BrokerCoreIndex,
begin: BlockNumberFor<T>,
assignment: Vec<(CoreAssignment, PartsOf57600)>,
end_hint: Option<BlockNumberFor<T>>,
) -> DispatchResult {
// Ignore requests not coming from the broker parachain or root.
Self::ensure_root_or_para(origin, T::BrokerId::get().into())?;
let core = u32::from(core).into();
<assigner_coretime::Pallet<T>>::assign_core(core, begin, assignment, end_hint)?;
Self::deposit_event(Event::<T>::CoreAssigned { core });
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
/// Ensure the origin is one of Root or the `para` itself.
fn ensure_root_or_para(
origin: <T as frame_system::Config>::RuntimeOrigin,
id: ParaId,
) -> DispatchResult {
if let Ok(caller_id) = ensure_parachain(<T as Config>::RuntimeOrigin::from(origin.clone()))
{
// Check if matching para id...
ensure!(caller_id == id, Error::<T>::NotBroker);
} else {
// Check if root...
ensure_root(origin.clone())?;
}
Ok(())
}
pub fn initializer_on_new_session(notification: &SessionChangeNotification<BlockNumberFor<T>>) {
let old_core_count = notification.prev_config.coretime_cores;
let new_core_count = notification.new_config.coretime_cores;
if new_core_count != old_core_count {
let core_count: u16 = new_core_count.saturated_into();
let message = Xcm(vec![mk_coretime_call(
crate::coretime::CoretimeCalls::NotifyCoreCount(core_count),
)]);
if let Err(err) = send_xcm::<T::SendXcm>(
MultiLocation {
parents: 0,
interior: Junctions::X1(Junction::Parachain(T::BrokerId::get())),
},
message,
) {
log::error!("Sending `NotifyCoreCount` to coretime chain failed: {:?}", err);
}
}
}
}
impl<T: Config> OnNewSession<BlockNumberFor<T>> for Pallet<T> {
fn on_new_session(notification: &SessionChangeNotification<BlockNumberFor<T>>) {
Self::initializer_on_new_session(notification);
}
}
fn mk_coretime_call(call: crate::coretime::CoretimeCalls) -> Instruction<()> {
Instruction::Transact {
origin_kind: OriginKind::Native,
require_weight_at_most: Weight::from_parts(1000000000, 200000),
call: BrokerRuntimePallets::Broker(call).encode().into(),
}
}
@@ -47,7 +47,7 @@ use test_helpers::{dummy_collator, dummy_collator_signature, dummy_validation_co
fn default_config() -> HostConfiguration<BlockNumber> {
let mut config = HostConfiguration::default();
config.on_demand_cores = 1;
config.coretime_cores = 1;
config.max_code_size = 0b100000;
config.max_head_data_size = 0b100000;
config.group_rotation_frequency = u32::MAX;
@@ -218,7 +218,7 @@ pub(crate) fn run_to_block(
}
pub(crate) fn expected_bits() -> usize {
Paras::parachains().len() + Configuration::config().on_demand_cores as usize
Paras::parachains().len() + Configuration::config().coretime_cores as usize
}
fn default_bitfield() -> AvailabilityBitfield {
@@ -60,6 +60,16 @@ pub struct SessionChangeNotification<BlockNumber> {
pub session_index: SessionIndex,
}
/// Inform something about a new session.
pub trait OnNewSession<N> {
/// A new session was started.
fn on_new_session(notification: &SessionChangeNotification<N>);
}
impl<N> OnNewSession<N> for () {
fn on_new_session(_: &SessionChangeNotification<N>) {}
}
/// Number of validators (not only parachain) in a session.
pub type ValidatorSetCount = u32;
@@ -120,6 +130,10 @@ pub mod pallet {
type Randomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
/// An origin which is allowed to force updates to parachains.
type ForceOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
/// Temporary hack to call `Coretime::on_new_session` on chains that support `Coretime` or
/// to disable it on the ones that don't support it. Can be removed and replaced by a simple
/// bound to `coretime::Config` once all chains support it.
type CoretimeOnNewSession: OnNewSession<BlockNumberFor<Self>>;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
@@ -271,6 +285,7 @@ impl<T: Config> Pallet<T> {
T::SlashingHandler::initializer_on_new_session(session_index);
dmp::Pallet::<T>::initializer_on_new_session(&notification, &outgoing_paras);
hrmp::Pallet::<T>::initializer_on_new_session(&notification, &outgoing_paras);
T::CoretimeOnNewSession::on_new_session(&notification);
}
/// Should be called when a new session occurs. Buffers the session notification to be applied
+2 -1
View File
@@ -23,10 +23,11 @@
#![cfg_attr(feature = "runtime-benchmarks", recursion_limit = "256")]
#![cfg_attr(not(feature = "std"), no_std)]
pub mod assigner;
pub mod assigner_coretime;
pub mod assigner_on_demand;
pub mod assigner_parachains;
pub mod configuration;
pub mod coretime;
pub mod disputes;
pub mod dmp;
pub mod hrmp;
+147 -12
View File
@@ -17,12 +17,17 @@
//! Mocks for all the traits.
use crate::{
assigner, assigner_on_demand, assigner_parachains, configuration, disputes, dmp, hrmp,
assigner_coretime, assigner_on_demand, assigner_parachains, configuration, coretime, disputes,
dmp, hrmp,
inclusion::{self, AggregateMessageOrigin, UmpQueueId},
initializer, origin, paras,
paras::ParaKind,
paras_inherent, scheduler, session_info, shared, ParaId,
paras_inherent, scheduler,
scheduler::common::{AssignmentProvider, AssignmentProviderConfig},
session_info, shared, ParaId,
};
use frame_support::pallet_prelude::*;
use primitives::CoreIndex;
use frame_support::{
assert_ok, derive_impl, parameter_types,
@@ -45,7 +50,9 @@ use sp_runtime::{
transaction_validity::TransactionPriority,
BuildStorage, FixedU128, Perbill, Permill,
};
use sp_std::collections::vec_deque::VecDeque;
use std::{cell::RefCell, collections::HashMap};
use xcm::v3::{MultiAssets, MultiLocation, SendError, SendResult, SendXcm, Xcm, XcmHash};
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlockU32<Test>;
@@ -62,9 +69,11 @@ frame_support::construct_runtime!(
ParaInclusion: inclusion,
ParaInherent: paras_inherent,
Scheduler: scheduler,
Assigner: assigner,
OnDemandAssigner: assigner_on_demand,
MockAssigner: mock_assigner,
ParachainsAssigner: assigner_parachains,
OnDemandAssigner: assigner_on_demand,
CoretimeAssigner: assigner_coretime,
Coretime: coretime,
Initializer: initializer,
Dmp: dmp,
Hrmp: hrmp,
@@ -178,6 +187,7 @@ impl crate::initializer::Config for Test {
type Randomness = TestRandomness<Self>;
type ForceOrigin = frame_system::EnsureRoot<u64>;
type WeightInfo = ();
type CoretimeOnNewSession = Coretime;
}
impl crate::configuration::Config for Test {
@@ -217,6 +227,7 @@ impl crate::paras::Config for Test {
type QueueFootprinter = ParaInclusion;
type NextSessionRotation = TestNextSessionRotation;
type OnNewHead = ();
type AssignCoretime = ();
}
impl crate::dmp::Config for Test {}
@@ -288,7 +299,7 @@ impl crate::disputes::SlashingHandler<BlockNumber> for Test {
}
impl crate::scheduler::Config for Test {
type AssignmentProvider = Assigner;
type AssignmentProvider = MockAssigner;
}
pub struct TestMessageQueueWeight;
@@ -342,17 +353,12 @@ impl pallet_message_queue::Config for Test {
type ServiceWeight = MessageQueueServiceWeight;
}
impl assigner::Config for Test {
type ParachainsAssignmentProvider = ParachainsAssigner;
type OnDemandAssignmentProvider = OnDemandAssigner;
}
impl assigner_parachains::Config for Test {}
parameter_types! {
pub const OnDemandTrafficDefaultValue: FixedU128 = FixedU128::from_u32(1);
}
impl assigner_parachains::Config for Test {}
impl assigner_on_demand::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
@@ -360,6 +366,37 @@ impl assigner_on_demand::Config for Test {
type WeightInfo = crate::assigner_on_demand::TestWeightInfo;
}
impl assigner_coretime::Config for Test {}
parameter_types! {
pub const BrokerId: u32 = 10u32;
}
impl coretime::Config for Test {
type RuntimeOrigin = RuntimeOrigin;
type RuntimeEvent = RuntimeEvent;
type Currency = pallet_balances::Pallet<Test>;
type BrokerId = BrokerId;
type WeightInfo = crate::coretime::TestWeightInfo;
type SendXcm = DummyXcmSender;
}
pub struct DummyXcmSender;
impl SendXcm for DummyXcmSender {
type Ticket = ();
fn validate(
_: &mut Option<MultiLocation>,
_: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
Ok(((), MultiAssets::new()))
}
/// Actually carry out the delivery operation for a previously validated message sending.
fn deliver(_ticket: Self::Ticket) -> Result<XcmHash, SendError> {
Ok([0u8; 32])
}
}
impl crate::inclusion::Config for Test {
type WeightInfo = ();
type RuntimeEvent = RuntimeEvent;
@@ -390,6 +427,104 @@ impl ValidatorSetWithIdentification<AccountId> for MockValidatorSet {
type IdentificationOf = FoolIdentificationOf;
}
/// A mock assigner which acts as the scheduler's `AssignmentProvider` for tests. The mock
/// assigner provides bare minimum functionality to test scheduler internals. Since they
/// have no direct effect on scheduler state, AssignmentProvider functions such as
/// `push_back_assignment` can be left empty.
pub mod mock_assigner {
use crate::scheduler::common::Assignment;
use super::*;
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + configuration::Config + paras::Config {}
#[pallet::storage]
pub(super) type MockAssignmentQueue<T: Config> =
StorageValue<_, VecDeque<Assignment>, ValueQuery>;
#[pallet::storage]
pub(super) type MockProviderConfig<T: Config> =
StorageValue<_, AssignmentProviderConfig<BlockNumber>, OptionQuery>;
#[pallet::storage]
pub(super) type MockCoreCount<T: Config> = StorageValue<_, u32, OptionQuery>;
}
impl<T: Config> Pallet<T> {
/// Adds a claim to the `MockAssignmentQueue` this claim can later be popped by the
/// scheduler when filling the claim queue for tests.
pub fn add_test_assignment(assignment: Assignment) {
MockAssignmentQueue::<T>::mutate(|queue| queue.push_back(assignment));
}
// This configuration needs to be customized to service `get_provider_config` in
// scheduler tests.
pub fn set_assignment_provider_config(config: AssignmentProviderConfig<BlockNumber>) {
MockProviderConfig::<T>::set(Some(config));
}
// Allows for customized core count in scheduler tests, rather than a core count
// derived from on-demand config + parachain count.
pub fn set_core_count(count: u32) {
MockCoreCount::<T>::set(Some(count));
}
}
impl<T: Config> AssignmentProvider<BlockNumber> for Pallet<T> {
// With regards to popping_assignments, the scheduler just needs to be tested under
// the following two conditions:
// 1. An assignment is provided
// 2. No assignment is provided
// A simple assignment queue populated to fit each test fulfills these needs.
fn pop_assignment_for_core(_core_idx: CoreIndex) -> Option<Assignment> {
let mut queue: VecDeque<Assignment> = MockAssignmentQueue::<T>::get();
let front = queue.pop_front();
// Write changes to storage.
MockAssignmentQueue::<T>::set(queue);
front
}
// We don't care about core affinity in the test assigner
fn report_processed(_assignment: Assignment) {}
// The results of this are tested in assigner_on_demand tests. No need to represent it
// in the mock assigner.
fn push_back_assignment(_assignment: Assignment) {}
// Gets the provider config we set earlier using `set_assignment_provider_config`, falling
// back to the on demand parachain configuration if none was set.
fn get_provider_config(_core_idx: CoreIndex) -> AssignmentProviderConfig<BlockNumber> {
match MockProviderConfig::<T>::get() {
Some(config) => config,
None => AssignmentProviderConfig {
max_availability_timeouts: 1,
ttl: BlockNumber::from(5u32),
},
}
}
#[cfg(any(feature = "runtime-benchmarks", test))]
fn get_mock_assignment(_: CoreIndex, para_id: ParaId) -> Assignment {
Assignment::Bulk(para_id)
}
fn session_core_count() -> u32 {
MockCoreCount::<T>::get().unwrap_or(5)
}
}
}
impl mock_assigner::pallet::Config for Test {}
pub struct FoolIdentificationOf;
impl sp_runtime::traits::Convert<AccountId, Option<()>> for FoolIdentificationOf {
fn convert(_: AccountId) -> Option<()> {
@@ -506,6 +506,21 @@ impl OnNewHead for Tuple {
}
}
/// Assign coretime to some parachain.
///
/// This assigns coretime to a parachain without using the coretime chain. Thus, this should only be
/// used for testing purposes.
pub trait AssignCoretime {
/// ONLY USE FOR TESTING OR GENESIS.
fn assign_coretime(id: ParaId) -> DispatchResult;
}
impl AssignCoretime for () {
fn assign_coretime(_: ParaId) -> DispatchResult {
Ok(())
}
}
pub trait WeightInfo {
fn force_set_current_code(c: u32) -> Weight;
fn force_set_current_head(s: u32) -> Weight;
@@ -605,6 +620,13 @@ pub mod pallet {
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
/// Runtime hook for assigning coretime for a given parachain.
///
/// This is only used at genesis or by root.
///
/// TODO: Remove once coretime is the standard accross all chains.
type AssignCoretime: AssignCoretime;
}
#[pallet::event]
@@ -838,6 +860,8 @@ pub mod pallet {
panic!("empty validation code is not allowed in genesis");
}
Pallet::<T>::initialize_para_now(&mut parachains, *id, genesis_args);
T::AssignCoretime::assign_coretime(*id)
.expect("Assigning coretime works at genesis; qed");
}
// parachains are flushed on drop
}
@@ -18,7 +18,9 @@ use super::*;
use crate::{inclusion, ParaId};
use frame_benchmarking::{benchmarks, impl_benchmark_test_suite};
use frame_system::RawOrigin;
use sp_std::collections::btree_map::BTreeMap;
use sp_std::{cmp::min, collections::btree_map::BTreeMap};
use primitives::v6::GroupIndex;
use crate::builder::BenchBuilder;
@@ -116,7 +118,9 @@ benchmarks! {
// There is 1 backed,
assert_eq!(benchmark.backed_candidates.len(), 1);
// with `v` validity votes.
assert_eq!(benchmark.backed_candidates.get(0).unwrap().validity_votes.len(), v as usize);
// let votes = v as usize;
let votes = min(scheduler::Pallet::<T>::group_validators(GroupIndex::from(0)).unwrap().len(), v as usize);
assert_eq!(benchmark.backed_candidates.get(0).unwrap().validity_votes.len(), votes);
benchmark.bitfields.clear();
benchmark.disputes.clear();
@@ -138,7 +142,7 @@ benchmarks! {
let descriptor = backing_validators.0.descriptor();
assert_eq!(ParaId::from(para_id), descriptor.para_id);
assert_eq!(header.hash(), descriptor.relay_parent);
assert_eq!(backing_validators.1.len(), v as usize);
assert_eq!(backing_validators.1.len(), votes);
}
assert_eq!(
@@ -167,11 +171,14 @@ benchmarks! {
let mut benchmark = scenario.data.clone();
// let votes = BenchBuilder::<T>::fallback_min_validity_votes() as usize;
let votes = min(scheduler::Pallet::<T>::group_validators(GroupIndex::from(0)).unwrap().len(), BenchBuilder::<T>::fallback_min_validity_votes() as usize);
// There is 1 backed
assert_eq!(benchmark.backed_candidates.len(), 1);
assert_eq!(
benchmark.backed_candidates.get(0).unwrap().validity_votes.len() as u32,
BenchBuilder::<T>::fallback_min_validity_votes()
benchmark.backed_candidates.get(0).unwrap().validity_votes.len(),
votes,
);
benchmark.bitfields.clear();
@@ -197,8 +204,8 @@ benchmarks! {
assert_eq!(ParaId::from(para_id), descriptor.para_id);
assert_eq!(header.hash(), descriptor.relay_parent);
assert_eq!(
backing_validators.1.len() as u32,
BenchBuilder::<T>::fallback_min_validity_votes()
backing_validators.1.len(),
votes,
);
}
@@ -548,7 +548,7 @@ impl<T: Config> Pallet<T> {
let disputed_bitfield = create_disputed_bitfield(expected_bits, freed_disputed.keys());
if !freed_disputed.is_empty() {
<scheduler::Pallet<T>>::update_claimqueue(freed_disputed.clone(), now);
<scheduler::Pallet<T>>::free_cores_and_fill_claimqueue(freed_disputed.clone(), now);
}
let bitfields = sanitize_bitfields::<T>(
@@ -580,7 +580,7 @@ impl<T: Config> Pallet<T> {
let freed = collect_all_freed_cores::<T, _>(freed_concluded.iter().cloned());
<scheduler::Pallet<T>>::update_claimqueue(freed, now);
<scheduler::Pallet<T>>::free_cores_and_fill_claimqueue(freed, now);
let scheduled = <scheduler::Pallet<T>>::scheduled_paras()
.map(|(core_idx, para_id)| (para_id, core_idx))
.collect();
@@ -25,7 +25,8 @@ mod enter {
use super::*;
use crate::{
builder::{Bench, BenchBuilder},
mock::{new_test_ext, BlockLength, BlockWeights, MockGenesisConfig, Test},
mock::{mock_assigner, new_test_ext, BlockLength, BlockWeights, MockGenesisConfig, Test},
scheduler::common::Assignment,
};
use assert_matches::assert_matches;
use frame_support::assert_ok;
@@ -39,6 +40,7 @@ mod enter {
backed_and_concluding: BTreeMap<u32, u32>,
num_validators_per_core: u32,
code_upgrade: Option<u32>,
fill_claimqueue: bool,
}
fn make_inherent_data(
@@ -48,6 +50,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core,
code_upgrade,
fill_claimqueue,
}: TestConfig,
) -> Bench<Test> {
let builder = BenchBuilder::<Test>::new()
@@ -58,7 +61,15 @@ mod enter {
.set_max_validators_per_core(num_validators_per_core)
.set_dispute_statements(dispute_statements)
.set_backed_and_concluding_cores(backed_and_concluding)
.set_dispute_sessions(&dispute_sessions[..]);
.set_dispute_sessions(&dispute_sessions[..])
.set_fill_claimqueue(fill_claimqueue);
// Setup some assignments as needed:
mock_assigner::Pallet::<Test>::set_core_count(builder.max_cores());
for core_index in 0..builder.max_cores() {
// Core index == para_id in this case
mock_assigner::Pallet::<Test>::add_test_assignment(Assignment::Bulk(core_index.into()));
}
if let Some(code_size) = code_upgrade {
builder.set_code_upgrade(code_size).build()
@@ -88,6 +99,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 1,
code_upgrade: None,
fill_claimqueue: false,
});
// We expect the scenario to have cores 0 & 1 with pending availability. The backed
@@ -238,6 +250,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 5,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -308,6 +321,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 6,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -376,6 +390,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 4,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -460,6 +475,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 5,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -544,6 +560,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 5,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -627,6 +644,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 5,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -666,15 +684,9 @@ mod enter {
// * 3 disputes.
assert_eq!(limit_inherent_data.disputes.len(), 2);
assert_ok!(Pallet::<Test>::enter(
frame_system::RawOrigin::None.into(),
limit_inherent_data,
));
// TODO [now]: this assertion fails with async backing runtime.
assert_eq!(
// The length of this vec is equal to the number of candidates, so we know our 2
// backed candidates did not get filtered out
// The length of this vec is equal to the number of candidates, so we know 1
// candidate got filtered out
Pallet::<Test>::on_chain_votes().unwrap().backing_validators_per_candidate.len(),
1
);
@@ -684,6 +696,11 @@ mod enter {
Pallet::<Test>::on_chain_votes().unwrap().session,
2
);
assert_ok!(Pallet::<Test>::enter(
frame_system::RawOrigin::None.into(),
limit_inherent_data,
));
});
}
@@ -713,6 +730,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 5,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -778,6 +796,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 5,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -841,6 +860,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 5,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -905,6 +925,7 @@ mod enter {
backed_and_concluding,
num_validators_per_core: 5,
code_upgrade: None,
fill_claimqueue: false,
});
let expected_para_inherent_data = scenario.data.clone();
@@ -62,7 +62,7 @@ pub fn availability_cores<T: initializer::Config>() -> Vec<CoreState<T::Hash, Bl
//
// At the end of a session we clear the claim queues: Without this update call, nothing would be
// scheduled to the client.
<scheduler::Pallet<T>>::update_claimqueue(Vec::new(), now);
<scheduler::Pallet<T>>::free_cores_and_fill_claimqueue(Vec::new(), now);
let time_out_for = <scheduler::Pallet<T>>::availability_timeout_predicate();
+77 -129
View File
@@ -65,7 +65,7 @@ pub mod migration;
pub mod pallet {
use super::*;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
#[pallet::pallet]
#[pallet::without_storage_info]
@@ -99,15 +99,14 @@ pub mod pallet {
#[pallet::storage]
#[pallet::getter(fn availability_cores)]
pub(crate) type AvailabilityCores<T: Config> =
StorageValue<_, Vec<CoreOccupied<BlockNumberFor<T>>>, ValueQuery>;
StorageValue<_, Vec<CoreOccupiedType<T>>, ValueQuery>;
/// Representation of a core in `AvailabilityCores`.
///
/// This is not to be confused with `CoreState` which is an enriched variant of this and exposed
/// to the node side. It also provides information about scheduled/upcoming assignments for
/// example and is computed on the fly in the `availability_cores` runtime call.
#[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
#[cfg_attr(feature = "std", derive(PartialEq))]
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq)]
pub enum CoreOccupied<N> {
/// No candidate is waiting availability on this core right now (the core is not occupied).
Free,
@@ -115,6 +114,9 @@ pub mod pallet {
Paras(ParasEntry<N>),
}
/// Conveninece type alias for `CoreOccupied`.
pub type CoreOccupiedType<T> = CoreOccupied<BlockNumberFor<T>>;
impl<N> CoreOccupied<N> {
/// Is core free?
pub fn is_free(&self) -> bool {
@@ -149,16 +151,13 @@ pub mod pallet {
/// a block. Runtime APIs should be used to determine scheduled cores/ for the upcoming block.
#[pallet::storage]
#[pallet::getter(fn claimqueue)]
pub(crate) type ClaimQueue<T: Config> = StorageValue<
_,
BTreeMap<CoreIndex, VecDeque<Option<ParasEntry<BlockNumberFor<T>>>>>,
ValueQuery,
>;
pub(crate) type ClaimQueue<T: Config> =
StorageValue<_, BTreeMap<CoreIndex, VecDeque<ParasEntryType<T>>>, ValueQuery>;
/// Assignments as tracked in the claim queue.
#[derive(Clone, Encode, Decode, TypeInfo, PartialEq, RuntimeDebug)]
pub struct ParasEntry<N = BlockNumber> {
/// The underlying `Assignment`
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq, Clone)]
pub struct ParasEntry<N> {
/// The underlying [`Assignment`].
pub assignment: Assignment,
/// The number of times the entry has timed out in availability already.
pub availability_timeouts: u32,
@@ -169,37 +168,18 @@ pub mod pallet {
pub ttl: N,
}
impl<N> ParasEntry<N> {
/// Return `Id` from the underlying `Assignment`.
pub fn para_id(&self) -> ParaId {
self.assignment.para_id
}
/// Convenience type declaration for `ParasEntry`.
pub type ParasEntryType<T> = ParasEntry<BlockNumberFor<T>>;
impl<N> ParasEntry<N> {
/// Create a new `ParasEntry`.
pub fn new(assignment: Assignment, now: N) -> Self {
ParasEntry { assignment, availability_timeouts: 0, ttl: now }
}
}
/// How a core is mapped to a backing group and a `ParaId`
#[derive(Clone, Encode, Decode, PartialEq, TypeInfo)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct CoreAssignment<BlockNumber> {
/// The core that is assigned.
pub core: CoreIndex,
/// The para id and accompanying information needed to collate and back a parablock.
pub paras_entry: ParasEntry<BlockNumber>,
}
impl<BlockNumber> CoreAssignment<BlockNumber> {
/// Returns the [`ParaId`] of the assignment.
/// Return `Id` from the underlying `Assignment`.
pub fn para_id(&self) -> ParaId {
self.paras_entry.para_id()
}
/// Returns the inner [`ParasEntry`] of the assignment.
pub fn to_paras_entry(self) -> ParasEntry<BlockNumber> {
self.paras_entry
self.assignment.para_id()
}
}
@@ -219,8 +199,6 @@ pub mod pallet {
}
type PositionInClaimqueue = u32;
type TimedoutParas<T> = BTreeMap<CoreIndex, ParasEntry<BlockNumberFor<T>>>;
type ConcludedParas = BTreeMap<CoreIndex, ParaId>;
impl<T: Config> Pallet<T> {
/// Called by the initializer to initialize the scheduler pallet.
@@ -253,7 +231,7 @@ impl<T: Config> Pallet<T> {
);
AvailabilityCores::<T>::mutate(|cores| {
cores.resize(n_cores as _, CoreOccupied::Free);
cores.resize_with(n_cores as _, || CoreOccupied::Free);
});
// shuffle validators into groups.
@@ -298,9 +276,8 @@ impl<T: Config> Pallet<T> {
/// with the reason for them being freed. Returns a tuple of concluded and timedout paras.
fn free_cores(
just_freed_cores: impl IntoIterator<Item = (CoreIndex, FreedReason)>,
) -> (ConcludedParas, TimedoutParas<T>) {
let mut timedout_paras: BTreeMap<CoreIndex, ParasEntry<BlockNumberFor<T>>> =
BTreeMap::new();
) -> (BTreeMap<CoreIndex, Assignment>, BTreeMap<CoreIndex, ParasEntryType<T>>) {
let mut timedout_paras: BTreeMap<CoreIndex, ParasEntryType<T>> = BTreeMap::new();
let mut concluded_paras = BTreeMap::new();
AvailabilityCores::<T>::mutate(|cores| {
@@ -310,21 +287,22 @@ impl<T: Config> Pallet<T> {
.into_iter()
.filter(|(freed_index, _)| (freed_index.0 as usize) < c_len)
.for_each(|(freed_index, freed_reason)| {
match &cores[freed_index.0 as usize] {
match sp_std::mem::replace(
&mut cores[freed_index.0 as usize],
CoreOccupied::Free,
) {
CoreOccupied::Free => {},
CoreOccupied::Paras(entry) => {
match freed_reason {
FreedReason::Concluded => {
concluded_paras.insert(freed_index, entry.para_id());
concluded_paras.insert(freed_index, entry.assignment);
},
FreedReason::TimedOut => {
timedout_paras.insert(freed_index, entry.clone());
timedout_paras.insert(freed_index, entry);
},
};
},
};
cores[freed_index.0 as usize] = CoreOccupied::Free;
})
});
@@ -379,30 +357,36 @@ impl<T: Config> Pallet<T> {
for (idx, _) in (0u32..).zip(availability_cores) {
let core_idx = CoreIndex(idx);
if let Some(core_claimqueue) = cq.get_mut(&core_idx) {
let mut dropped_claims: Vec<Option<ParaId>> = vec![];
core_claimqueue.retain(|maybe_entry| {
if let Some(entry) = maybe_entry {
let mut i = 0;
let mut num_dropped = 0;
while i < core_claimqueue.len() {
let maybe_dropped = if let Some(entry) = core_claimqueue.get(i) {
if entry.ttl < now {
dropped_claims.push(Some(entry.para_id()));
return false
core_claimqueue.remove(i)
} else {
None
}
}
true
});
} else {
None
};
// For all claims dropped due to TTL, attempt to pop a new entry to
// the back of the claimqueue.
for drop in dropped_claims {
match T::AssignmentProvider::pop_assignment_for_core(core_idx, drop) {
Some(assignment) => {
let AssignmentProviderConfig { ttl, .. } =
T::AssignmentProvider::get_provider_config(core_idx);
core_claimqueue.push_back(Some(ParasEntry::new(
assignment.clone(),
now + ttl,
)));
},
None => (),
if let Some(dropped) = maybe_dropped {
num_dropped += 1;
T::AssignmentProvider::report_processed(dropped.assignment);
} else {
i += 1;
}
}
for _ in 0..num_dropped {
// For all claims dropped due to TTL, attempt to pop a new entry to
// the back of the claimqueue.
if let Some(assignment) =
T::AssignmentProvider::pop_assignment_for_core(core_idx)
{
let AssignmentProviderConfig { ttl, .. } =
T::AssignmentProvider::get_provider_config(core_idx);
core_claimqueue.push_back(ParasEntry::new(assignment, now + ttl));
}
}
}
@@ -514,14 +498,12 @@ impl<T: Config> Pallet<T> {
/// Return the next thing that will be scheduled on this core assuming it is currently
/// occupied and the candidate occupying it became available.
pub(crate) fn next_up_on_available(core: CoreIndex) -> Option<ScheduledCore> {
ClaimQueue::<T>::get().get(&core).and_then(|a| {
a.iter()
.find_map(|e| e.as_ref())
.map(|pe| Self::paras_entry_to_scheduled_core(pe))
})
ClaimQueue::<T>::get()
.get(&core)
.and_then(|a| a.front().map(|pe| Self::paras_entry_to_scheduled_core(pe)))
}
fn paras_entry_to_scheduled_core(pe: &ParasEntry<BlockNumberFor<T>>) -> ScheduledCore {
fn paras_entry_to_scheduled_core(pe: &ParasEntryType<T>) -> ScheduledCore {
ScheduledCore { para_id: pe.para_id(), collator: None }
}
@@ -552,35 +534,33 @@ impl<T: Config> Pallet<T> {
/// Pushes occupied cores to the assignment provider.
fn push_occupied_cores_to_assignment_provider() {
AvailabilityCores::<T>::mutate(|cores| {
for (core_idx, core) in cores.iter_mut().enumerate() {
match core {
for core in cores.iter_mut() {
match sp_std::mem::replace(core, CoreOccupied::Free) {
CoreOccupied::Free => continue,
CoreOccupied::Paras(entry) => {
let core_idx = CoreIndex::from(core_idx as u32);
Self::maybe_push_assignment(core_idx, entry.clone());
Self::maybe_push_assignment(entry);
},
}
*core = CoreOccupied::Free;
}
});
}
// on new session
fn push_claimqueue_items_to_assignment_provider() {
for (core_idx, core_claimqueue) in ClaimQueue::<T>::take() {
for (_, claim_queue) in ClaimQueue::<T>::take() {
// Push back in reverse order so that when we pop from the provider again,
// the entries in the claimqueue are in the same order as they are right now.
for para_entry in core_claimqueue.into_iter().flatten().rev() {
Self::maybe_push_assignment(core_idx, para_entry);
for para_entry in claim_queue.into_iter().rev() {
Self::maybe_push_assignment(para_entry);
}
}
}
/// Push assignments back to the provider on session change unless the paras
/// timed out on availability before.
fn maybe_push_assignment(core_idx: CoreIndex, pe: ParasEntry<BlockNumberFor<T>>) {
fn maybe_push_assignment(pe: ParasEntryType<T>) {
if pe.availability_timeouts == 0 {
T::AssignmentProvider::push_assignment_for_core(core_idx, pe.assignment);
T::AssignmentProvider::push_back_assignment(pe.assignment);
}
}
@@ -591,31 +571,8 @@ impl<T: Config> Pallet<T> {
<configuration::Pallet<T>>::config().scheduling_lookahead
}
/// Updates the claimqueue by moving it to the next paras and filling empty spots with new
/// paras.
pub(crate) fn update_claimqueue(
just_freed_cores: impl IntoIterator<Item = (CoreIndex, FreedReason)>,
now: BlockNumberFor<T>,
) {
Self::move_claimqueue_forward();
Self::free_cores_and_fill_claimqueue(just_freed_cores, now)
}
/// Moves all elements in the claimqueue forward.
fn move_claimqueue_forward() {
let mut cq = ClaimQueue::<T>::get();
for core_queue in cq.values_mut() {
// First pop the finished claims from the front.
if let Some(None) = core_queue.front() {
core_queue.pop_front();
}
}
ClaimQueue::<T>::set(cq);
}
/// Frees cores and fills the free claimqueue spots by popping from the `AssignmentProvider`.
fn free_cores_and_fill_claimqueue(
pub fn free_cores_and_fill_claimqueue(
just_freed_cores: impl IntoIterator<Item = (CoreIndex, FreedReason)>,
now: BlockNumberFor<T>,
) {
@@ -651,19 +608,19 @@ impl<T: Config> Pallet<T> {
} else {
// Consider timed out assignments for on demand parachains as concluded for
// the assignment provider
let ret = concluded_paras.insert(core_idx, entry.para_id());
let ret = concluded_paras.insert(core_idx, entry.assignment);
debug_assert!(ret.is_none());
}
}
// We consider occupied cores to be part of the claimqueue
if let Some(concluded_para) = concluded_paras.remove(&core_idx) {
T::AssignmentProvider::report_processed(concluded_para);
}
// We consider occupied cores to be part of the claimqueue
let n_lookahead_used = cq.get(&core_idx).map_or(0, |v| v.len() as u32) +
if Self::is_core_occupied(core_idx) { 1 } else { 0 };
for _ in n_lookahead_used..n_lookahead {
let concluded_para = concluded_paras.remove(&core_idx);
if let Some(assignment) =
T::AssignmentProvider::pop_assignment_for_core(core_idx, concluded_para)
{
if let Some(assignment) = T::AssignmentProvider::pop_assignment_for_core(core_idx) {
Self::add_to_claimqueue(core_idx, ParasEntry::new(assignment, now + ttl));
}
}
@@ -680,9 +637,9 @@ impl<T: Config> Pallet<T> {
}
}
fn add_to_claimqueue(core_idx: CoreIndex, pe: ParasEntry<BlockNumberFor<T>>) {
fn add_to_claimqueue(core_idx: CoreIndex, pe: ParasEntryType<T>) {
ClaimQueue::<T>::mutate(|la| {
la.entry(core_idx).or_default().push_back(Some(pe));
la.entry(core_idx).or_default().push_back(pe);
});
}
@@ -690,19 +647,16 @@ impl<T: Config> Pallet<T> {
fn remove_from_claimqueue(
core_idx: CoreIndex,
para_id: ParaId,
) -> Result<(PositionInClaimqueue, ParasEntry<BlockNumberFor<T>>), &'static str> {
) -> Result<(PositionInClaimqueue, ParasEntryType<T>), &'static str> {
ClaimQueue::<T>::mutate(|cq| {
let core_claims = cq.get_mut(&core_idx).ok_or("core_idx not found in lookahead")?;
let pos = core_claims
.iter()
.position(|a| a.as_ref().map_or(false, |pe| pe.para_id() == para_id))
.position(|pe| pe.para_id() == para_id)
.ok_or("para id not found at core_idx lookahead")?;
let pe = core_claims
.remove(pos)
.ok_or("remove returned None")?
.ok_or("Element in Claimqueue was None.")?;
let pe = core_claims.remove(pos).ok_or("remove returned None")?;
Ok((pos as u32, pe))
})
@@ -710,16 +664,10 @@ impl<T: Config> Pallet<T> {
/// Paras scheduled next in the claim queue.
pub(crate) fn scheduled_paras() -> impl Iterator<Item = (CoreIndex, ParaId)> {
Self::scheduled_entries().map(|(core_idx, e)| (core_idx, e.assignment.para_id))
}
/// Internal access to entries at the top of the claim queue.
fn scheduled_entries() -> impl Iterator<Item = (CoreIndex, ParasEntry<BlockNumberFor<T>>)> {
let claimqueue = ClaimQueue::<T>::get();
claimqueue
.into_iter()
.filter_map(|(core_idx, v)| v.front().cloned().flatten().map(|e| (core_idx, e)))
.filter_map(|(core_idx, v)| v.front().map(|e| (core_idx, e.assignment.para_id())))
}
#[cfg(any(feature = "runtime-benchmarks", test))]
@@ -16,29 +16,39 @@
//! Common traits and types used by the scheduler and assignment providers.
use frame_support::pallet_prelude::*;
use primitives::{CoreIndex, Id as ParaId};
use scale_info::TypeInfo;
use sp_std::prelude::*;
use sp_runtime::{
codec::{Decode, Encode},
RuntimeDebug,
};
// Only used to link to configuration documentation.
#[allow(unused)]
use crate::configuration::HostConfiguration;
use primitives::{CoreIndex, Id as ParaId};
/// An assignment for a parachain scheduled to be backed and included in a relay chain block.
#[derive(Clone, Encode, Decode, PartialEq, TypeInfo, RuntimeDebug)]
pub struct Assignment {
/// Assignment's ParaId
pub para_id: ParaId,
/// Assignment (ParaId -> CoreIndex).
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, Clone, PartialEq)]
pub enum Assignment {
/// A pool assignment.
Pool {
/// The assigned para id.
para_id: ParaId,
/// The core index the para got assigned to.
core_index: CoreIndex,
},
/// A bulk assignment.
Bulk(ParaId),
}
impl Assignment {
/// Create a new `Assignment`.
pub fn new(para_id: ParaId) -> Self {
Self { para_id }
/// Returns the [`ParaId`] this assignment is associated to.
pub fn para_id(&self) -> ParaId {
match self {
Self::Pool { para_id, .. } => *para_id,
Self::Bulk(para_id) => *para_id,
}
}
}
#[derive(Encode, Decode, TypeInfo)]
/// A set of variables required by the scheduler in order to operate.
pub struct AssignmentProviderConfig<BlockNumber> {
/// How many times a collation can time out on availability.
@@ -51,22 +61,42 @@ pub struct AssignmentProviderConfig<BlockNumber> {
}
pub trait AssignmentProvider<BlockNumber> {
/// How many cores are allocated to this provider.
fn session_core_count() -> u32;
/// Pops an [`Assignment`] from the provider for a specified [`CoreIndex`].
/// The `concluded_para` field makes the caller report back to the provider
/// which [`ParaId`] it processed last on the supplied [`CoreIndex`].
fn pop_assignment_for_core(
core_idx: CoreIndex,
concluded_para: Option<ParaId>,
) -> Option<Assignment>;
///
/// This is where assignments come into existance.
fn pop_assignment_for_core(core_idx: CoreIndex) -> Option<Assignment>;
/// Push back an already popped assignment. Intended for provider implementations
/// that need to be able to keep track of assignments over session boundaries,
/// such as the on demand assignment provider.
fn push_assignment_for_core(core_idx: CoreIndex, assignment: Assignment);
/// A previously popped `Assignment` has been fully processed.
///
/// Report back to the assignment provider that an assignment is done and no longer present in
/// the scheduler.
///
/// This is one way of the life of an assignment coming to an end.
fn report_processed(assignment: Assignment);
/// Push back a previously popped assignment.
///
/// If the assignment could not be processed within the current session, it can be pushed back
/// to the assignment provider in order to be poppped again later.
///
/// This is the second way the life of an assignment can come to an end.
fn push_back_assignment(assignment: Assignment);
/// Returns a set of variables needed by the scheduler
fn get_provider_config(core_idx: CoreIndex) -> AssignmentProviderConfig<BlockNumber>;
/// Push some assignment for mocking/benchmarks purposes.
///
/// Useful for benchmarks and testing. The returned assignment is "valid" and can if need be
/// passed into `report_processed` for example.
#[cfg(any(feature = "runtime-benchmarks", test))]
fn get_mock_assignment(core_idx: CoreIndex, para_id: ParaId) -> Assignment;
/// How many cores are allocated to this provider.
///
/// As the name suggests the core count has to be session buffered:
///
/// - Core count has to be predetermined for the next session in the current session.
/// - Core count must not change during a session.
fn session_core_count() -> u32;
}
@@ -22,9 +22,18 @@ use frame_support::{
traits::OnRuntimeUpgrade, weights::Weight,
};
/// Old/legacy assignment representation (v0).
///
/// `Assignment` used to be a concrete type with the same layout V0Assignment, idential on all
/// assignment providers. This can be removed once storage has been migrated.
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Clone)]
struct V0Assignment {
pub para_id: ParaId,
}
/// Old scheduler with explicit parathreads and `Scheduled` storage instead of `ClaimQueue`.
mod v0 {
use super::*;
use primitives::{CollatorId, Id};
#[storage_alias]
@@ -90,29 +99,123 @@ mod v0 {
}
}
pub mod v1 {
// `ClaimQueue` got introduced.
//
// - Items are `Option` for some weird reason.
// - Assignments only consist of `ParaId`, `Assignment` is a concrete type (Same as V0Assignment).
mod v1 {
use frame_support::{
pallet_prelude::ValueQuery, storage_alias, traits::OnRuntimeUpgrade, weights::Weight,
};
use frame_system::pallet_prelude::BlockNumberFor;
use super::*;
use crate::scheduler;
#[allow(deprecated)]
pub type MigrateToV1<T> = VersionedMigration<
0,
1,
UncheckedMigrateToV1<T>,
#[storage_alias]
pub(super) type ClaimQueue<T: Config> = StorageValue<
Pallet<T>,
<T as frame_system::Config>::DbWeight,
BTreeMap<CoreIndex, VecDeque<Option<ParasEntry<BlockNumberFor<T>>>>>,
ValueQuery,
>;
#[deprecated(note = "Use MigrateToV1 instead")]
#[storage_alias]
pub(super) type AvailabilityCores<T: Config> =
StorageValue<Pallet<T>, Vec<CoreOccupied<BlockNumberFor<T>>>, ValueQuery>;
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq)]
pub(super) enum CoreOccupied<N> {
/// No candidate is waiting availability on this core right now (the core is not occupied).
Free,
/// A para is currently waiting for availability/inclusion on this core.
Paras(ParasEntry<N>),
}
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq)]
pub(super) struct ParasEntry<N> {
/// The underlying `Assignment`
pub(super) assignment: V0Assignment,
/// The number of times the entry has timed out in availability already.
pub(super) availability_timeouts: u32,
/// The block height until this entry needs to be backed.
///
/// If missed the entry will be removed from the claim queue without ever having occupied
/// the core.
pub(super) ttl: N,
}
impl<N> ParasEntry<N> {
/// Create a new `ParasEntry`.
pub(super) fn new(assignment: V0Assignment, now: N) -> Self {
ParasEntry { assignment, availability_timeouts: 0, ttl: now }
}
/// Return `Id` from the underlying `Assignment`.
pub(super) fn para_id(&self) -> ParaId {
self.assignment.para_id
}
}
fn add_to_claimqueue<T: Config>(core_idx: CoreIndex, pe: ParasEntry<BlockNumberFor<T>>) {
ClaimQueue::<T>::mutate(|la| {
la.entry(core_idx).or_default().push_back(Some(pe));
});
}
/// Migration to V1
pub struct UncheckedMigrateToV1<T>(sp_std::marker::PhantomData<T>);
#[allow(deprecated)]
impl<T: Config> OnRuntimeUpgrade for UncheckedMigrateToV1<T> {
fn on_runtime_upgrade() -> Weight {
let weight_consumed = migrate_to_v1::<T>();
let mut weight: Weight = Weight::zero();
log::info!(target: scheduler::LOG_TARGET, "Migrating para scheduler storage to v1");
v0::ParathreadQueue::<T>::kill();
v0::ParathreadClaimIndex::<T>::kill();
weight_consumed
let now = <frame_system::Pallet<T>>::block_number();
let scheduled = v0::Scheduled::<T>::take();
let sched_len = scheduled.len() as u64;
for core_assignment in scheduled {
let core_idx = core_assignment.core;
let assignment = V0Assignment { para_id: core_assignment.para_id };
let pe = v1::ParasEntry::new(assignment, now);
v1::add_to_claimqueue::<T>(core_idx, pe);
}
let parachains = paras::Pallet::<T>::parachains();
let availability_cores = v0::AvailabilityCores::<T>::take();
let mut new_availability_cores = Vec::new();
for (core_index, core) in availability_cores.into_iter().enumerate() {
let new_core = if let Some(core) = core {
match core {
v0::CoreOccupied::Parachain =>
v1::CoreOccupied::Paras(v1::ParasEntry::new(
V0Assignment { para_id: parachains[core_index] },
now,
)),
v0::CoreOccupied::Parathread(entry) => v1::CoreOccupied::Paras(
v1::ParasEntry::new(V0Assignment { para_id: entry.claim.0 }, now),
),
}
} else {
v1::CoreOccupied::Free
};
new_availability_cores.push(new_core);
}
v1::AvailabilityCores::<T>::set(new_availability_cores);
// 2x as once for Scheduled and once for Claimqueue
weight.saturating_accrue(T::DbWeight::get().reads_writes(2 * sched_len, 2 * sched_len));
// reading parachains + availability_cores, writing AvailabilityCores
weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 1));
// 2x kill
weight.saturating_accrue(T::DbWeight::get().writes(2));
log::info!(target: scheduler::LOG_TARGET, "Migrated para scheduler storage to v1");
weight
}
#[cfg(feature = "try-runtime")]
@@ -138,9 +241,9 @@ pub mod v1 {
);
let expected_len = u32::decode(&mut &state[..]).unwrap();
let availability_cores_waiting = super::AvailabilityCores::<T>::get()
.iter()
.filter(|c| !matches!(c, CoreOccupied::Free))
let availability_cores_waiting = v1::AvailabilityCores::<T>::get()
.into_iter()
.filter(|c| !matches!(c, v1::CoreOccupied::Free))
.count();
ensure!(
@@ -154,51 +257,150 @@ pub mod v1 {
}
}
pub fn migrate_to_v1<T: crate::scheduler::Config>() -> Weight {
let mut weight: Weight = Weight::zero();
/// 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,
>;
v0::ParathreadQueue::<T>::kill();
v0::ParathreadClaimIndex::<T>::kill();
mod v2 {
use super::*;
use crate::scheduler;
let now = <frame_system::Pallet<T>>::block_number();
let scheduled = v0::Scheduled::<T>::take();
let sched_len = scheduled.len() as u64;
for core_assignment in scheduled {
let core_idx = core_assignment.core;
let assignment = Assignment::new(core_assignment.para_id);
let pe = ParasEntry::new(assignment, now);
Pallet::<T>::add_to_claimqueue(core_idx, pe);
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq)]
pub(crate) enum CoreOccupied<N> {
Free,
Paras(ParasEntry<N>),
}
let parachains = paras::Pallet::<T>::parachains();
let availability_cores = v0::AvailabilityCores::<T>::take();
let mut new_availability_cores = Vec::new();
for (core_index, core) in availability_cores.into_iter().enumerate() {
let new_core = if let Some(core) = core {
match core {
v0::CoreOccupied::Parachain => CoreOccupied::Paras(ParasEntry::new(
Assignment::new(parachains[core_index]),
now,
)),
v0::CoreOccupied::Parathread(entry) =>
CoreOccupied::Paras(ParasEntry::new(Assignment::new(entry.claim.0), now)),
}
} else {
CoreOccupied::Free
};
new_availability_cores.push(new_core);
#[derive(Encode, Decode, TypeInfo, RuntimeDebug, PartialEq)]
pub(crate) struct ParasEntry<N> {
pub assignment: Assignment,
pub availability_timeouts: u32,
pub ttl: N,
}
super::AvailabilityCores::<T>::set(new_availability_cores);
// V2 (no Option wrapper) and new [`Assignment`].
#[storage_alias]
pub(crate) type ClaimQueue<T: Config> = StorageValue<
Pallet<T>,
BTreeMap<CoreIndex, VecDeque<ParasEntry<BlockNumberFor<T>>>>,
ValueQuery,
>;
// 2x as once for Scheduled and once for Claimqueue
weight = weight.saturating_add(T::DbWeight::get().reads_writes(2 * sched_len, 2 * sched_len));
// reading parachains + availability_cores, writing AvailabilityCores
weight = weight.saturating_add(T::DbWeight::get().reads_writes(2, 1));
// 2x kill
weight = weight.saturating_add(T::DbWeight::get().writes(2));
#[storage_alias]
pub(crate) type AvailabilityCores<T: Config> =
StorageValue<Pallet<T>, Vec<CoreOccupied<BlockNumberFor<T>>>, ValueQuery>;
weight
fn is_bulk<T: Config>(core_index: CoreIndex) -> bool {
core_index.0 < paras::Parachains::<T>::decode_len().unwrap_or(0) as u32
}
/// Migration to V2
pub struct UncheckedMigrateToV2<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for UncheckedMigrateToV2<T> {
fn on_runtime_upgrade() -> Weight {
let mut weight: Weight = Weight::zero();
let old = v1::ClaimQueue::<T>::take();
let new = old
.into_iter()
.map(|(k, v)| {
(
k,
v.into_iter()
.flatten()
.map(|p| {
let assignment = if is_bulk::<T>(k) {
Assignment::Bulk(p.para_id())
} else {
Assignment::Pool { para_id: p.para_id(), core_index: k }
};
ParasEntry {
assignment,
availability_timeouts: p.availability_timeouts,
ttl: p.ttl,
}
})
.collect::<VecDeque<_>>(),
)
})
.collect::<BTreeMap<CoreIndex, VecDeque<ParasEntry<BlockNumberFor<T>>>>>();
ClaimQueue::<T>::put(new);
let old = v1::AvailabilityCores::<T>::get();
let new = old
.into_iter()
.enumerate()
.map(|(k, a)| match a {
v1::CoreOccupied::Free => CoreOccupied::Free,
v1::CoreOccupied::Paras(paras) => {
let assignment = if is_bulk::<T>((k as u32).into()) {
Assignment::Bulk(paras.para_id())
} else {
Assignment::Pool {
para_id: paras.para_id(),
core_index: (k as u32).into(),
}
};
CoreOccupied::Paras(ParasEntry {
assignment,
availability_timeouts: paras.availability_timeouts,
ttl: paras.ttl,
})
},
})
.collect::<Vec<_>>();
AvailabilityCores::<T>::put(new);
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
log::info!(target: scheduler::LOG_TARGET, "Migrating para scheduler storage to v2");
weight
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::DispatchError> {
log::trace!(
target: crate::scheduler::LOG_TARGET,
"ClaimQueue before migration: {}",
v1::ClaimQueue::<T>::get().len()
);
let bytes = u32::to_be_bytes(v1::ClaimQueue::<T>::get().len() as u32);
Ok(bytes.to_vec())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::DispatchError> {
log::trace!(target: crate::scheduler::LOG_TARGET, "Running post_upgrade()");
let old_len = u32::from_be_bytes(state.try_into().unwrap());
ensure!(
v2::ClaimQueue::<T>::get().len() as u32 == old_len,
"Old ClaimQueue completely moved to new ClaimQueue after migration"
);
Ok(())
}
}
}
/// Migrate `V1` to `V2` of the storage format.
pub type MigrateV1ToV2<T> = VersionedMigration<
1,
2,
v2::UncheckedMigrateToV2<T>,
Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;
File diff suppressed because it is too large Load Diff
@@ -62,7 +62,7 @@ fn run_to_block(
fn default_config() -> HostConfiguration<BlockNumber> {
HostConfiguration {
on_demand_cores: 1,
coretime_cores: 1,
dispute_period: 2,
needed_approvals: 3,
..Default::default()