pallet_scheduler/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! > Made with *Substrate*, for *Polkadot*.
19//!
20//! [![github]](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/scheduler) -
21//! [![polkadot]](https://polkadot.com)
22//!
23//! [polkadot]: https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white
24//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
25//!
26//! # Scheduler Pallet
27//!
28//! A Pallet for scheduling runtime calls.
29//!
30//! ## Overview
31//!
32//! This Pallet exposes capabilities for scheduling runtime calls to occur at a specified block
33//! number or at a specified period. These scheduled runtime calls may be named or anonymous and may
34//! be canceled.
35//!
36//! __NOTE:__ Instead of using the filter contained in the origin to call `fn schedule`, scheduled
37//! runtime calls will be dispatched with the default filter for the origin: namely
38//! `frame_system::Config::BaseCallFilter` for all origin types (except root which will get no
39//! filter).
40//!
41//! If a call is scheduled using proxy or whatever mechanism which adds filter, then those filter
42//! will not be used when dispatching the schedule runtime call.
43//!
44//! ### Examples
45//!
46//! 1. Scheduling a runtime call at a specific block.
47#![doc = docify::embed!("src/tests.rs", basic_scheduling_works)]
48//!
49//! 2. Scheduling a preimage hash of a runtime call at a specific block
50#![doc = docify::embed!("src/tests.rs", scheduling_with_preimages_works)]
51
52//!
53//! ## Pallet API
54//!
55//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
56//! including its configuration trait, dispatchables, storage items, events and errors.
57//!
58//! ## Warning
59//!
60//! This Pallet executes all scheduled runtime calls in the [`on_initialize`] hook. Do not execute
61//! any runtime calls which should not be considered mandatory.
62//!
63//! Please be aware that any scheduled runtime calls executed in a future block may __fail__ or may
64//! result in __undefined behavior__ since the runtime could have upgraded between the time of
65//! scheduling and execution. For example, the runtime upgrade could have:
66//!
67//! * Modified the implementation of the runtime call (runtime specification upgrade).
68//!     * Could lead to undefined behavior.
69//! * Removed or changed the ordering/index of the runtime call.
70//!     * Could fail due to the runtime call index not being part of the `Call`.
71//!     * Could lead to undefined behavior, such as executing another runtime call with the same
72//!       index.
73//!
74//! [`on_initialize`]: frame_support::traits::Hooks::on_initialize
75
76// Ensure we're `no_std` when compiling for Wasm.
77#![cfg_attr(not(feature = "std"), no_std)]
78
79#[cfg(feature = "runtime-benchmarks")]
80mod benchmarking;
81pub mod migration;
82#[cfg(test)]
83mod mock;
84#[cfg(test)]
85mod tests;
86pub mod weights;
87
88extern crate alloc;
89
90use alloc::{boxed::Box, vec::Vec};
91use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
92use core::{borrow::Borrow, cmp::Ordering, marker::PhantomData};
93use frame_support::{
94	dispatch::{DispatchResult, GetDispatchInfo, Parameter, RawOrigin},
95	ensure,
96	traits::{
97		schedule::{self, DispatchTime, MaybeHashed},
98		Bounded, CallerTrait, EnsureOrigin, Get, IsType, OriginTrait, PalletInfoAccess,
99		PrivilegeCmp, QueryPreimage, StorageVersion, StorePreimage,
100	},
101	weights::{Weight, WeightMeter},
102};
103use frame_system::{self as system};
104use scale_info::TypeInfo;
105use sp_io::hashing::blake2_256;
106use sp_runtime::{
107	traits::{BadOrigin, BlockNumberProvider, Dispatchable, One, Saturating, Zero},
108	BoundedVec, DispatchError, RuntimeDebug,
109};
110
111pub use pallet::*;
112pub use weights::WeightInfo;
113
114/// Just a simple index for naming period tasks.
115pub type PeriodicIndex = u32;
116/// The location of a scheduled task that can be used to remove it.
117pub type TaskAddress<BlockNumber> = (BlockNumber, u32);
118
119pub type CallOrHashOf<T> =
120	MaybeHashed<<T as Config>::RuntimeCall, <T as frame_system::Config>::Hash>;
121
122pub type BoundedCallOf<T> =
123	Bounded<<T as Config>::RuntimeCall, <T as frame_system::Config>::Hashing>;
124
125pub type BlockNumberFor<T> =
126	<<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
127
128/// The configuration of the retry mechanism for a given task along with its current state.
129#[derive(
130	Clone,
131	Copy,
132	RuntimeDebug,
133	PartialEq,
134	Eq,
135	Encode,
136	Decode,
137	DecodeWithMemTracking,
138	MaxEncodedLen,
139	TypeInfo,
140)]
141pub struct RetryConfig<Period> {
142	/// Initial amount of retries allowed.
143	pub total_retries: u8,
144	/// Amount of retries left.
145	pub remaining: u8,
146	/// Period of time between retry attempts.
147	pub period: Period,
148}
149
150#[cfg_attr(any(feature = "std", test), derive(PartialEq, Eq))]
151#[derive(Clone, RuntimeDebug, Encode, Decode)]
152struct ScheduledV1<Call, BlockNumber> {
153	maybe_id: Option<Vec<u8>>,
154	priority: schedule::Priority,
155	call: Call,
156	maybe_periodic: Option<schedule::Period<BlockNumber>>,
157}
158
159/// Information regarding an item to be executed in the future.
160#[derive(
161	Clone,
162	RuntimeDebug,
163	PartialEq,
164	Eq,
165	Encode,
166	Decode,
167	MaxEncodedLen,
168	TypeInfo,
169	DecodeWithMemTracking,
170)]
171pub struct Scheduled<Name, Call, BlockNumber, PalletsOrigin, AccountId> {
172	/// The unique identity for this task, if there is one.
173	pub maybe_id: Option<Name>,
174	/// This task's priority.
175	pub priority: schedule::Priority,
176	/// The call to be dispatched.
177	pub call: Call,
178	/// If the call is periodic, then this points to the information concerning that.
179	pub maybe_periodic: Option<schedule::Period<BlockNumber>>,
180	/// The origin with which to dispatch the call.
181	pub origin: PalletsOrigin,
182	#[doc(hidden)]
183	pub _phantom: PhantomData<AccountId>,
184}
185
186impl<Name, Call, BlockNumber, PalletsOrigin, AccountId>
187	Scheduled<Name, Call, BlockNumber, PalletsOrigin, AccountId>
188where
189	Call: Clone,
190	PalletsOrigin: Clone,
191{
192	/// Create a new task to be used for retry attempts of the original one. The cloned task will
193	/// have the same `priority`, `call` and `origin`, but will always be non-periodic and unnamed.
194	pub fn as_retry(&self) -> Self {
195		Self {
196			maybe_id: None,
197			priority: self.priority,
198			call: self.call.clone(),
199			maybe_periodic: None,
200			origin: self.origin.clone(),
201			_phantom: Default::default(),
202		}
203	}
204}
205
206use crate::{Scheduled as ScheduledV3, Scheduled as ScheduledV2};
207
208pub type ScheduledV2Of<T> = ScheduledV2<
209	Vec<u8>,
210	<T as Config>::RuntimeCall,
211	BlockNumberFor<T>,
212	<T as Config>::PalletsOrigin,
213	<T as frame_system::Config>::AccountId,
214>;
215
216pub type ScheduledV3Of<T> = ScheduledV3<
217	Vec<u8>,
218	CallOrHashOf<T>,
219	BlockNumberFor<T>,
220	<T as Config>::PalletsOrigin,
221	<T as frame_system::Config>::AccountId,
222>;
223
224pub type ScheduledOf<T> = Scheduled<
225	TaskName,
226	BoundedCallOf<T>,
227	BlockNumberFor<T>,
228	<T as Config>::PalletsOrigin,
229	<T as frame_system::Config>::AccountId,
230>;
231
232pub(crate) trait MarginalWeightInfo: WeightInfo {
233	fn service_task(maybe_lookup_len: Option<usize>, named: bool, periodic: bool) -> Weight {
234		let base = Self::service_task_base();
235		let mut total = match maybe_lookup_len {
236			None => base,
237			Some(l) => Self::service_task_fetched(l as u32),
238		};
239		if named {
240			total.saturating_accrue(Self::service_task_named().saturating_sub(base));
241		}
242		if periodic {
243			total.saturating_accrue(Self::service_task_periodic().saturating_sub(base));
244		}
245		total
246	}
247}
248impl<T: WeightInfo> MarginalWeightInfo for T {}
249
250#[frame_support::pallet]
251pub mod pallet {
252	use super::*;
253	use frame_support::{dispatch::PostDispatchInfo, pallet_prelude::*};
254	use frame_system::pallet_prelude::{BlockNumberFor as SystemBlockNumberFor, OriginFor};
255
256	/// The in-code storage version.
257	const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
258
259	#[pallet::pallet]
260	#[pallet::storage_version(STORAGE_VERSION)]
261	pub struct Pallet<T>(_);
262
263	/// `system::Config` should always be included in our implied traits.
264	#[pallet::config]
265	pub trait Config: frame_system::Config {
266		/// The overarching event type.
267		#[allow(deprecated)]
268		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
269
270		/// The aggregated origin which the dispatch will take.
271		type RuntimeOrigin: OriginTrait<PalletsOrigin = Self::PalletsOrigin>
272			+ From<Self::PalletsOrigin>
273			+ IsType<<Self as system::Config>::RuntimeOrigin>;
274
275		/// The caller origin, overarching type of all pallets origins.
276		type PalletsOrigin: From<system::RawOrigin<Self::AccountId>>
277			+ CallerTrait<Self::AccountId>
278			+ MaxEncodedLen;
279
280		/// The aggregated call type.
281		type RuntimeCall: Parameter
282			+ Dispatchable<
283				RuntimeOrigin = <Self as Config>::RuntimeOrigin,
284				PostInfo = PostDispatchInfo,
285			> + GetDispatchInfo
286			+ From<system::Call<Self>>;
287
288		/// The maximum weight that may be scheduled per block for any dispatchables.
289		#[pallet::constant]
290		type MaximumWeight: Get<Weight>;
291
292		/// Required origin to schedule or cancel calls.
293		type ScheduleOrigin: EnsureOrigin<<Self as system::Config>::RuntimeOrigin>;
294
295		/// Compare the privileges of origins.
296		///
297		/// This will be used when canceling a task, to ensure that the origin that tries
298		/// to cancel has greater or equal privileges as the origin that created the scheduled task.
299		///
300		/// For simplicity the [`EqualPrivilegeOnly`](frame_support::traits::EqualPrivilegeOnly) can
301		/// be used. This will only check if two given origins are equal.
302		type OriginPrivilegeCmp: PrivilegeCmp<Self::PalletsOrigin>;
303
304		/// The maximum number of scheduled calls in the queue for a single block.
305		///
306		/// NOTE:
307		/// + Dependent pallets' benchmarks might require a higher limit for the setting. Set a
308		/// higher limit under `runtime-benchmarks` feature.
309		#[pallet::constant]
310		type MaxScheduledPerBlock: Get<u32>;
311
312		/// Weight information for extrinsics in this pallet.
313		type WeightInfo: WeightInfo;
314
315		/// The preimage provider with which we look up call hashes to get the call.
316		type Preimages: QueryPreimage<H = Self::Hashing> + StorePreimage;
317
318		/// Query the current block number.
319		///
320		/// Must return monotonically increasing values when called from consecutive blocks. It is
321		/// generally expected that the values also do not differ "too much" between consecutive
322		/// blocks. A future addition to this pallet will allow bigger difference between
323		/// consecutive blocks to make it possible to be utilized by parachains with *Agile
324		/// Coretime*. *Agile Coretime* parachains are currently not supported and must continue to
325		/// use their local block number provider.
326		///
327		/// Can be configured to return either:
328		/// - the local block number of the runtime via `frame_system::Pallet`
329		/// - a remote block number, eg from the relay chain through `RelaychainDataProvider`
330		/// - an arbitrary value through a custom implementation of the trait
331		///
332		/// Suggested values:
333		/// - Solo- and Relay-chains should use `frame_system::Pallet`. There are no concerns with
334		///   this configuration.
335		/// - Parachains should also use `frame_system::Pallet` for the time being. The scheduler
336		///   pallet is not yet ready for the case that big numbers of blocks are skipped. In an
337		///   *Agile Coretime* chain with relay chain number provider configured, it could otherwise
338		///   happen that the scheduler will not be able to catch up to its agendas, since too many
339		///   relay blocks are missing if the parachain only produces blocks rarely.
340		///
341		/// There is currently no migration provided to "hot-swap" block number providers and it is
342		/// therefore highly advised to stay with the default (local) values. If you still want to
343		/// swap block number providers on the fly, then please at least ensure that you do not run
344		/// any pallet migration in the same runtime upgrade.
345		type BlockNumberProvider: BlockNumberProvider;
346	}
347
348	/// Block number at which the agenda began incomplete execution.
349	#[pallet::storage]
350	pub type IncompleteSince<T: Config> = StorageValue<_, BlockNumberFor<T>>;
351
352	/// Items to be executed, indexed by the block number that they should be executed on.
353	#[pallet::storage]
354	pub type Agenda<T: Config> = StorageMap<
355		_,
356		Twox64Concat,
357		BlockNumberFor<T>,
358		BoundedVec<Option<ScheduledOf<T>>, T::MaxScheduledPerBlock>,
359		ValueQuery,
360	>;
361
362	/// Retry configurations for items to be executed, indexed by task address.
363	#[pallet::storage]
364	pub type Retries<T: Config> = StorageMap<
365		_,
366		Blake2_128Concat,
367		TaskAddress<BlockNumberFor<T>>,
368		RetryConfig<BlockNumberFor<T>>,
369		OptionQuery,
370	>;
371
372	/// Lookup from a name to the block number and index of the task.
373	///
374	/// For v3 -> v4 the previously unbounded identities are Blake2-256 hashed to form the v4
375	/// identities.
376	#[pallet::storage]
377	pub type Lookup<T: Config> =
378		StorageMap<_, Twox64Concat, TaskName, TaskAddress<BlockNumberFor<T>>>;
379
380	/// Events type.
381	#[pallet::event]
382	#[pallet::generate_deposit(pub(super) fn deposit_event)]
383	pub enum Event<T: Config> {
384		/// Scheduled some task.
385		Scheduled { when: BlockNumberFor<T>, index: u32 },
386		/// Canceled some task.
387		Canceled { when: BlockNumberFor<T>, index: u32 },
388		/// Dispatched some task.
389		Dispatched {
390			task: TaskAddress<BlockNumberFor<T>>,
391			id: Option<TaskName>,
392			result: DispatchResult,
393		},
394		/// Set a retry configuration for some task.
395		RetrySet {
396			task: TaskAddress<BlockNumberFor<T>>,
397			id: Option<TaskName>,
398			period: BlockNumberFor<T>,
399			retries: u8,
400		},
401		/// Cancel a retry configuration for some task.
402		RetryCancelled { task: TaskAddress<BlockNumberFor<T>>, id: Option<TaskName> },
403		/// The call for the provided hash was not found so the task has been aborted.
404		CallUnavailable { task: TaskAddress<BlockNumberFor<T>>, id: Option<TaskName> },
405		/// The given task was unable to be renewed since the agenda is full at that block.
406		PeriodicFailed { task: TaskAddress<BlockNumberFor<T>>, id: Option<TaskName> },
407		/// The given task was unable to be retried since the agenda is full at that block or there
408		/// was not enough weight to reschedule it.
409		RetryFailed { task: TaskAddress<BlockNumberFor<T>>, id: Option<TaskName> },
410		/// The given task can never be executed since it is overweight.
411		PermanentlyOverweight { task: TaskAddress<BlockNumberFor<T>>, id: Option<TaskName> },
412		/// Agenda is incomplete from `when`.
413		AgendaIncomplete { when: BlockNumberFor<T> },
414	}
415
416	#[pallet::error]
417	pub enum Error<T> {
418		/// Failed to schedule a call
419		FailedToSchedule,
420		/// Cannot find the scheduled call.
421		NotFound,
422		/// Given target block number is in the past.
423		TargetBlockNumberInPast,
424		/// Reschedule failed because it does not change scheduled time.
425		RescheduleNoChange,
426		/// Attempt to use a non-named function on a named task.
427		Named,
428	}
429
430	#[pallet::hooks]
431	impl<T: Config> Hooks<SystemBlockNumberFor<T>> for Pallet<T> {
432		/// Execute the scheduled calls
433		fn on_initialize(_now: SystemBlockNumberFor<T>) -> Weight {
434			let now = T::BlockNumberProvider::current_block_number();
435			let mut weight_counter = frame_system::Pallet::<T>::remaining_block_weight()
436				.limit_to(T::MaximumWeight::get());
437			Self::service_agendas(&mut weight_counter, now, u32::MAX);
438			weight_counter.consumed()
439		}
440
441		#[cfg(feature = "std")]
442		fn integrity_test() {
443			/// Calculate the maximum weight that a lookup of a given size can take.
444			fn lookup_weight<T: Config>(s: usize) -> Weight {
445				T::WeightInfo::service_agendas_base() +
446					T::WeightInfo::service_agenda_base(T::MaxScheduledPerBlock::get()) +
447					T::WeightInfo::service_task(Some(s), true, true)
448			}
449
450			let limit = sp_runtime::Perbill::from_percent(90) * T::MaximumWeight::get();
451
452			let small_lookup = lookup_weight::<T>(128);
453			assert!(small_lookup.all_lte(limit), "Must be possible to submit a small lookup");
454
455			let medium_lookup = lookup_weight::<T>(1024);
456			assert!(medium_lookup.all_lte(limit), "Must be possible to submit a medium lookup");
457
458			let large_lookup = lookup_weight::<T>(1024 * 1024);
459			assert!(large_lookup.all_lte(limit), "Must be possible to submit a large lookup");
460		}
461	}
462
463	#[pallet::call]
464	impl<T: Config> Pallet<T> {
465		/// Anonymously schedule a task.
466		#[pallet::call_index(0)]
467		#[pallet::weight(<T as Config>::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))]
468		pub fn schedule(
469			origin: OriginFor<T>,
470			when: BlockNumberFor<T>,
471			maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
472			priority: schedule::Priority,
473			call: Box<<T as Config>::RuntimeCall>,
474		) -> DispatchResult {
475			T::ScheduleOrigin::ensure_origin(origin.clone())?;
476			let origin = <T as Config>::RuntimeOrigin::from(origin);
477			Self::do_schedule(
478				DispatchTime::At(when),
479				maybe_periodic,
480				priority,
481				origin.caller().clone(),
482				T::Preimages::bound(*call)?,
483			)?;
484			Ok(())
485		}
486
487		/// Cancel an anonymously scheduled task.
488		#[pallet::call_index(1)]
489		#[pallet::weight(<T as Config>::WeightInfo::cancel(T::MaxScheduledPerBlock::get()))]
490		pub fn cancel(origin: OriginFor<T>, when: BlockNumberFor<T>, index: u32) -> DispatchResult {
491			T::ScheduleOrigin::ensure_origin(origin.clone())?;
492			let origin = <T as Config>::RuntimeOrigin::from(origin);
493			Self::do_cancel(Some(origin.caller().clone()), (when, index))?;
494			Ok(())
495		}
496
497		/// Schedule a named task.
498		#[pallet::call_index(2)]
499		#[pallet::weight(<T as Config>::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))]
500		pub fn schedule_named(
501			origin: OriginFor<T>,
502			id: TaskName,
503			when: BlockNumberFor<T>,
504			maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
505			priority: schedule::Priority,
506			call: Box<<T as Config>::RuntimeCall>,
507		) -> DispatchResult {
508			T::ScheduleOrigin::ensure_origin(origin.clone())?;
509			let origin = <T as Config>::RuntimeOrigin::from(origin);
510			Self::do_schedule_named(
511				id,
512				DispatchTime::At(when),
513				maybe_periodic,
514				priority,
515				origin.caller().clone(),
516				T::Preimages::bound(*call)?,
517			)?;
518			Ok(())
519		}
520
521		/// Cancel a named scheduled task.
522		#[pallet::call_index(3)]
523		#[pallet::weight(<T as Config>::WeightInfo::cancel_named(T::MaxScheduledPerBlock::get()))]
524		pub fn cancel_named(origin: OriginFor<T>, id: TaskName) -> DispatchResult {
525			T::ScheduleOrigin::ensure_origin(origin.clone())?;
526			let origin = <T as Config>::RuntimeOrigin::from(origin);
527			Self::do_cancel_named(Some(origin.caller().clone()), id)?;
528			Ok(())
529		}
530
531		/// Anonymously schedule a task after a delay.
532		#[pallet::call_index(4)]
533		#[pallet::weight(<T as Config>::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))]
534		pub fn schedule_after(
535			origin: OriginFor<T>,
536			after: BlockNumberFor<T>,
537			maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
538			priority: schedule::Priority,
539			call: Box<<T as Config>::RuntimeCall>,
540		) -> DispatchResult {
541			T::ScheduleOrigin::ensure_origin(origin.clone())?;
542			let origin = <T as Config>::RuntimeOrigin::from(origin);
543			Self::do_schedule(
544				DispatchTime::After(after),
545				maybe_periodic,
546				priority,
547				origin.caller().clone(),
548				T::Preimages::bound(*call)?,
549			)?;
550			Ok(())
551		}
552
553		/// Schedule a named task after a delay.
554		#[pallet::call_index(5)]
555		#[pallet::weight(<T as Config>::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))]
556		pub fn schedule_named_after(
557			origin: OriginFor<T>,
558			id: TaskName,
559			after: BlockNumberFor<T>,
560			maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
561			priority: schedule::Priority,
562			call: Box<<T as Config>::RuntimeCall>,
563		) -> DispatchResult {
564			T::ScheduleOrigin::ensure_origin(origin.clone())?;
565			let origin = <T as Config>::RuntimeOrigin::from(origin);
566			Self::do_schedule_named(
567				id,
568				DispatchTime::After(after),
569				maybe_periodic,
570				priority,
571				origin.caller().clone(),
572				T::Preimages::bound(*call)?,
573			)?;
574			Ok(())
575		}
576
577		/// Set a retry configuration for a task so that, in case its scheduled run fails, it will
578		/// be retried after `period` blocks, for a total amount of `retries` retries or until it
579		/// succeeds.
580		///
581		/// Tasks which need to be scheduled for a retry are still subject to weight metering and
582		/// agenda space, same as a regular task. If a periodic task fails, it will be scheduled
583		/// normally while the task is retrying.
584		///
585		/// Tasks scheduled as a result of a retry for a periodic task are unnamed, non-periodic
586		/// clones of the original task. Their retry configuration will be derived from the
587		/// original task's configuration, but will have a lower value for `remaining` than the
588		/// original `total_retries`.
589		#[pallet::call_index(6)]
590		#[pallet::weight(<T as Config>::WeightInfo::set_retry())]
591		pub fn set_retry(
592			origin: OriginFor<T>,
593			task: TaskAddress<BlockNumberFor<T>>,
594			retries: u8,
595			period: BlockNumberFor<T>,
596		) -> DispatchResult {
597			T::ScheduleOrigin::ensure_origin(origin.clone())?;
598			let origin = <T as Config>::RuntimeOrigin::from(origin);
599			let (when, index) = task;
600			let agenda = Agenda::<T>::get(when);
601			let scheduled = agenda
602				.get(index as usize)
603				.and_then(Option::as_ref)
604				.ok_or(Error::<T>::NotFound)?;
605			Self::ensure_privilege(origin.caller(), &scheduled.origin)?;
606			Retries::<T>::insert(
607				(when, index),
608				RetryConfig { total_retries: retries, remaining: retries, period },
609			);
610			Self::deposit_event(Event::RetrySet { task, id: None, period, retries });
611			Ok(())
612		}
613
614		/// Set a retry configuration for a named task so that, in case its scheduled run fails, it
615		/// will be retried after `period` blocks, for a total amount of `retries` retries or until
616		/// it succeeds.
617		///
618		/// Tasks which need to be scheduled for a retry are still subject to weight metering and
619		/// agenda space, same as a regular task. If a periodic task fails, it will be scheduled
620		/// normally while the task is retrying.
621		///
622		/// Tasks scheduled as a result of a retry for a periodic task are unnamed, non-periodic
623		/// clones of the original task. Their retry configuration will be derived from the
624		/// original task's configuration, but will have a lower value for `remaining` than the
625		/// original `total_retries`.
626		#[pallet::call_index(7)]
627		#[pallet::weight(<T as Config>::WeightInfo::set_retry_named())]
628		pub fn set_retry_named(
629			origin: OriginFor<T>,
630			id: TaskName,
631			retries: u8,
632			period: BlockNumberFor<T>,
633		) -> DispatchResult {
634			T::ScheduleOrigin::ensure_origin(origin.clone())?;
635			let origin = <T as Config>::RuntimeOrigin::from(origin);
636			let (when, agenda_index) = Lookup::<T>::get(&id).ok_or(Error::<T>::NotFound)?;
637			let agenda = Agenda::<T>::get(when);
638			let scheduled = agenda
639				.get(agenda_index as usize)
640				.and_then(Option::as_ref)
641				.ok_or(Error::<T>::NotFound)?;
642			Self::ensure_privilege(origin.caller(), &scheduled.origin)?;
643			Retries::<T>::insert(
644				(when, agenda_index),
645				RetryConfig { total_retries: retries, remaining: retries, period },
646			);
647			Self::deposit_event(Event::RetrySet {
648				task: (when, agenda_index),
649				id: Some(id),
650				period,
651				retries,
652			});
653			Ok(())
654		}
655
656		/// Removes the retry configuration of a task.
657		#[pallet::call_index(8)]
658		#[pallet::weight(<T as Config>::WeightInfo::cancel_retry())]
659		pub fn cancel_retry(
660			origin: OriginFor<T>,
661			task: TaskAddress<BlockNumberFor<T>>,
662		) -> DispatchResult {
663			T::ScheduleOrigin::ensure_origin(origin.clone())?;
664			let origin = <T as Config>::RuntimeOrigin::from(origin);
665			Self::do_cancel_retry(origin.caller(), task)?;
666			Self::deposit_event(Event::RetryCancelled { task, id: None });
667			Ok(())
668		}
669
670		/// Cancel the retry configuration of a named task.
671		#[pallet::call_index(9)]
672		#[pallet::weight(<T as Config>::WeightInfo::cancel_retry_named())]
673		pub fn cancel_retry_named(origin: OriginFor<T>, id: TaskName) -> DispatchResult {
674			T::ScheduleOrigin::ensure_origin(origin.clone())?;
675			let origin = <T as Config>::RuntimeOrigin::from(origin);
676			let task = Lookup::<T>::get(&id).ok_or(Error::<T>::NotFound)?;
677			Self::do_cancel_retry(origin.caller(), task)?;
678			Self::deposit_event(Event::RetryCancelled { task, id: Some(id) });
679			Ok(())
680		}
681	}
682}
683
684impl<T: Config> Pallet<T> {
685	/// Migrate storage format from V1 to V4.
686	///
687	/// Returns the weight consumed by this migration.
688	pub fn migrate_v1_to_v4() -> Weight {
689		use migration::v1 as old;
690		let mut weight = T::DbWeight::get().reads_writes(1, 1);
691
692		// Delete all undecodable values.
693		// `StorageMap::translate` is not enough since it just skips them and leaves the keys in.
694		let keys = old::Agenda::<T>::iter_keys().collect::<Vec<_>>();
695		for key in keys {
696			weight.saturating_accrue(T::DbWeight::get().reads(1));
697			if let Err(_) = old::Agenda::<T>::try_get(&key) {
698				weight.saturating_accrue(T::DbWeight::get().writes(1));
699				old::Agenda::<T>::remove(&key);
700				log::warn!("Deleted undecodable agenda");
701			}
702		}
703
704		Agenda::<T>::translate::<
705			Vec<Option<ScheduledV1<<T as Config>::RuntimeCall, BlockNumberFor<T>>>>,
706			_,
707		>(|_, agenda| {
708			Some(BoundedVec::truncate_from(
709				agenda
710					.into_iter()
711					.map(|schedule| {
712						weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
713
714						schedule.and_then(|schedule| {
715							if let Some(id) = schedule.maybe_id.as_ref() {
716								let name = blake2_256(id);
717								if let Some(item) = old::Lookup::<T>::take(id) {
718									Lookup::<T>::insert(name, item);
719								}
720								weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
721							}
722
723							let call = T::Preimages::bound(schedule.call).ok()?;
724
725							if call.lookup_needed() {
726								weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1));
727							}
728
729							Some(Scheduled {
730								maybe_id: schedule.maybe_id.map(|x| blake2_256(&x[..])),
731								priority: schedule.priority,
732								call,
733								maybe_periodic: schedule.maybe_periodic,
734								origin: system::RawOrigin::Root.into(),
735								_phantom: Default::default(),
736							})
737						})
738					})
739					.collect::<Vec<_>>(),
740			))
741		});
742
743		#[allow(deprecated)]
744		frame_support::storage::migration::remove_storage_prefix(
745			Self::name().as_bytes(),
746			b"StorageVersion",
747			&[],
748		);
749
750		StorageVersion::new(4).put::<Self>();
751
752		weight + T::DbWeight::get().writes(2)
753	}
754
755	/// Migrate storage format from V2 to V4.
756	///
757	/// Returns the weight consumed by this migration.
758	pub fn migrate_v2_to_v4() -> Weight {
759		use migration::v2 as old;
760		let mut weight = T::DbWeight::get().reads_writes(1, 1);
761
762		// Delete all undecodable values.
763		// `StorageMap::translate` is not enough since it just skips them and leaves the keys in.
764		let keys = old::Agenda::<T>::iter_keys().collect::<Vec<_>>();
765		for key in keys {
766			weight.saturating_accrue(T::DbWeight::get().reads(1));
767			if let Err(_) = old::Agenda::<T>::try_get(&key) {
768				weight.saturating_accrue(T::DbWeight::get().writes(1));
769				old::Agenda::<T>::remove(&key);
770				log::warn!("Deleted undecodable agenda");
771			}
772		}
773
774		Agenda::<T>::translate::<Vec<Option<ScheduledV2Of<T>>>, _>(|_, agenda| {
775			Some(BoundedVec::truncate_from(
776				agenda
777					.into_iter()
778					.map(|schedule| {
779						weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
780						schedule.and_then(|schedule| {
781							if let Some(id) = schedule.maybe_id.as_ref() {
782								let name = blake2_256(id);
783								if let Some(item) = old::Lookup::<T>::take(id) {
784									Lookup::<T>::insert(name, item);
785								}
786								weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
787							}
788
789							let call = T::Preimages::bound(schedule.call).ok()?;
790							if call.lookup_needed() {
791								weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1));
792							}
793
794							Some(Scheduled {
795								maybe_id: schedule.maybe_id.map(|x| blake2_256(&x[..])),
796								priority: schedule.priority,
797								call,
798								maybe_periodic: schedule.maybe_periodic,
799								origin: schedule.origin,
800								_phantom: Default::default(),
801							})
802						})
803					})
804					.collect::<Vec<_>>(),
805			))
806		});
807
808		#[allow(deprecated)]
809		frame_support::storage::migration::remove_storage_prefix(
810			Self::name().as_bytes(),
811			b"StorageVersion",
812			&[],
813		);
814
815		StorageVersion::new(4).put::<Self>();
816
817		weight + T::DbWeight::get().writes(2)
818	}
819
820	/// Migrate storage format from V3 to V4.
821	///
822	/// Returns the weight consumed by this migration.
823	#[allow(deprecated)]
824	pub fn migrate_v3_to_v4() -> Weight {
825		use migration::v3 as old;
826		let mut weight = T::DbWeight::get().reads_writes(2, 1);
827
828		// Delete all undecodable values.
829		// `StorageMap::translate` is not enough since it just skips them and leaves the keys in.
830		let blocks = old::Agenda::<T>::iter_keys().collect::<Vec<_>>();
831		for block in blocks {
832			weight.saturating_accrue(T::DbWeight::get().reads(1));
833			if let Err(_) = old::Agenda::<T>::try_get(&block) {
834				weight.saturating_accrue(T::DbWeight::get().writes(1));
835				old::Agenda::<T>::remove(&block);
836				log::warn!("Deleted undecodable agenda of block: {:?}", block);
837			}
838		}
839
840		Agenda::<T>::translate::<Vec<Option<ScheduledV3Of<T>>>, _>(|block, agenda| {
841			log::info!("Migrating agenda of block: {:?}", &block);
842			Some(BoundedVec::truncate_from(
843				agenda
844					.into_iter()
845					.map(|schedule| {
846						weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
847						schedule
848							.and_then(|schedule| {
849								if let Some(id) = schedule.maybe_id.as_ref() {
850									let name = blake2_256(id);
851									if let Some(item) = old::Lookup::<T>::take(id) {
852										Lookup::<T>::insert(name, item);
853										log::info!("Migrated name for id: {:?}", id);
854									} else {
855										log::error!("No name in Lookup for id: {:?}", &id);
856									}
857									weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
858								} else {
859									log::info!("Schedule is unnamed");
860								}
861
862								let call = match schedule.call {
863									MaybeHashed::Hash(h) => {
864										let bounded = Bounded::from_legacy_hash(h);
865										// Check that the call can be decoded in the new runtime.
866										if let Err(err) = T::Preimages::peek::<
867											<T as Config>::RuntimeCall,
868										>(&bounded)
869										{
870											log::error!(
871												"Dropping undecodable call {:?}: {:?}",
872												&h,
873												&err
874											);
875											return None
876										}
877										weight.saturating_accrue(T::DbWeight::get().reads(1));
878										log::info!("Migrated call by hash, hash: {:?}", h);
879										bounded
880									},
881									MaybeHashed::Value(v) => {
882										let call = T::Preimages::bound(v)
883											.map_err(|e| {
884												log::error!("Could not bound Call: {:?}", e)
885											})
886											.ok()?;
887										if call.lookup_needed() {
888											weight.saturating_accrue(
889												T::DbWeight::get().reads_writes(0, 1),
890											);
891										}
892										log::info!(
893											"Migrated call by value, hash: {:?}",
894											call.hash()
895										);
896										call
897									},
898								};
899
900								Some(Scheduled {
901									maybe_id: schedule.maybe_id.map(|x| blake2_256(&x[..])),
902									priority: schedule.priority,
903									call,
904									maybe_periodic: schedule.maybe_periodic,
905									origin: schedule.origin,
906									_phantom: Default::default(),
907								})
908							})
909							.or_else(|| {
910								log::info!("Schedule in agenda for block {:?} is empty - nothing to do here.", &block);
911								None
912							})
913					})
914					.collect::<Vec<_>>(),
915			))
916		});
917
918		#[allow(deprecated)]
919		frame_support::storage::migration::remove_storage_prefix(
920			Self::name().as_bytes(),
921			b"StorageVersion",
922			&[],
923		);
924
925		StorageVersion::new(4).put::<Self>();
926
927		weight + T::DbWeight::get().writes(2)
928	}
929}
930
931impl<T: Config> Pallet<T> {
932	/// Helper to migrate scheduler when the pallet origin type has changed.
933	pub fn migrate_origin<OldOrigin: Into<T::PalletsOrigin> + codec::Decode>() {
934		Agenda::<T>::translate::<
935			Vec<
936				Option<
937					Scheduled<
938						TaskName,
939						BoundedCallOf<T>,
940						BlockNumberFor<T>,
941						OldOrigin,
942						T::AccountId,
943					>,
944				>,
945			>,
946			_,
947		>(|_, agenda| {
948			Some(BoundedVec::truncate_from(
949				agenda
950					.into_iter()
951					.map(|schedule| {
952						schedule.map(|schedule| Scheduled {
953							maybe_id: schedule.maybe_id,
954							priority: schedule.priority,
955							call: schedule.call,
956							maybe_periodic: schedule.maybe_periodic,
957							origin: schedule.origin.into(),
958							_phantom: Default::default(),
959						})
960					})
961					.collect::<Vec<_>>(),
962			))
963		});
964	}
965
966	fn resolve_time(
967		when: DispatchTime<BlockNumberFor<T>>,
968	) -> Result<BlockNumberFor<T>, DispatchError> {
969		let now = T::BlockNumberProvider::current_block_number();
970		let when = match when {
971			DispatchTime::At(x) => x,
972			// The current block has already completed it's scheduled tasks, so
973			// Schedule the task at lest one block after this current block.
974			DispatchTime::After(x) => now.saturating_add(x).saturating_add(One::one()),
975		};
976
977		if when <= now {
978			return Err(Error::<T>::TargetBlockNumberInPast.into())
979		}
980
981		Ok(when)
982	}
983
984	fn place_task(
985		when: BlockNumberFor<T>,
986		what: ScheduledOf<T>,
987	) -> Result<TaskAddress<BlockNumberFor<T>>, (DispatchError, ScheduledOf<T>)> {
988		let maybe_name = what.maybe_id;
989		let index = Self::push_to_agenda(when, what)?;
990		let address = (when, index);
991		if let Some(name) = maybe_name {
992			Lookup::<T>::insert(name, address)
993		}
994		Self::deposit_event(Event::Scheduled { when: address.0, index: address.1 });
995		Ok(address)
996	}
997
998	fn push_to_agenda(
999		when: BlockNumberFor<T>,
1000		what: ScheduledOf<T>,
1001	) -> Result<u32, (DispatchError, ScheduledOf<T>)> {
1002		let mut agenda = Agenda::<T>::get(when);
1003		let index = if (agenda.len() as u32) < T::MaxScheduledPerBlock::get() {
1004			// will always succeed due to the above check.
1005			let _ = agenda.try_push(Some(what));
1006			agenda.len() as u32 - 1
1007		} else {
1008			if let Some(hole_index) = agenda.iter().position(|i| i.is_none()) {
1009				agenda[hole_index] = Some(what);
1010				hole_index as u32
1011			} else {
1012				return Err((DispatchError::Exhausted, what))
1013			}
1014		};
1015		Agenda::<T>::insert(when, agenda);
1016		Ok(index)
1017	}
1018
1019	/// Remove trailing `None` items of an agenda at `when`. If all items are `None` remove the
1020	/// agenda record entirely.
1021	fn cleanup_agenda(when: BlockNumberFor<T>) {
1022		let mut agenda = Agenda::<T>::get(when);
1023		match agenda.iter().rposition(|i| i.is_some()) {
1024			Some(i) if agenda.len() > i + 1 => {
1025				agenda.truncate(i + 1);
1026				Agenda::<T>::insert(when, agenda);
1027			},
1028			Some(_) => {},
1029			None => {
1030				Agenda::<T>::remove(when);
1031			},
1032		}
1033	}
1034
1035	fn do_schedule(
1036		when: DispatchTime<BlockNumberFor<T>>,
1037		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1038		priority: schedule::Priority,
1039		origin: T::PalletsOrigin,
1040		call: BoundedCallOf<T>,
1041	) -> Result<TaskAddress<BlockNumberFor<T>>, DispatchError> {
1042		let when = Self::resolve_time(when)?;
1043
1044		let lookup_hash = call.lookup_hash();
1045
1046		// sanitize maybe_periodic
1047		let maybe_periodic = maybe_periodic
1048			.filter(|p| p.1 > 1 && !p.0.is_zero())
1049			// Remove one from the number of repetitions since we will schedule one now.
1050			.map(|(p, c)| (p, c - 1));
1051		let task = Scheduled {
1052			maybe_id: None,
1053			priority,
1054			call,
1055			maybe_periodic,
1056			origin,
1057			_phantom: PhantomData,
1058		};
1059		let res = Self::place_task(when, task).map_err(|x| x.0)?;
1060
1061		if let Some(hash) = lookup_hash {
1062			// Request the call to be made available.
1063			T::Preimages::request(&hash);
1064		}
1065
1066		Ok(res)
1067	}
1068
1069	fn do_cancel(
1070		origin: Option<T::PalletsOrigin>,
1071		(when, index): TaskAddress<BlockNumberFor<T>>,
1072	) -> Result<(), DispatchError> {
1073		let scheduled = Agenda::<T>::try_mutate(when, |agenda| {
1074			agenda.get_mut(index as usize).map_or(
1075				Ok(None),
1076				|s| -> Result<Option<Scheduled<_, _, _, _, _>>, DispatchError> {
1077					if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) {
1078						Self::ensure_privilege(o, &s.origin)?;
1079					};
1080					Ok(s.take())
1081				},
1082			)
1083		})?;
1084		if let Some(s) = scheduled {
1085			T::Preimages::drop(&s.call);
1086			if let Some(id) = s.maybe_id {
1087				Lookup::<T>::remove(id);
1088			}
1089			Retries::<T>::remove((when, index));
1090			Self::cleanup_agenda(when);
1091			Self::deposit_event(Event::Canceled { when, index });
1092			Ok(())
1093		} else {
1094			return Err(Error::<T>::NotFound.into())
1095		}
1096	}
1097
1098	fn do_reschedule(
1099		(when, index): TaskAddress<BlockNumberFor<T>>,
1100		new_time: DispatchTime<BlockNumberFor<T>>,
1101	) -> Result<TaskAddress<BlockNumberFor<T>>, DispatchError> {
1102		let new_time = Self::resolve_time(new_time)?;
1103
1104		if new_time == when {
1105			return Err(Error::<T>::RescheduleNoChange.into())
1106		}
1107
1108		let task = Agenda::<T>::try_mutate(when, |agenda| {
1109			let task = agenda.get_mut(index as usize).ok_or(Error::<T>::NotFound)?;
1110			ensure!(!matches!(task, Some(Scheduled { maybe_id: Some(_), .. })), Error::<T>::Named);
1111			task.take().ok_or(Error::<T>::NotFound)
1112		})?;
1113		Self::cleanup_agenda(when);
1114		Self::deposit_event(Event::Canceled { when, index });
1115
1116		Self::place_task(new_time, task).map_err(|x| x.0)
1117	}
1118
1119	fn do_schedule_named(
1120		id: TaskName,
1121		when: DispatchTime<BlockNumberFor<T>>,
1122		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1123		priority: schedule::Priority,
1124		origin: T::PalletsOrigin,
1125		call: BoundedCallOf<T>,
1126	) -> Result<TaskAddress<BlockNumberFor<T>>, DispatchError> {
1127		// ensure id it is unique
1128		if Lookup::<T>::contains_key(&id) {
1129			return Err(Error::<T>::FailedToSchedule.into())
1130		}
1131
1132		let when = Self::resolve_time(when)?;
1133
1134		let lookup_hash = call.lookup_hash();
1135
1136		// sanitize maybe_periodic
1137		let maybe_periodic = maybe_periodic
1138			.filter(|p| p.1 > 1 && !p.0.is_zero())
1139			// Remove one from the number of repetitions since we will schedule one now.
1140			.map(|(p, c)| (p, c - 1));
1141
1142		let task = Scheduled {
1143			maybe_id: Some(id),
1144			priority,
1145			call,
1146			maybe_periodic,
1147			origin,
1148			_phantom: Default::default(),
1149		};
1150		let res = Self::place_task(when, task).map_err(|x| x.0)?;
1151
1152		if let Some(hash) = lookup_hash {
1153			// Request the call to be made available.
1154			T::Preimages::request(&hash);
1155		}
1156
1157		Ok(res)
1158	}
1159
1160	fn do_cancel_named(origin: Option<T::PalletsOrigin>, id: TaskName) -> DispatchResult {
1161		Lookup::<T>::try_mutate_exists(id, |lookup| -> DispatchResult {
1162			if let Some((when, index)) = lookup.take() {
1163				let i = index as usize;
1164				Agenda::<T>::try_mutate(when, |agenda| -> DispatchResult {
1165					if let Some(s) = agenda.get_mut(i) {
1166						if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) {
1167							Self::ensure_privilege(o, &s.origin)?;
1168							Retries::<T>::remove((when, index));
1169							T::Preimages::drop(&s.call);
1170						}
1171						*s = None;
1172					}
1173					Ok(())
1174				})?;
1175				Self::cleanup_agenda(when);
1176				Self::deposit_event(Event::Canceled { when, index });
1177				Ok(())
1178			} else {
1179				return Err(Error::<T>::NotFound.into())
1180			}
1181		})
1182	}
1183
1184	fn do_reschedule_named(
1185		id: TaskName,
1186		new_time: DispatchTime<BlockNumberFor<T>>,
1187	) -> Result<TaskAddress<BlockNumberFor<T>>, DispatchError> {
1188		let new_time = Self::resolve_time(new_time)?;
1189
1190		let lookup = Lookup::<T>::get(id);
1191		let (when, index) = lookup.ok_or(Error::<T>::NotFound)?;
1192
1193		if new_time == when {
1194			return Err(Error::<T>::RescheduleNoChange.into())
1195		}
1196
1197		let task = Agenda::<T>::try_mutate(when, |agenda| {
1198			let task = agenda.get_mut(index as usize).ok_or(Error::<T>::NotFound)?;
1199			task.take().ok_or(Error::<T>::NotFound)
1200		})?;
1201		Self::cleanup_agenda(when);
1202		Self::deposit_event(Event::Canceled { when, index });
1203		Self::place_task(new_time, task).map_err(|x| x.0)
1204	}
1205
1206	fn do_cancel_retry(
1207		origin: &T::PalletsOrigin,
1208		(when, index): TaskAddress<BlockNumberFor<T>>,
1209	) -> Result<(), DispatchError> {
1210		let agenda = Agenda::<T>::get(when);
1211		let scheduled = agenda
1212			.get(index as usize)
1213			.and_then(Option::as_ref)
1214			.ok_or(Error::<T>::NotFound)?;
1215		Self::ensure_privilege(origin, &scheduled.origin)?;
1216		Retries::<T>::remove((when, index));
1217		Ok(())
1218	}
1219}
1220
1221enum ServiceTaskError {
1222	/// Could not be executed due to missing preimage.
1223	Unavailable,
1224	/// Could not be executed due to weight limitations.
1225	Overweight,
1226}
1227use ServiceTaskError::*;
1228
1229impl<T: Config> Pallet<T> {
1230	/// Service up to `max` agendas queue starting from earliest incompletely executed agenda.
1231	fn service_agendas(weight: &mut WeightMeter, now: BlockNumberFor<T>, max: u32) {
1232		if weight.try_consume(T::WeightInfo::service_agendas_base()).is_err() {
1233			return
1234		}
1235
1236		let mut incomplete_since = now + One::one();
1237		let mut when = IncompleteSince::<T>::take().unwrap_or(now);
1238		let mut is_first = true; // first task from the first agenda.
1239
1240		let max_items = T::MaxScheduledPerBlock::get();
1241		let mut count_down = max;
1242		let service_agenda_base_weight = T::WeightInfo::service_agenda_base(max_items);
1243		while count_down > 0 && when <= now && weight.can_consume(service_agenda_base_weight) {
1244			if !Self::service_agenda(weight, is_first, now, when, u32::MAX) {
1245				incomplete_since = incomplete_since.min(when);
1246			}
1247			is_first = false;
1248			when.saturating_inc();
1249			count_down.saturating_dec();
1250		}
1251		incomplete_since = incomplete_since.min(when);
1252		if incomplete_since <= now {
1253			Self::deposit_event(Event::AgendaIncomplete { when: incomplete_since });
1254			IncompleteSince::<T>::put(incomplete_since);
1255		} else {
1256			// The next scheduler iteration should typically start from `now + 1` (`next_iter_now`).
1257			// However, if the [`Config::BlockNumberProvider`] is not a local block number provider,
1258			// then `next_iter_now` could be `now + n` where `n > 1`. In this case, we want to start
1259			// from `now + 1` to ensure we don't miss any agendas.
1260			IncompleteSince::<T>::put(now + One::one());
1261		}
1262	}
1263
1264	/// Returns `true` if the agenda was fully completed, `false` if it should be revisited at a
1265	/// later block.
1266	fn service_agenda(
1267		weight: &mut WeightMeter,
1268		mut is_first: bool,
1269		now: BlockNumberFor<T>,
1270		when: BlockNumberFor<T>,
1271		max: u32,
1272	) -> bool {
1273		let mut agenda = Agenda::<T>::get(when);
1274		let mut ordered = agenda
1275			.iter()
1276			.enumerate()
1277			.filter_map(|(index, maybe_item)| {
1278				maybe_item.as_ref().map(|item| (index as u32, item.priority))
1279			})
1280			.collect::<Vec<_>>();
1281		ordered.sort_by_key(|k| k.1);
1282		let within_limit = weight
1283			.try_consume(T::WeightInfo::service_agenda_base(ordered.len() as u32))
1284			.is_ok();
1285		debug_assert!(within_limit, "weight limit should have been checked in advance");
1286
1287		// Items which we know can be executed and have postponed for execution in a later block.
1288		let mut postponed = (ordered.len() as u32).saturating_sub(max);
1289		// Items which we don't know can ever be executed.
1290		let mut dropped = 0;
1291
1292		for (agenda_index, _) in ordered.into_iter().take(max as usize) {
1293			let Some(task) = agenda[agenda_index as usize].take() else { continue };
1294			let base_weight = T::WeightInfo::service_task(
1295				task.call.lookup_len().map(|x| x as usize),
1296				task.maybe_id.is_some(),
1297				task.maybe_periodic.is_some(),
1298			);
1299			if !weight.can_consume(base_weight) {
1300				postponed += 1;
1301				agenda[agenda_index as usize] = Some(task);
1302				break
1303			}
1304			let result = Self::service_task(weight, now, when, agenda_index, is_first, task);
1305			agenda[agenda_index as usize] = match result {
1306				Err((Unavailable, slot)) => {
1307					dropped += 1;
1308					slot
1309				},
1310				Err((Overweight, slot)) => {
1311					postponed += 1;
1312					slot
1313				},
1314				Ok(()) => {
1315					is_first = false;
1316					None
1317				},
1318			};
1319		}
1320		if postponed > 0 || dropped > 0 {
1321			Agenda::<T>::insert(when, agenda);
1322		} else {
1323			Agenda::<T>::remove(when);
1324		}
1325
1326		postponed == 0
1327	}
1328
1329	/// Service (i.e. execute) the given task, being careful not to overflow the `weight` counter.
1330	///
1331	/// This involves:
1332	/// - removing and potentially replacing the `Lookup` entry for the task.
1333	/// - realizing the task's call which can include a preimage lookup.
1334	/// - Rescheduling the task for execution in a later agenda if periodic.
1335	fn service_task(
1336		weight: &mut WeightMeter,
1337		now: BlockNumberFor<T>,
1338		when: BlockNumberFor<T>,
1339		agenda_index: u32,
1340		is_first: bool,
1341		mut task: ScheduledOf<T>,
1342	) -> Result<(), (ServiceTaskError, Option<ScheduledOf<T>>)> {
1343		if let Some(ref id) = task.maybe_id {
1344			Lookup::<T>::remove(id);
1345		}
1346
1347		let (call, lookup_len) = match T::Preimages::peek(&task.call) {
1348			Ok(c) => c,
1349			Err(_) => {
1350				Self::deposit_event(Event::CallUnavailable {
1351					task: (when, agenda_index),
1352					id: task.maybe_id,
1353				});
1354
1355				// It was not available when we needed it, so we don't need to have requested it
1356				// anymore.
1357				T::Preimages::drop(&task.call);
1358
1359				// We don't know why `peek` failed, thus we most account here for the "full weight".
1360				let _ = weight.try_consume(T::WeightInfo::service_task(
1361					task.call.lookup_len().map(|x| x as usize),
1362					task.maybe_id.is_some(),
1363					task.maybe_periodic.is_some(),
1364				));
1365
1366				return Err((Unavailable, Some(task)))
1367			},
1368		};
1369
1370		let _ = weight.try_consume(T::WeightInfo::service_task(
1371			lookup_len.map(|x| x as usize),
1372			task.maybe_id.is_some(),
1373			task.maybe_periodic.is_some(),
1374		));
1375
1376		match Self::execute_dispatch(weight, task.origin.clone(), call) {
1377			Err(()) if is_first => {
1378				T::Preimages::drop(&task.call);
1379				Self::deposit_event(Event::PermanentlyOverweight {
1380					task: (when, agenda_index),
1381					id: task.maybe_id,
1382				});
1383				Err((Unavailable, Some(task)))
1384			},
1385			Err(()) => Err((Overweight, Some(task))),
1386			Ok(result) => {
1387				let failed = result.is_err();
1388				let maybe_retry_config = Retries::<T>::take((when, agenda_index));
1389				Self::deposit_event(Event::Dispatched {
1390					task: (when, agenda_index),
1391					id: task.maybe_id,
1392					result,
1393				});
1394
1395				match maybe_retry_config {
1396					Some(retry_config) if failed => {
1397						Self::schedule_retry(weight, now, when, agenda_index, &task, retry_config);
1398					},
1399					_ => {},
1400				}
1401
1402				if let &Some((period, count)) = &task.maybe_periodic {
1403					if count > 1 {
1404						task.maybe_periodic = Some((period, count - 1));
1405					} else {
1406						task.maybe_periodic = None;
1407					}
1408					let wake = now.saturating_add(period);
1409					match Self::place_task(wake, task) {
1410						Ok(new_address) =>
1411							if let Some(retry_config) = maybe_retry_config {
1412								Retries::<T>::insert(new_address, retry_config);
1413							},
1414						Err((_, task)) => {
1415							// TODO: Leave task in storage somewhere for it to be rescheduled
1416							// manually.
1417							T::Preimages::drop(&task.call);
1418							Self::deposit_event(Event::PeriodicFailed {
1419								task: (when, agenda_index),
1420								id: task.maybe_id,
1421							});
1422						},
1423					}
1424				} else {
1425					T::Preimages::drop(&task.call);
1426				}
1427				Ok(())
1428			},
1429		}
1430	}
1431
1432	/// Make a dispatch to the given `call` from the given `origin`, ensuring that the `weight`
1433	/// counter does not exceed its limit and that it is counted accurately (e.g. accounted using
1434	/// post info if available).
1435	///
1436	/// NOTE: Only the weight for this function will be counted (origin lookup, dispatch and the
1437	/// call itself).
1438	///
1439	/// Returns an error if the call is overweight.
1440	fn execute_dispatch(
1441		weight: &mut WeightMeter,
1442		origin: T::PalletsOrigin,
1443		call: <T as Config>::RuntimeCall,
1444	) -> Result<DispatchResult, ()> {
1445		let base_weight = match origin.as_system_ref() {
1446			Some(&RawOrigin::Signed(_)) => T::WeightInfo::execute_dispatch_signed(),
1447			_ => T::WeightInfo::execute_dispatch_unsigned(),
1448		};
1449		let call_weight = call.get_dispatch_info().call_weight;
1450		// We only allow a scheduled call if it cannot push the weight past the limit.
1451		let max_weight = base_weight.saturating_add(call_weight);
1452
1453		if !weight.can_consume(max_weight) {
1454			return Err(())
1455		}
1456
1457		let dispatch_origin = origin.into();
1458		let (maybe_actual_call_weight, result) = match call.dispatch(dispatch_origin) {
1459			Ok(post_info) => (post_info.actual_weight, Ok(())),
1460			Err(error_and_info) =>
1461				(error_and_info.post_info.actual_weight, Err(error_and_info.error)),
1462		};
1463		let call_weight = maybe_actual_call_weight.unwrap_or(call_weight);
1464		let _ = weight.try_consume(base_weight);
1465		let _ = weight.try_consume(call_weight);
1466		Ok(result)
1467	}
1468
1469	/// Check if a task has a retry configuration in place and, if so, try to reschedule it.
1470	///
1471	/// Possible causes for failure to schedule a retry for a task:
1472	/// - there wasn't enough weight to run the task reschedule logic
1473	/// - there was no retry configuration in place
1474	/// - there were no more retry attempts left
1475	/// - the agenda was full.
1476	fn schedule_retry(
1477		weight: &mut WeightMeter,
1478		now: BlockNumberFor<T>,
1479		when: BlockNumberFor<T>,
1480		agenda_index: u32,
1481		task: &ScheduledOf<T>,
1482		retry_config: RetryConfig<BlockNumberFor<T>>,
1483	) {
1484		if weight
1485			.try_consume(T::WeightInfo::schedule_retry(T::MaxScheduledPerBlock::get()))
1486			.is_err()
1487		{
1488			Self::deposit_event(Event::RetryFailed {
1489				task: (when, agenda_index),
1490				id: task.maybe_id,
1491			});
1492			return;
1493		}
1494
1495		let RetryConfig { total_retries, mut remaining, period } = retry_config;
1496		remaining = match remaining.checked_sub(1) {
1497			Some(n) => n,
1498			None => return,
1499		};
1500		let wake = now.saturating_add(period);
1501		match Self::place_task(wake, task.as_retry()) {
1502			Ok(address) => {
1503				// Reinsert the retry config to the new address of the task after it was
1504				// placed.
1505				Retries::<T>::insert(address, RetryConfig { total_retries, remaining, period });
1506			},
1507			Err((_, task)) => {
1508				// TODO: Leave task in storage somewhere for it to be
1509				// rescheduled manually.
1510				T::Preimages::drop(&task.call);
1511				Self::deposit_event(Event::RetryFailed {
1512					task: (when, agenda_index),
1513					id: task.maybe_id,
1514				});
1515			},
1516		}
1517	}
1518
1519	/// Ensure that `left` has at least the same level of privilege or higher than `right`.
1520	///
1521	/// Returns an error if `left` has a lower level of privilege or the two cannot be compared.
1522	fn ensure_privilege(
1523		left: &<T as Config>::PalletsOrigin,
1524		right: &<T as Config>::PalletsOrigin,
1525	) -> Result<(), DispatchError> {
1526		if matches!(T::OriginPrivilegeCmp::cmp_privilege(left, right), Some(Ordering::Less) | None)
1527		{
1528			return Err(BadOrigin.into());
1529		}
1530		Ok(())
1531	}
1532}
1533
1534#[allow(deprecated)]
1535impl<T: Config> schedule::v2::Anon<BlockNumberFor<T>, <T as Config>::RuntimeCall, T::PalletsOrigin>
1536	for Pallet<T>
1537{
1538	type Address = TaskAddress<BlockNumberFor<T>>;
1539	type Hash = T::Hash;
1540
1541	fn schedule(
1542		when: DispatchTime<BlockNumberFor<T>>,
1543		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1544		priority: schedule::Priority,
1545		origin: T::PalletsOrigin,
1546		call: CallOrHashOf<T>,
1547	) -> Result<Self::Address, DispatchError> {
1548		let call = call.as_value().ok_or(DispatchError::CannotLookup)?;
1549		let call = T::Preimages::bound(call)?.transmute();
1550		Self::do_schedule(when, maybe_periodic, priority, origin, call)
1551	}
1552
1553	fn cancel((when, index): Self::Address) -> Result<(), ()> {
1554		Self::do_cancel(None, (when, index)).map_err(|_| ())
1555	}
1556
1557	fn reschedule(
1558		address: Self::Address,
1559		when: DispatchTime<BlockNumberFor<T>>,
1560	) -> Result<Self::Address, DispatchError> {
1561		Self::do_reschedule(address, when)
1562	}
1563
1564	fn next_dispatch_time((when, index): Self::Address) -> Result<BlockNumberFor<T>, ()> {
1565		Agenda::<T>::get(when).get(index as usize).ok_or(()).map(|_| when)
1566	}
1567}
1568
1569// TODO: migrate `schedule::v2::Anon` to `v3`
1570#[allow(deprecated)]
1571impl<T: Config> schedule::v2::Named<BlockNumberFor<T>, <T as Config>::RuntimeCall, T::PalletsOrigin>
1572	for Pallet<T>
1573{
1574	type Address = TaskAddress<BlockNumberFor<T>>;
1575	type Hash = T::Hash;
1576
1577	fn schedule_named(
1578		id: Vec<u8>,
1579		when: DispatchTime<BlockNumberFor<T>>,
1580		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1581		priority: schedule::Priority,
1582		origin: T::PalletsOrigin,
1583		call: CallOrHashOf<T>,
1584	) -> Result<Self::Address, ()> {
1585		let call = call.as_value().ok_or(())?;
1586		let call = T::Preimages::bound(call).map_err(|_| ())?.transmute();
1587		let name = blake2_256(&id[..]);
1588		Self::do_schedule_named(name, when, maybe_periodic, priority, origin, call).map_err(|_| ())
1589	}
1590
1591	fn cancel_named(id: Vec<u8>) -> Result<(), ()> {
1592		let name = blake2_256(&id[..]);
1593		Self::do_cancel_named(None, name).map_err(|_| ())
1594	}
1595
1596	fn reschedule_named(
1597		id: Vec<u8>,
1598		when: DispatchTime<BlockNumberFor<T>>,
1599	) -> Result<Self::Address, DispatchError> {
1600		let name = blake2_256(&id[..]);
1601		Self::do_reschedule_named(name, when)
1602	}
1603
1604	fn next_dispatch_time(id: Vec<u8>) -> Result<BlockNumberFor<T>, ()> {
1605		let name = blake2_256(&id[..]);
1606		Lookup::<T>::get(name)
1607			.and_then(|(when, index)| Agenda::<T>::get(when).get(index as usize).map(|_| when))
1608			.ok_or(())
1609	}
1610}
1611
1612impl<T: Config> schedule::v3::Anon<BlockNumberFor<T>, <T as Config>::RuntimeCall, T::PalletsOrigin>
1613	for Pallet<T>
1614{
1615	type Address = TaskAddress<BlockNumberFor<T>>;
1616	type Hasher = T::Hashing;
1617
1618	fn schedule(
1619		when: DispatchTime<BlockNumberFor<T>>,
1620		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1621		priority: schedule::Priority,
1622		origin: T::PalletsOrigin,
1623		call: BoundedCallOf<T>,
1624	) -> Result<Self::Address, DispatchError> {
1625		Self::do_schedule(when, maybe_periodic, priority, origin, call)
1626	}
1627
1628	fn cancel((when, index): Self::Address) -> Result<(), DispatchError> {
1629		Self::do_cancel(None, (when, index)).map_err(map_err_to_v3_err::<T>)
1630	}
1631
1632	fn reschedule(
1633		address: Self::Address,
1634		when: DispatchTime<BlockNumberFor<T>>,
1635	) -> Result<Self::Address, DispatchError> {
1636		Self::do_reschedule(address, when).map_err(map_err_to_v3_err::<T>)
1637	}
1638
1639	fn next_dispatch_time(
1640		(when, index): Self::Address,
1641	) -> Result<BlockNumberFor<T>, DispatchError> {
1642		Agenda::<T>::get(when)
1643			.get(index as usize)
1644			.ok_or(DispatchError::Unavailable)
1645			.map(|_| when)
1646	}
1647}
1648
1649use schedule::v3::TaskName;
1650
1651impl<T: Config> schedule::v3::Named<BlockNumberFor<T>, <T as Config>::RuntimeCall, T::PalletsOrigin>
1652	for Pallet<T>
1653{
1654	type Address = TaskAddress<BlockNumberFor<T>>;
1655	type Hasher = T::Hashing;
1656
1657	fn schedule_named(
1658		id: TaskName,
1659		when: DispatchTime<BlockNumberFor<T>>,
1660		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1661		priority: schedule::Priority,
1662		origin: T::PalletsOrigin,
1663		call: BoundedCallOf<T>,
1664	) -> Result<Self::Address, DispatchError> {
1665		Self::do_schedule_named(id, when, maybe_periodic, priority, origin, call)
1666	}
1667
1668	fn cancel_named(id: TaskName) -> Result<(), DispatchError> {
1669		Self::do_cancel_named(None, id).map_err(map_err_to_v3_err::<T>)
1670	}
1671
1672	fn reschedule_named(
1673		id: TaskName,
1674		when: DispatchTime<BlockNumberFor<T>>,
1675	) -> Result<Self::Address, DispatchError> {
1676		Self::do_reschedule_named(id, when).map_err(map_err_to_v3_err::<T>)
1677	}
1678
1679	fn next_dispatch_time(id: TaskName) -> Result<BlockNumberFor<T>, DispatchError> {
1680		Lookup::<T>::get(id)
1681			.and_then(|(when, index)| Agenda::<T>::get(when).get(index as usize).map(|_| when))
1682			.ok_or(DispatchError::Unavailable)
1683	}
1684}
1685
1686/// Maps a pallet error to an `schedule::v3` error.
1687fn map_err_to_v3_err<T: Config>(err: DispatchError) -> DispatchError {
1688	if err == DispatchError::from(Error::<T>::NotFound) {
1689		DispatchError::Unavailable
1690	} else {
1691		err
1692	}
1693}