referrerpolicy=no-referrer-when-downgrade

polkadot_runtime_parachains/scheduler/assigner_coretime/
mod.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! The parachain coretime assignment module.
18//!
19//! Handles scheduling of assignments coming from the coretime/broker chain. For on-demand
20//! assignments it relies on the separate on-demand pallet, where it forwards requests
21//! to.
22//!
23//! `CoreDescriptor` contains pointers to the begin and the end of a list of schedules, together
24//! with the currently active assignments.
25
26mod mock_helpers;
27#[cfg(test)]
28mod tests;
29
30use crate::{configuration, on_demand, ParaId};
31
32use alloc::{
33	collections::{BTreeMap, VecDeque},
34	vec::Vec,
35};
36use frame_support::{defensive, pallet_prelude::*};
37use frame_system::pallet_prelude::*;
38use polkadot_primitives::CoreIndex;
39use scale_info::TypeInfo;
40use sp_runtime::{
41	codec::{Decode, Encode},
42	traits::Saturating,
43	Debug,
44};
45
46pub use pallet_broker::CoreAssignment;
47
48pub use super::Config;
49
50/// Fraction expressed as a nominator with an assumed denominator of 57,600.
51#[derive(
52	Debug,
53	Clone,
54	Copy,
55	PartialEq,
56	Eq,
57	PartialOrd,
58	Ord,
59	Encode,
60	Decode,
61	DecodeWithMemTracking,
62	TypeInfo,
63)]
64pub struct PartsOf57600(u16);
65
66impl PartsOf57600 {
67	pub const ZERO: Self = Self(0);
68	pub const FULL: Self = Self(57600);
69
70	pub fn new_saturating(v: u16) -> Self {
71		Self::ZERO.saturating_add(Self(v))
72	}
73
74	/// Returns the inner value (test-only accessor).
75	#[cfg(test)]
76	pub fn value(&self) -> u16 {
77		self.0
78	}
79
80	pub fn is_full(&self) -> bool {
81		*self == Self::FULL
82	}
83
84	pub fn saturating_add(self, rhs: Self) -> Self {
85		let inner = self.0.saturating_add(rhs.0);
86		if inner > 57600 {
87			Self(57600)
88		} else {
89			Self(inner)
90		}
91	}
92
93	pub fn saturating_sub(self, rhs: Self) -> Self {
94		Self(self.0.saturating_sub(rhs.0))
95	}
96
97	pub fn checked_add(self, rhs: Self) -> Option<Self> {
98		let inner = self.0.saturating_add(rhs.0);
99		if inner > 57600 {
100			None
101		} else {
102			Some(Self(inner))
103		}
104	}
105}
106
107/// Assignments as they are scheduled by block number
108///
109/// for a particular core.
110#[derive(Encode, Decode, TypeInfo)]
111#[cfg_attr(test, derive(PartialEq, Debug))]
112pub struct Schedule<N> {
113	/// Original assignments.
114	assignments: Vec<(CoreAssignment, PartsOf57600)>,
115	/// When do our assignments become invalid, if at all?
116	///
117	/// If this is `Some`, then this `CoreState` will be dropped at that block number. If this is
118	/// `None`, then we will keep serving our core assignments in a circle until a new set of
119	/// assignments is scheduled.
120	end_hint: Option<N>,
121
122	/// The next queued schedule for this core.
123	///
124	/// Schedules are forming a queue.
125	next_schedule: Option<N>,
126}
127
128impl<N> Schedule<N> {
129	/// Creates a new Schedule (for tests).
130	#[cfg(test)]
131	pub fn new(
132		assignments: Vec<(CoreAssignment, PartsOf57600)>,
133		end_hint: Option<N>,
134		next_schedule: Option<N>,
135	) -> Self {
136		Self { assignments, end_hint, next_schedule }
137	}
138
139	/// Accessor for assignments (needed by tests).
140	pub fn assignments(&self) -> &[(CoreAssignment, PartsOf57600)] {
141		&self.assignments
142	}
143
144	/// Accessor for end_hint (needed by tests).
145	#[cfg(test)]
146	pub fn end_hint(&self) -> Option<N>
147	where
148		N: Copy,
149	{
150		self.end_hint
151	}
152
153	/// Accessor for next_schedule (needed by migrations, tests, and try-runtime).
154	pub(super) fn next_schedule(&self) -> Option<N>
155	where
156		N: Copy,
157	{
158		self.next_schedule
159	}
160}
161
162/// Descriptor for a core.
163///
164/// Contains pointers to first and last schedule into `CoreSchedules` for that core and keeps track
165/// of the currently active work as well.
166#[derive(Encode, Decode, TypeInfo, Default)]
167#[cfg_attr(test, derive(PartialEq, Debug, Clone))]
168pub struct CoreDescriptor<N> {
169	/// Meta data about the queued schedules for this core.
170	queue: Option<QueueDescriptor<N>>,
171	/// Currently performed work.
172	current_work: Option<WorkState<N>>,
173}
174
175impl<N: PartialOrd> CoreDescriptor<N> {
176	/// Creates a new CoreDescriptor (for tests).
177	#[cfg(test)]
178	pub(super) fn new(
179		queue: Option<QueueDescriptor<N>>,
180		current_work: Option<WorkState<N>>,
181	) -> Self {
182		Self { queue, current_work }
183	}
184
185	/// Any work currently on that core?
186	///
187	/// Params: until - until (exclusive) which block number we are interested.
188	fn has_assignments_until(&self, until: N) -> bool {
189		self.current_work.is_some() || self.queue.as_ref().map_or(false, |q| q.first < until)
190	}
191
192	/// Accessor for queue (needed by migrations, tests, and try-runtime).
193	pub fn queue(&self) -> Option<&QueueDescriptor<N>> {
194		self.queue.as_ref()
195	}
196
197	/// Accessor for current_work (needed by migrations, tests, and try-runtime).
198	pub fn current_work(&self) -> Option<&WorkState<N>> {
199		self.current_work.as_ref()
200	}
201}
202
203/// Pointers into `CoreSchedules` for a particular core.
204///
205/// Schedules in `CoreSchedules` form a queue. `Schedule::next_schedule` always pointing to the next
206/// item.
207#[derive(Encode, Decode, TypeInfo, Copy, Clone)]
208#[cfg_attr(test, derive(PartialEq, Debug))]
209pub struct QueueDescriptor<N> {
210	/// First scheduled item, that is not yet active.
211	pub first: N,
212	/// Last scheduled item.
213	pub last: N,
214}
215
216#[derive(Encode, Decode, TypeInfo)]
217#[cfg_attr(test, derive(PartialEq, Debug, Clone))]
218pub struct WorkState<N> {
219	/// Assignments with current state.
220	///
221	/// Assignments and book keeping on how much has been served already. We keep track of serviced
222	/// assignments in order to adhere to the specified ratios.
223	pub assignments: Vec<(CoreAssignment, AssignmentState)>,
224	/// When do our assignments become invalid if at all?
225	///
226	/// If this is `Some`, then this `CoreState` will be dropped at that block number. If this is
227	/// `None`, then we will keep serving our core assignments in a circle until a new set of
228	/// assignments is scheduled.
229	pub end_hint: Option<N>,
230	/// Position in the assignments we are currently in.
231	///
232	/// Aka which core assignment will be popped next on
233	/// `AssignmentProvider::advance_assignments`.
234	pub pos: u16,
235	/// Step width
236	///
237	/// How much we subtract from `AssignmentState::remaining` for a core served.
238	pub step: PartsOf57600,
239}
240
241#[derive(Encode, Decode, TypeInfo)]
242#[cfg_attr(test, derive(PartialEq, Debug, Clone, Copy))]
243pub struct AssignmentState {
244	/// Ratio of the core this assignment has.
245	///
246	/// As initially received via `assign_core`.
247	pub ratio: PartsOf57600,
248	/// How many parts are remaining in this round?
249	///
250	/// At the end of each round (in preparation for the next), ratio will be added to remaining.
251	/// Then every time we get scheduled we subtract a core worth of points. Once we reach 0 or a
252	/// number lower than what a core is worth (`CoreState::step` size), we move on to the next
253	/// item in the `Vec`.
254	///
255	/// The first round starts with remaining = ratio.
256	pub remaining: PartsOf57600,
257}
258
259/// How storage is accessed.
260enum AccessMode<'a, T: Config> {
261	/// We only want to peek (no side effects).
262	Peek { on_demand_orders: &'a mut on_demand::OrderQueue<BlockNumberFor<T>> },
263	/// We need to update state.
264	Pop,
265}
266
267impl<'a, T: Config> AccessMode<'a, T> {
268	/// Construct a peeking access mode.
269	fn peek(on_demand_orders: &'a mut on_demand::OrderQueue<BlockNumberFor<T>>) -> Self {
270		Self::Peek { on_demand_orders }
271	}
272
273	/// Construct popping/modifying access mode.
274	fn pop() -> Self {
275		Self::Pop
276	}
277
278	/// Pop pool assignments according to access mode.
279	fn pop_assignment_for_ondemand_cores(
280		&mut self,
281		now: BlockNumberFor<T>,
282		num_cores: u32,
283	) -> impl Iterator<Item = ParaId> {
284		match self {
285			Self::Peek { on_demand_orders } => on_demand_orders
286				.pop_assignment_for_cores::<T>(now, num_cores)
287				.collect::<Vec<_>>(),
288			Self::Pop => {
289				on_demand::Pallet::<T>::pop_assignment_for_cores(now, num_cores).collect::<Vec<_>>()
290			},
291		}
292		.into_iter()
293	}
294
295	/// Get core schedule according to access mode (either take or get).
296	fn get_core_schedule(
297		&self,
298		next_scheduled: BlockNumberFor<T>,
299		core_idx: CoreIndex,
300	) -> Option<Schedule<BlockNumberFor<T>>> {
301		match self {
302			Self::Peek { .. } => super::CoreSchedules::<T>::get((next_scheduled, core_idx)),
303			Self::Pop => super::CoreSchedules::<T>::take((next_scheduled, core_idx)),
304		}
305	}
306}
307
308/// Assignments that got advanced.
309struct AdvancedAssignments {
310	bulk_assignments: Vec<(CoreIndex, ParaId)>,
311	pool_assignments: Vec<(CoreIndex, ParaId)>,
312}
313
314impl AdvancedAssignments {
315	fn into_iter(self) -> impl Iterator<Item = (CoreIndex, ParaId)> {
316		let Self { bulk_assignments, pool_assignments } = self;
317		bulk_assignments.into_iter().chain(pool_assignments.into_iter())
318	}
319}
320
321impl<N> From<Schedule<N>> for WorkState<N> {
322	fn from(schedule: Schedule<N>) -> Self {
323		let Schedule { assignments, end_hint, next_schedule: _ } = schedule;
324		let step =
325			if let Some(min_step_assignment) = assignments.iter().min_by(|a, b| a.1.cmp(&b.1)) {
326				min_step_assignment.1
327			} else {
328				// Assignments empty, should not exist. In any case step size does not matter here:
329				log::debug!("assignments of a `Schedule` should never be empty.");
330				PartsOf57600(1)
331			};
332		let assignments = assignments
333			.into_iter()
334			.map(|(a, ratio)| (a, AssignmentState { ratio, remaining: ratio }))
335			.collect();
336
337		Self { assignments, end_hint, pos: 0, step }
338	}
339}
340
341#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
342pub enum Error {
343	AssignmentsEmpty,
344	/// assign_core is only allowed to append new assignments at the end of already existing
345	/// ones or update the last entry.
346	DisallowedInsert,
347}
348
349/// Peek `num_entries` into the future.
350///
351/// First element for each `CoreIndex` will tell what would be retrieved when
352/// `advance_assignments` is called at the next block. The second what one would get in the
353/// block after the next block and so forth.
354///
355/// The predictions are accurate in the sense that if an assignment `B` was predicted, it will
356/// never happen that `advance_assignments` at that block will retrieve an assignment `A`.
357/// What can happen though is that the prediction is empty (returned vec does not contain that
358/// element), but `advance_assignments` at that block will then return something regardless.
359///
360/// Invariants:
361///
362/// - `advance_assignments` must be called for each core each block
363/// exactly once for the prediction offered by `peek_next_block` to stay accurate.
364/// - This function is meant to be called from a runtime API and thus uses the state of the
365/// block after the current one to show an accurate prediction of upcoming schedules.
366pub(super) fn peek_next_block<T: super::Config>(
367	num_entries: u32,
368) -> BTreeMap<CoreIndex, VecDeque<ParaId>> {
369	let now = frame_system::Pallet::<T>::block_number().saturating_plus_one();
370	peek_impl::<T>(now, num_entries)
371}
372
373/// Advance assignments.
374///
375/// We move forward one step with the assignments on each core.
376///
377/// Parameters:
378///
379/// - blocked: Lambda, for each core it returns true, the assignment could not actually be
380/// served.
381///
382/// Returns: Advanced assignments. Blocked cores will still be advanced, but will not be
383/// contained in the output.
384pub(super) fn advance_assignments<T: Config, F: Fn(CoreIndex) -> bool>(
385	is_blocked: F,
386) -> BTreeMap<CoreIndex, ParaId> {
387	let now = frame_system::Pallet::<T>::block_number();
388
389	let assignments = super::CoreDescriptors::<T>::mutate(|core_states| {
390		advance_assignments_single_impl::<T>(now, core_states, AccessMode::<T>::pop())
391	});
392
393	// Give blocked on-demand orders another chance:
394	for blocked in assignments.pool_assignments.iter().filter_map(|(core_idx, para_id)| {
395		if is_blocked(*core_idx) {
396			Some(*para_id)
397		} else {
398			None
399		}
400	}) {
401		on_demand::Pallet::<T>::push_back_order(blocked);
402	}
403
404	let mut assignments: BTreeMap<CoreIndex, ParaId> =
405		assignments.into_iter().filter(|(core_idx, _)| !is_blocked(*core_idx)).collect();
406
407	// Try to fill missing assignments from the next position (duplication to allow asynchronous
408	// backing even for first assignment coming in on a previously empty core):
409	let next = now.saturating_plus_one();
410	let mut core_states = super::CoreDescriptors::<T>::get();
411	let mut on_demand_orders = on_demand::Pallet::<T>::peek_order_queue();
412	let next_assignments = advance_assignments_single_impl(
413		next,
414		&mut core_states,
415		AccessMode::<T>::peek(&mut on_demand_orders),
416	)
417	.into_iter();
418
419	for (core_idx, next_assignment) in
420		next_assignments.filter(|(core_idx, _)| !is_blocked(*core_idx))
421	{
422		assignments.entry(core_idx).or_insert_with(|| next_assignment);
423	}
424	assignments
425}
426
427/// Append another assignment for a core.
428///
429/// Important: Only appending is allowed or insertion into the last item. Meaning,
430/// all already existing assignments must have a `begin` smaller or equal than the one passed
431/// here.
432/// Updating the last entry is supported to allow for making a core assignment multiple calls to
433/// assign_core. Thus if you have too much interlacing for e.g. a single UMP message you can
434/// split that up into multiple messages, each triggering a call to `assign_core`, together
435/// forming the total assignment.
436///
437/// Inserting arbitrarily causes a `DispatchError::DisallowedInsert` error.
438///
439/// Inserting too early (changing assignments within the lookahead depth), will
440/// get the begin auto-adjusted to maintain the stable claim queue invariant, if
441/// there existed assignments before.
442// With the restriction of only allowing for appends this function allows for
443// O(1) complexity. It could easily be lifted, if need be and in fact an
444// implementation is available
445// [here](https://github.com/paritytech/polkadot-sdk/pull/1694/commits/c0c23b01fd2830910cde92c11960dad12cdff398#diff-0c85a46e448de79a5452395829986ee8747e17a857c27ab624304987d2dde8baR386).
446// The problem is that insertion complexity then depends on the size of the existing queue,
447// which makes determining weights hard and could lead to issues like overweight blocks (at
448// least in theory).
449pub(super) fn assign_core<T: Config>(
450	core_idx: CoreIndex,
451	mut begin: BlockNumberFor<T>,
452	mut assignments: Vec<(CoreAssignment, PartsOf57600)>,
453	end_hint: Option<BlockNumberFor<T>>,
454) -> Result<(), Error> {
455	// There should be at least one assignment.
456	ensure!(!assignments.is_empty(), Error::AssignmentsEmpty);
457
458	super::CoreDescriptors::<T>::mutate(|core_descriptors| {
459		let core_descriptor = core_descriptors.entry(core_idx).or_default();
460
461		let config = configuration::ActiveConfig::<T>::get();
462		// Plus 1 because claim queue is always 1 block ahead:
463		let now = frame_system::Pallet::<T>::block_number().saturating_plus_one();
464		let lookahead = config.scheduler_params.lookahead.into();
465		let claim_queue_end = now.saturating_add(lookahead);
466		let assignments_exist = core_descriptor.has_assignments_until(claim_queue_end);
467		// Maintain invariant of stable claim queue (existing visible assignments not getting
468		// replaced):
469		if assignments_exist && begin < claim_queue_end {
470			log::debug!(
471				target: "runtime::parachains::assigner-coretime",
472				"Claim queue needs to be stable, schedule change within claim queue length is not supported. Adjusting begin from {:?} to {:?}",
473				begin,
474				claim_queue_end
475			);
476			begin = claim_queue_end;
477		}
478
479		let new_queue = match core_descriptor.queue {
480			Some(queue) => {
481				ensure!(begin >= queue.last, Error::DisallowedInsert);
482
483				// Update queue if we are appending:
484				if begin > queue.last {
485					super::CoreSchedules::<T>::mutate((queue.last, core_idx), |schedule| {
486						if let Some(schedule) = schedule.as_mut() {
487							debug_assert!(schedule.next_schedule.is_none(), "queue.end was supposed to be the end, so the next item must be `None`!");
488							schedule.next_schedule = Some(begin);
489						} else {
490							defensive!("Queue end entry does not exist?");
491						}
492					});
493				}
494
495				super::CoreSchedules::<T>::mutate((begin, core_idx), |schedule| {
496					let assignments = if let Some(mut old_schedule) = schedule.take() {
497						old_schedule.assignments.append(&mut assignments);
498						old_schedule.assignments
499					} else {
500						assignments
501					};
502					*schedule = Some(Schedule { assignments, end_hint, next_schedule: None });
503				});
504
505				QueueDescriptor { first: queue.first, last: begin }
506			},
507			None => {
508				// Queue empty, just insert:
509				super::CoreSchedules::<T>::insert(
510					(begin, core_idx),
511					Schedule { assignments, end_hint, next_schedule: None },
512				);
513				QueueDescriptor { first: begin, last: begin }
514			},
515		};
516		core_descriptor.queue = Some(new_queue);
517		Ok(())
518	})
519}
520
521fn num_coretime_cores<T: Config>() -> u32 {
522	configuration::ActiveConfig::<T>::get().scheduler_params.num_cores
523}
524
525fn peek_impl<T: Config>(
526	mut now: BlockNumberFor<T>,
527	num_entries: u32,
528) -> BTreeMap<CoreIndex, VecDeque<ParaId>> {
529	let mut core_states = super::CoreDescriptors::<T>::get();
530	let mut result = BTreeMap::new();
531	let mut on_demand_orders = on_demand::Pallet::<T>::peek_order_queue();
532	for i in 0..num_entries {
533		let assignments = advance_assignments_single_impl(
534			now,
535			&mut core_states,
536			AccessMode::<T>::peek(&mut on_demand_orders),
537		)
538		.into_iter();
539		for (core_idx, para_id) in assignments {
540			let claim_queue: &mut VecDeque<ParaId> = result.entry(core_idx).or_default();
541			// Stop filling on holes, otherwise we get claims at the wrong positions.
542			if claim_queue.len() == i as usize {
543				claim_queue.push_back(para_id)
544			} else if claim_queue.len() == 0 && i == 1 {
545				// Except for position 1: Claim queue was empty before. We now have an incoming
546				// assignment on position 1: Duplicate it to position 0 so the chain will
547				// get a full asynchronous backing opportunity (and a bonus synchronous
548				// backing opportunity).
549				claim_queue.push_back(para_id);
550				// And fill position 1:
551				claim_queue.push_back(para_id);
552			}
553		}
554		now.saturating_inc();
555	}
556	result
557}
558
559/// Pop assignments for `now`.
560fn advance_assignments_single_impl<T: Config>(
561	now: BlockNumberFor<T>,
562	core_states: &mut BTreeMap<CoreIndex, CoreDescriptor<BlockNumberFor<T>>>,
563	mut mode: AccessMode<T>,
564) -> AdvancedAssignments {
565	let mut bulk_assignments = Vec::with_capacity(num_coretime_cores::<T>() as _);
566	let mut pool_cores = Vec::with_capacity(num_coretime_cores::<T>() as _);
567	for (core_idx, core_state) in core_states.iter_mut() {
568		ensure_workload::<T>(now, *core_idx, core_state, &mode);
569
570		let Some(work_state) = core_state.current_work.as_mut() else { continue };
571
572		// Wrap around:
573		work_state.pos = work_state.pos % work_state.assignments.len() as u16;
574		let (a_type, a_state) = &mut work_state
575			.assignments
576			.get_mut(work_state.pos as usize)
577			.expect("We limited pos to the size of the vec one line above. qed");
578
579		// advance for next pop:
580		a_state.remaining = a_state.remaining.saturating_sub(work_state.step);
581		if a_state.remaining < work_state.step {
582			// Assignment exhausted, need to move to the next and credit remaining for
583			// next round.
584			work_state.pos += 1;
585			// Reset to ratio + still remaining "credits":
586			a_state.remaining = a_state.remaining.saturating_add(a_state.ratio);
587		}
588		match *a_type {
589			CoreAssignment::Pool => pool_cores.push(*core_idx),
590			CoreAssignment::Task(para_id) => bulk_assignments.push((*core_idx, para_id.into())),
591			CoreAssignment::Idle => {},
592		}
593	}
594
595	let pool_assignments = mode.pop_assignment_for_ondemand_cores(now, pool_cores.len() as _);
596	let pool_assignments = pool_cores.into_iter().zip(pool_assignments).collect();
597
598	AdvancedAssignments { bulk_assignments, pool_assignments }
599}
600
601/// Ensure given workload for core is up to date.
602fn ensure_workload<T: Config>(
603	now: BlockNumberFor<T>,
604	core_idx: CoreIndex,
605	descriptor: &mut CoreDescriptor<BlockNumberFor<T>>,
606	mode: &AccessMode<T>,
607) {
608	// Workload expired?
609	if descriptor
610		.current_work
611		.as_ref()
612		.and_then(|w| w.end_hint)
613		.map_or(false, |e| e <= now)
614	{
615		descriptor.current_work = None;
616	}
617
618	let Some(queue) = descriptor.queue else {
619		// No queue.
620		return;
621	};
622
623	let mut next_scheduled = queue.first;
624
625	if next_scheduled > now {
626		// Not yet ready.
627		return;
628	}
629
630	// Update is needed:
631	let update = loop {
632		let Some(update) = mode.get_core_schedule(next_scheduled, core_idx) else { break None };
633
634		// Still good?
635		if update.end_hint.map_or(true, |e| e > now) {
636			break Some(update);
637		}
638		// Move on if possible:
639		if let Some(n) = update.next_schedule {
640			next_scheduled = n;
641		} else {
642			break None;
643		}
644	};
645
646	let new_first = update.as_ref().and_then(|u| u.next_schedule);
647	descriptor.current_work = update.map(Into::into);
648
649	descriptor.queue = new_first.map(|new_first| {
650		QueueDescriptor {
651			first: new_first,
652			// `last` stays unaffected, if not empty:
653			last: queue.last,
654		}
655	});
656}