referrerpolicy=no-referrer-when-downgrade

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 = WeightMeter::with_limit(T::MaximumWeight::get());
436			Self::service_agendas(&mut weight_counter, now, u32::MAX);
437			weight_counter.consumed()
438		}
439
440		#[cfg(feature = "std")]
441		fn integrity_test() {
442			/// Calculate the maximum weight that a lookup of a given size can take.
443			fn lookup_weight<T: Config>(s: usize) -> Weight {
444				T::WeightInfo::service_agendas_base() +
445					T::WeightInfo::service_agenda_base(T::MaxScheduledPerBlock::get()) +
446					T::WeightInfo::service_task(Some(s), true, true)
447			}
448
449			let limit = sp_runtime::Perbill::from_percent(90) * T::MaximumWeight::get();
450
451			let small_lookup = lookup_weight::<T>(128);
452			assert!(small_lookup.all_lte(limit), "Must be possible to submit a small lookup");
453
454			let medium_lookup = lookup_weight::<T>(1024);
455			assert!(medium_lookup.all_lte(limit), "Must be possible to submit a medium lookup");
456
457			let large_lookup = lookup_weight::<T>(1024 * 1024);
458			assert!(large_lookup.all_lte(limit), "Must be possible to submit a large lookup");
459		}
460	}
461
462	#[pallet::call]
463	impl<T: Config> Pallet<T> {
464		/// Anonymously schedule a task.
465		#[pallet::call_index(0)]
466		#[pallet::weight(<T as Config>::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))]
467		pub fn schedule(
468			origin: OriginFor<T>,
469			when: BlockNumberFor<T>,
470			maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
471			priority: schedule::Priority,
472			call: Box<<T as Config>::RuntimeCall>,
473		) -> DispatchResult {
474			T::ScheduleOrigin::ensure_origin(origin.clone())?;
475			let origin = <T as Config>::RuntimeOrigin::from(origin);
476			Self::do_schedule(
477				DispatchTime::At(when),
478				maybe_periodic,
479				priority,
480				origin.caller().clone(),
481				T::Preimages::bound(*call)?,
482			)?;
483			Ok(())
484		}
485
486		/// Cancel an anonymously scheduled task.
487		#[pallet::call_index(1)]
488		#[pallet::weight(<T as Config>::WeightInfo::cancel(T::MaxScheduledPerBlock::get()))]
489		pub fn cancel(origin: OriginFor<T>, when: BlockNumberFor<T>, index: u32) -> DispatchResult {
490			T::ScheduleOrigin::ensure_origin(origin.clone())?;
491			let origin = <T as Config>::RuntimeOrigin::from(origin);
492			Self::do_cancel(Some(origin.caller().clone()), (when, index))?;
493			Ok(())
494		}
495
496		/// Schedule a named task.
497		#[pallet::call_index(2)]
498		#[pallet::weight(<T as Config>::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))]
499		pub fn schedule_named(
500			origin: OriginFor<T>,
501			id: TaskName,
502			when: BlockNumberFor<T>,
503			maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
504			priority: schedule::Priority,
505			call: Box<<T as Config>::RuntimeCall>,
506		) -> DispatchResult {
507			T::ScheduleOrigin::ensure_origin(origin.clone())?;
508			let origin = <T as Config>::RuntimeOrigin::from(origin);
509			Self::do_schedule_named(
510				id,
511				DispatchTime::At(when),
512				maybe_periodic,
513				priority,
514				origin.caller().clone(),
515				T::Preimages::bound(*call)?,
516			)?;
517			Ok(())
518		}
519
520		/// Cancel a named scheduled task.
521		#[pallet::call_index(3)]
522		#[pallet::weight(<T as Config>::WeightInfo::cancel_named(T::MaxScheduledPerBlock::get()))]
523		pub fn cancel_named(origin: OriginFor<T>, id: TaskName) -> DispatchResult {
524			T::ScheduleOrigin::ensure_origin(origin.clone())?;
525			let origin = <T as Config>::RuntimeOrigin::from(origin);
526			Self::do_cancel_named(Some(origin.caller().clone()), id)?;
527			Ok(())
528		}
529
530		/// Anonymously schedule a task after a delay.
531		#[pallet::call_index(4)]
532		#[pallet::weight(<T as Config>::WeightInfo::schedule(T::MaxScheduledPerBlock::get()))]
533		pub fn schedule_after(
534			origin: OriginFor<T>,
535			after: BlockNumberFor<T>,
536			maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
537			priority: schedule::Priority,
538			call: Box<<T as Config>::RuntimeCall>,
539		) -> DispatchResult {
540			T::ScheduleOrigin::ensure_origin(origin.clone())?;
541			let origin = <T as Config>::RuntimeOrigin::from(origin);
542			Self::do_schedule(
543				DispatchTime::After(after),
544				maybe_periodic,
545				priority,
546				origin.caller().clone(),
547				T::Preimages::bound(*call)?,
548			)?;
549			Ok(())
550		}
551
552		/// Schedule a named task after a delay.
553		#[pallet::call_index(5)]
554		#[pallet::weight(<T as Config>::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get()))]
555		pub fn schedule_named_after(
556			origin: OriginFor<T>,
557			id: TaskName,
558			after: BlockNumberFor<T>,
559			maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
560			priority: schedule::Priority,
561			call: Box<<T as Config>::RuntimeCall>,
562		) -> DispatchResult {
563			T::ScheduleOrigin::ensure_origin(origin.clone())?;
564			let origin = <T as Config>::RuntimeOrigin::from(origin);
565			Self::do_schedule_named(
566				id,
567				DispatchTime::After(after),
568				maybe_periodic,
569				priority,
570				origin.caller().clone(),
571				T::Preimages::bound(*call)?,
572			)?;
573			Ok(())
574		}
575
576		/// Set a retry configuration for a task so that, in case its scheduled run fails, it will
577		/// be retried after `period` blocks, for a total amount of `retries` retries or until it
578		/// succeeds.
579		///
580		/// Tasks which need to be scheduled for a retry are still subject to weight metering and
581		/// agenda space, same as a regular task. If a periodic task fails, it will be scheduled
582		/// normally while the task is retrying.
583		///
584		/// Tasks scheduled as a result of a retry for a periodic task are unnamed, non-periodic
585		/// clones of the original task. Their retry configuration will be derived from the
586		/// original task's configuration, but will have a lower value for `remaining` than the
587		/// original `total_retries`.
588		#[pallet::call_index(6)]
589		#[pallet::weight(<T as Config>::WeightInfo::set_retry())]
590		pub fn set_retry(
591			origin: OriginFor<T>,
592			task: TaskAddress<BlockNumberFor<T>>,
593			retries: u8,
594			period: BlockNumberFor<T>,
595		) -> DispatchResult {
596			T::ScheduleOrigin::ensure_origin(origin.clone())?;
597			let origin = <T as Config>::RuntimeOrigin::from(origin);
598			let (when, index) = task;
599			let agenda = Agenda::<T>::get(when);
600			let scheduled = agenda
601				.get(index as usize)
602				.and_then(Option::as_ref)
603				.ok_or(Error::<T>::NotFound)?;
604			Self::ensure_privilege(origin.caller(), &scheduled.origin)?;
605			Retries::<T>::insert(
606				(when, index),
607				RetryConfig { total_retries: retries, remaining: retries, period },
608			);
609			Self::deposit_event(Event::RetrySet { task, id: None, period, retries });
610			Ok(())
611		}
612
613		/// Set a retry configuration for a named task so that, in case its scheduled run fails, it
614		/// will be retried after `period` blocks, for a total amount of `retries` retries or until
615		/// it succeeds.
616		///
617		/// Tasks which need to be scheduled for a retry are still subject to weight metering and
618		/// agenda space, same as a regular task. If a periodic task fails, it will be scheduled
619		/// normally while the task is retrying.
620		///
621		/// Tasks scheduled as a result of a retry for a periodic task are unnamed, non-periodic
622		/// clones of the original task. Their retry configuration will be derived from the
623		/// original task's configuration, but will have a lower value for `remaining` than the
624		/// original `total_retries`.
625		#[pallet::call_index(7)]
626		#[pallet::weight(<T as Config>::WeightInfo::set_retry_named())]
627		pub fn set_retry_named(
628			origin: OriginFor<T>,
629			id: TaskName,
630			retries: u8,
631			period: BlockNumberFor<T>,
632		) -> DispatchResult {
633			T::ScheduleOrigin::ensure_origin(origin.clone())?;
634			let origin = <T as Config>::RuntimeOrigin::from(origin);
635			let (when, agenda_index) = Lookup::<T>::get(&id).ok_or(Error::<T>::NotFound)?;
636			let agenda = Agenda::<T>::get(when);
637			let scheduled = agenda
638				.get(agenda_index as usize)
639				.and_then(Option::as_ref)
640				.ok_or(Error::<T>::NotFound)?;
641			Self::ensure_privilege(origin.caller(), &scheduled.origin)?;
642			Retries::<T>::insert(
643				(when, agenda_index),
644				RetryConfig { total_retries: retries, remaining: retries, period },
645			);
646			Self::deposit_event(Event::RetrySet {
647				task: (when, agenda_index),
648				id: Some(id),
649				period,
650				retries,
651			});
652			Ok(())
653		}
654
655		/// Removes the retry configuration of a task.
656		#[pallet::call_index(8)]
657		#[pallet::weight(<T as Config>::WeightInfo::cancel_retry())]
658		pub fn cancel_retry(
659			origin: OriginFor<T>,
660			task: TaskAddress<BlockNumberFor<T>>,
661		) -> DispatchResult {
662			T::ScheduleOrigin::ensure_origin(origin.clone())?;
663			let origin = <T as Config>::RuntimeOrigin::from(origin);
664			Self::do_cancel_retry(origin.caller(), task)?;
665			Self::deposit_event(Event::RetryCancelled { task, id: None });
666			Ok(())
667		}
668
669		/// Cancel the retry configuration of a named task.
670		#[pallet::call_index(9)]
671		#[pallet::weight(<T as Config>::WeightInfo::cancel_retry_named())]
672		pub fn cancel_retry_named(origin: OriginFor<T>, id: TaskName) -> DispatchResult {
673			T::ScheduleOrigin::ensure_origin(origin.clone())?;
674			let origin = <T as Config>::RuntimeOrigin::from(origin);
675			let task = Lookup::<T>::get(&id).ok_or(Error::<T>::NotFound)?;
676			Self::do_cancel_retry(origin.caller(), task)?;
677			Self::deposit_event(Event::RetryCancelled { task, id: Some(id) });
678			Ok(())
679		}
680	}
681}
682
683impl<T: Config> Pallet<T> {
684	/// Migrate storage format from V1 to V4.
685	///
686	/// Returns the weight consumed by this migration.
687	pub fn migrate_v1_to_v4() -> Weight {
688		use migration::v1 as old;
689		let mut weight = T::DbWeight::get().reads_writes(1, 1);
690
691		// Delete all undecodable values.
692		// `StorageMap::translate` is not enough since it just skips them and leaves the keys in.
693		let keys = old::Agenda::<T>::iter_keys().collect::<Vec<_>>();
694		for key in keys {
695			weight.saturating_accrue(T::DbWeight::get().reads(1));
696			if let Err(_) = old::Agenda::<T>::try_get(&key) {
697				weight.saturating_accrue(T::DbWeight::get().writes(1));
698				old::Agenda::<T>::remove(&key);
699				log::warn!("Deleted undecodable agenda");
700			}
701		}
702
703		Agenda::<T>::translate::<
704			Vec<Option<ScheduledV1<<T as Config>::RuntimeCall, BlockNumberFor<T>>>>,
705			_,
706		>(|_, agenda| {
707			Some(BoundedVec::truncate_from(
708				agenda
709					.into_iter()
710					.map(|schedule| {
711						weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
712
713						schedule.and_then(|schedule| {
714							if let Some(id) = schedule.maybe_id.as_ref() {
715								let name = blake2_256(id);
716								if let Some(item) = old::Lookup::<T>::take(id) {
717									Lookup::<T>::insert(name, item);
718								}
719								weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
720							}
721
722							let call = T::Preimages::bound(schedule.call).ok()?;
723
724							if call.lookup_needed() {
725								weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1));
726							}
727
728							Some(Scheduled {
729								maybe_id: schedule.maybe_id.map(|x| blake2_256(&x[..])),
730								priority: schedule.priority,
731								call,
732								maybe_periodic: schedule.maybe_periodic,
733								origin: system::RawOrigin::Root.into(),
734								_phantom: Default::default(),
735							})
736						})
737					})
738					.collect::<Vec<_>>(),
739			))
740		});
741
742		#[allow(deprecated)]
743		frame_support::storage::migration::remove_storage_prefix(
744			Self::name().as_bytes(),
745			b"StorageVersion",
746			&[],
747		);
748
749		StorageVersion::new(4).put::<Self>();
750
751		weight + T::DbWeight::get().writes(2)
752	}
753
754	/// Migrate storage format from V2 to V4.
755	///
756	/// Returns the weight consumed by this migration.
757	pub fn migrate_v2_to_v4() -> Weight {
758		use migration::v2 as old;
759		let mut weight = T::DbWeight::get().reads_writes(1, 1);
760
761		// Delete all undecodable values.
762		// `StorageMap::translate` is not enough since it just skips them and leaves the keys in.
763		let keys = old::Agenda::<T>::iter_keys().collect::<Vec<_>>();
764		for key in keys {
765			weight.saturating_accrue(T::DbWeight::get().reads(1));
766			if let Err(_) = old::Agenda::<T>::try_get(&key) {
767				weight.saturating_accrue(T::DbWeight::get().writes(1));
768				old::Agenda::<T>::remove(&key);
769				log::warn!("Deleted undecodable agenda");
770			}
771		}
772
773		Agenda::<T>::translate::<Vec<Option<ScheduledV2Of<T>>>, _>(|_, agenda| {
774			Some(BoundedVec::truncate_from(
775				agenda
776					.into_iter()
777					.map(|schedule| {
778						weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
779						schedule.and_then(|schedule| {
780							if let Some(id) = schedule.maybe_id.as_ref() {
781								let name = blake2_256(id);
782								if let Some(item) = old::Lookup::<T>::take(id) {
783									Lookup::<T>::insert(name, item);
784								}
785								weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
786							}
787
788							let call = T::Preimages::bound(schedule.call).ok()?;
789							if call.lookup_needed() {
790								weight.saturating_accrue(T::DbWeight::get().reads_writes(0, 1));
791							}
792
793							Some(Scheduled {
794								maybe_id: schedule.maybe_id.map(|x| blake2_256(&x[..])),
795								priority: schedule.priority,
796								call,
797								maybe_periodic: schedule.maybe_periodic,
798								origin: schedule.origin,
799								_phantom: Default::default(),
800							})
801						})
802					})
803					.collect::<Vec<_>>(),
804			))
805		});
806
807		#[allow(deprecated)]
808		frame_support::storage::migration::remove_storage_prefix(
809			Self::name().as_bytes(),
810			b"StorageVersion",
811			&[],
812		);
813
814		StorageVersion::new(4).put::<Self>();
815
816		weight + T::DbWeight::get().writes(2)
817	}
818
819	/// Migrate storage format from V3 to V4.
820	///
821	/// Returns the weight consumed by this migration.
822	#[allow(deprecated)]
823	pub fn migrate_v3_to_v4() -> Weight {
824		use migration::v3 as old;
825		let mut weight = T::DbWeight::get().reads_writes(2, 1);
826
827		// Delete all undecodable values.
828		// `StorageMap::translate` is not enough since it just skips them and leaves the keys in.
829		let blocks = old::Agenda::<T>::iter_keys().collect::<Vec<_>>();
830		for block in blocks {
831			weight.saturating_accrue(T::DbWeight::get().reads(1));
832			if let Err(_) = old::Agenda::<T>::try_get(&block) {
833				weight.saturating_accrue(T::DbWeight::get().writes(1));
834				old::Agenda::<T>::remove(&block);
835				log::warn!("Deleted undecodable agenda of block: {:?}", block);
836			}
837		}
838
839		Agenda::<T>::translate::<Vec<Option<ScheduledV3Of<T>>>, _>(|block, agenda| {
840			log::info!("Migrating agenda of block: {:?}", &block);
841			Some(BoundedVec::truncate_from(
842				agenda
843					.into_iter()
844					.map(|schedule| {
845						weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
846						schedule
847							.and_then(|schedule| {
848								if let Some(id) = schedule.maybe_id.as_ref() {
849									let name = blake2_256(id);
850									if let Some(item) = old::Lookup::<T>::take(id) {
851										Lookup::<T>::insert(name, item);
852										log::info!("Migrated name for id: {:?}", id);
853									} else {
854										log::error!("No name in Lookup for id: {:?}", &id);
855									}
856									weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
857								} else {
858									log::info!("Schedule is unnamed");
859								}
860
861								let call = match schedule.call {
862									MaybeHashed::Hash(h) => {
863										let bounded = Bounded::from_legacy_hash(h);
864										// Check that the call can be decoded in the new runtime.
865										if let Err(err) = T::Preimages::peek::<
866											<T as Config>::RuntimeCall,
867										>(&bounded)
868										{
869											log::error!(
870												"Dropping undecodable call {:?}: {:?}",
871												&h,
872												&err
873											);
874											return None
875										}
876										weight.saturating_accrue(T::DbWeight::get().reads(1));
877										log::info!("Migrated call by hash, hash: {:?}", h);
878										bounded
879									},
880									MaybeHashed::Value(v) => {
881										let call = T::Preimages::bound(v)
882											.map_err(|e| {
883												log::error!("Could not bound Call: {:?}", e)
884											})
885											.ok()?;
886										if call.lookup_needed() {
887											weight.saturating_accrue(
888												T::DbWeight::get().reads_writes(0, 1),
889											);
890										}
891										log::info!(
892											"Migrated call by value, hash: {:?}",
893											call.hash()
894										);
895										call
896									},
897								};
898
899								Some(Scheduled {
900									maybe_id: schedule.maybe_id.map(|x| blake2_256(&x[..])),
901									priority: schedule.priority,
902									call,
903									maybe_periodic: schedule.maybe_periodic,
904									origin: schedule.origin,
905									_phantom: Default::default(),
906								})
907							})
908							.or_else(|| {
909								log::info!("Schedule in agenda for block {:?} is empty - nothing to do here.", &block);
910								None
911							})
912					})
913					.collect::<Vec<_>>(),
914			))
915		});
916
917		#[allow(deprecated)]
918		frame_support::storage::migration::remove_storage_prefix(
919			Self::name().as_bytes(),
920			b"StorageVersion",
921			&[],
922		);
923
924		StorageVersion::new(4).put::<Self>();
925
926		weight + T::DbWeight::get().writes(2)
927	}
928}
929
930impl<T: Config> Pallet<T> {
931	/// Helper to migrate scheduler when the pallet origin type has changed.
932	pub fn migrate_origin<OldOrigin: Into<T::PalletsOrigin> + codec::Decode>() {
933		Agenda::<T>::translate::<
934			Vec<
935				Option<
936					Scheduled<
937						TaskName,
938						BoundedCallOf<T>,
939						BlockNumberFor<T>,
940						OldOrigin,
941						T::AccountId,
942					>,
943				>,
944			>,
945			_,
946		>(|_, agenda| {
947			Some(BoundedVec::truncate_from(
948				agenda
949					.into_iter()
950					.map(|schedule| {
951						schedule.map(|schedule| Scheduled {
952							maybe_id: schedule.maybe_id,
953							priority: schedule.priority,
954							call: schedule.call,
955							maybe_periodic: schedule.maybe_periodic,
956							origin: schedule.origin.into(),
957							_phantom: Default::default(),
958						})
959					})
960					.collect::<Vec<_>>(),
961			))
962		});
963	}
964
965	fn resolve_time(
966		when: DispatchTime<BlockNumberFor<T>>,
967	) -> Result<BlockNumberFor<T>, DispatchError> {
968		let now = T::BlockNumberProvider::current_block_number();
969		let when = match when {
970			DispatchTime::At(x) => x,
971			// The current block has already completed it's scheduled tasks, so
972			// Schedule the task at lest one block after this current block.
973			DispatchTime::After(x) => now.saturating_add(x).saturating_add(One::one()),
974		};
975
976		if when <= now {
977			return Err(Error::<T>::TargetBlockNumberInPast.into())
978		}
979
980		Ok(when)
981	}
982
983	fn place_task(
984		when: BlockNumberFor<T>,
985		what: ScheduledOf<T>,
986	) -> Result<TaskAddress<BlockNumberFor<T>>, (DispatchError, ScheduledOf<T>)> {
987		let maybe_name = what.maybe_id;
988		let index = Self::push_to_agenda(when, what)?;
989		let address = (when, index);
990		if let Some(name) = maybe_name {
991			Lookup::<T>::insert(name, address)
992		}
993		Self::deposit_event(Event::Scheduled { when: address.0, index: address.1 });
994		Ok(address)
995	}
996
997	fn push_to_agenda(
998		when: BlockNumberFor<T>,
999		what: ScheduledOf<T>,
1000	) -> Result<u32, (DispatchError, ScheduledOf<T>)> {
1001		let mut agenda = Agenda::<T>::get(when);
1002		let index = if (agenda.len() as u32) < T::MaxScheduledPerBlock::get() {
1003			// will always succeed due to the above check.
1004			let _ = agenda.try_push(Some(what));
1005			agenda.len() as u32 - 1
1006		} else {
1007			if let Some(hole_index) = agenda.iter().position(|i| i.is_none()) {
1008				agenda[hole_index] = Some(what);
1009				hole_index as u32
1010			} else {
1011				return Err((DispatchError::Exhausted, what))
1012			}
1013		};
1014		Agenda::<T>::insert(when, agenda);
1015		Ok(index)
1016	}
1017
1018	/// Remove trailing `None` items of an agenda at `when`. If all items are `None` remove the
1019	/// agenda record entirely.
1020	fn cleanup_agenda(when: BlockNumberFor<T>) {
1021		let mut agenda = Agenda::<T>::get(when);
1022		match agenda.iter().rposition(|i| i.is_some()) {
1023			Some(i) if agenda.len() > i + 1 => {
1024				agenda.truncate(i + 1);
1025				Agenda::<T>::insert(when, agenda);
1026			},
1027			Some(_) => {},
1028			None => {
1029				Agenda::<T>::remove(when);
1030			},
1031		}
1032	}
1033
1034	fn do_schedule(
1035		when: DispatchTime<BlockNumberFor<T>>,
1036		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1037		priority: schedule::Priority,
1038		origin: T::PalletsOrigin,
1039		call: BoundedCallOf<T>,
1040	) -> Result<TaskAddress<BlockNumberFor<T>>, DispatchError> {
1041		let when = Self::resolve_time(when)?;
1042
1043		let lookup_hash = call.lookup_hash();
1044
1045		// sanitize maybe_periodic
1046		let maybe_periodic = maybe_periodic
1047			.filter(|p| p.1 > 1 && !p.0.is_zero())
1048			// Remove one from the number of repetitions since we will schedule one now.
1049			.map(|(p, c)| (p, c - 1));
1050		let task = Scheduled {
1051			maybe_id: None,
1052			priority,
1053			call,
1054			maybe_periodic,
1055			origin,
1056			_phantom: PhantomData,
1057		};
1058		let res = Self::place_task(when, task).map_err(|x| x.0)?;
1059
1060		if let Some(hash) = lookup_hash {
1061			// Request the call to be made available.
1062			T::Preimages::request(&hash);
1063		}
1064
1065		Ok(res)
1066	}
1067
1068	fn do_cancel(
1069		origin: Option<T::PalletsOrigin>,
1070		(when, index): TaskAddress<BlockNumberFor<T>>,
1071	) -> Result<(), DispatchError> {
1072		let scheduled = Agenda::<T>::try_mutate(when, |agenda| {
1073			agenda.get_mut(index as usize).map_or(
1074				Ok(None),
1075				|s| -> Result<Option<Scheduled<_, _, _, _, _>>, DispatchError> {
1076					if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) {
1077						Self::ensure_privilege(o, &s.origin)?;
1078					};
1079					Ok(s.take())
1080				},
1081			)
1082		})?;
1083		if let Some(s) = scheduled {
1084			T::Preimages::drop(&s.call);
1085			if let Some(id) = s.maybe_id {
1086				Lookup::<T>::remove(id);
1087			}
1088			Retries::<T>::remove((when, index));
1089			Self::cleanup_agenda(when);
1090			Self::deposit_event(Event::Canceled { when, index });
1091			Ok(())
1092		} else {
1093			return Err(Error::<T>::NotFound.into())
1094		}
1095	}
1096
1097	fn do_reschedule(
1098		(when, index): TaskAddress<BlockNumberFor<T>>,
1099		new_time: DispatchTime<BlockNumberFor<T>>,
1100	) -> Result<TaskAddress<BlockNumberFor<T>>, DispatchError> {
1101		let new_time = Self::resolve_time(new_time)?;
1102
1103		if new_time == when {
1104			return Err(Error::<T>::RescheduleNoChange.into())
1105		}
1106
1107		let task = Agenda::<T>::try_mutate(when, |agenda| {
1108			let task = agenda.get_mut(index as usize).ok_or(Error::<T>::NotFound)?;
1109			ensure!(!matches!(task, Some(Scheduled { maybe_id: Some(_), .. })), Error::<T>::Named);
1110			task.take().ok_or(Error::<T>::NotFound)
1111		})?;
1112		Self::cleanup_agenda(when);
1113		Self::deposit_event(Event::Canceled { when, index });
1114
1115		Self::place_task(new_time, task).map_err(|x| x.0)
1116	}
1117
1118	fn do_schedule_named(
1119		id: TaskName,
1120		when: DispatchTime<BlockNumberFor<T>>,
1121		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1122		priority: schedule::Priority,
1123		origin: T::PalletsOrigin,
1124		call: BoundedCallOf<T>,
1125	) -> Result<TaskAddress<BlockNumberFor<T>>, DispatchError> {
1126		// ensure id it is unique
1127		if Lookup::<T>::contains_key(&id) {
1128			return Err(Error::<T>::FailedToSchedule.into())
1129		}
1130
1131		let when = Self::resolve_time(when)?;
1132
1133		let lookup_hash = call.lookup_hash();
1134
1135		// sanitize maybe_periodic
1136		let maybe_periodic = maybe_periodic
1137			.filter(|p| p.1 > 1 && !p.0.is_zero())
1138			// Remove one from the number of repetitions since we will schedule one now.
1139			.map(|(p, c)| (p, c - 1));
1140
1141		let task = Scheduled {
1142			maybe_id: Some(id),
1143			priority,
1144			call,
1145			maybe_periodic,
1146			origin,
1147			_phantom: Default::default(),
1148		};
1149		let res = Self::place_task(when, task).map_err(|x| x.0)?;
1150
1151		if let Some(hash) = lookup_hash {
1152			// Request the call to be made available.
1153			T::Preimages::request(&hash);
1154		}
1155
1156		Ok(res)
1157	}
1158
1159	fn do_cancel_named(origin: Option<T::PalletsOrigin>, id: TaskName) -> DispatchResult {
1160		Lookup::<T>::try_mutate_exists(id, |lookup| -> DispatchResult {
1161			if let Some((when, index)) = lookup.take() {
1162				let i = index as usize;
1163				Agenda::<T>::try_mutate(when, |agenda| -> DispatchResult {
1164					if let Some(s) = agenda.get_mut(i) {
1165						if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) {
1166							Self::ensure_privilege(o, &s.origin)?;
1167							Retries::<T>::remove((when, index));
1168							T::Preimages::drop(&s.call);
1169						}
1170						*s = None;
1171					}
1172					Ok(())
1173				})?;
1174				Self::cleanup_agenda(when);
1175				Self::deposit_event(Event::Canceled { when, index });
1176				Ok(())
1177			} else {
1178				return Err(Error::<T>::NotFound.into())
1179			}
1180		})
1181	}
1182
1183	fn do_reschedule_named(
1184		id: TaskName,
1185		new_time: DispatchTime<BlockNumberFor<T>>,
1186	) -> Result<TaskAddress<BlockNumberFor<T>>, DispatchError> {
1187		let new_time = Self::resolve_time(new_time)?;
1188
1189		let lookup = Lookup::<T>::get(id);
1190		let (when, index) = lookup.ok_or(Error::<T>::NotFound)?;
1191
1192		if new_time == when {
1193			return Err(Error::<T>::RescheduleNoChange.into())
1194		}
1195
1196		let task = Agenda::<T>::try_mutate(when, |agenda| {
1197			let task = agenda.get_mut(index as usize).ok_or(Error::<T>::NotFound)?;
1198			task.take().ok_or(Error::<T>::NotFound)
1199		})?;
1200		Self::cleanup_agenda(when);
1201		Self::deposit_event(Event::Canceled { when, index });
1202		Self::place_task(new_time, task).map_err(|x| x.0)
1203	}
1204
1205	fn do_cancel_retry(
1206		origin: &T::PalletsOrigin,
1207		(when, index): TaskAddress<BlockNumberFor<T>>,
1208	) -> Result<(), DispatchError> {
1209		let agenda = Agenda::<T>::get(when);
1210		let scheduled = agenda
1211			.get(index as usize)
1212			.and_then(Option::as_ref)
1213			.ok_or(Error::<T>::NotFound)?;
1214		Self::ensure_privilege(origin, &scheduled.origin)?;
1215		Retries::<T>::remove((when, index));
1216		Ok(())
1217	}
1218}
1219
1220enum ServiceTaskError {
1221	/// Could not be executed due to missing preimage.
1222	Unavailable,
1223	/// Could not be executed due to weight limitations.
1224	Overweight,
1225}
1226use ServiceTaskError::*;
1227
1228impl<T: Config> Pallet<T> {
1229	/// Service up to `max` agendas queue starting from earliest incompletely executed agenda.
1230	fn service_agendas(weight: &mut WeightMeter, now: BlockNumberFor<T>, max: u32) {
1231		if weight.try_consume(T::WeightInfo::service_agendas_base()).is_err() {
1232			return
1233		}
1234
1235		let mut incomplete_since = now + One::one();
1236		let mut when = IncompleteSince::<T>::take().unwrap_or(now);
1237		let mut is_first = true; // first task from the first agenda.
1238
1239		let max_items = T::MaxScheduledPerBlock::get();
1240		let mut count_down = max;
1241		let service_agenda_base_weight = T::WeightInfo::service_agenda_base(max_items);
1242		while count_down > 0 && when <= now && weight.can_consume(service_agenda_base_weight) {
1243			if !Self::service_agenda(weight, is_first, now, when, u32::MAX) {
1244				incomplete_since = incomplete_since.min(when);
1245			}
1246			is_first = false;
1247			when.saturating_inc();
1248			count_down.saturating_dec();
1249		}
1250		incomplete_since = incomplete_since.min(when);
1251		if incomplete_since <= now {
1252			Self::deposit_event(Event::AgendaIncomplete { when: incomplete_since });
1253			IncompleteSince::<T>::put(incomplete_since);
1254		} else {
1255			// The next scheduler iteration should typically start from `now + 1` (`next_iter_now`).
1256			// However, if the [`Config::BlockNumberProvider`] is not a local block number provider,
1257			// then `next_iter_now` could be `now + n` where `n > 1`. In this case, we want to start
1258			// from `now + 1` to ensure we don't miss any agendas.
1259			IncompleteSince::<T>::put(now + One::one());
1260		}
1261	}
1262
1263	/// Returns `true` if the agenda was fully completed, `false` if it should be revisited at a
1264	/// later block.
1265	fn service_agenda(
1266		weight: &mut WeightMeter,
1267		mut is_first: bool,
1268		now: BlockNumberFor<T>,
1269		when: BlockNumberFor<T>,
1270		max: u32,
1271	) -> bool {
1272		let mut agenda = Agenda::<T>::get(when);
1273		let mut ordered = agenda
1274			.iter()
1275			.enumerate()
1276			.filter_map(|(index, maybe_item)| {
1277				maybe_item.as_ref().map(|item| (index as u32, item.priority))
1278			})
1279			.collect::<Vec<_>>();
1280		ordered.sort_by_key(|k| k.1);
1281		let within_limit = weight
1282			.try_consume(T::WeightInfo::service_agenda_base(ordered.len() as u32))
1283			.is_ok();
1284		debug_assert!(within_limit, "weight limit should have been checked in advance");
1285
1286		// Items which we know can be executed and have postponed for execution in a later block.
1287		let mut postponed = (ordered.len() as u32).saturating_sub(max);
1288		// Items which we don't know can ever be executed.
1289		let mut dropped = 0;
1290
1291		for (agenda_index, _) in ordered.into_iter().take(max as usize) {
1292			let Some(task) = agenda[agenda_index as usize].take() else { continue };
1293			let base_weight = T::WeightInfo::service_task(
1294				task.call.lookup_len().map(|x| x as usize),
1295				task.maybe_id.is_some(),
1296				task.maybe_periodic.is_some(),
1297			);
1298			if !weight.can_consume(base_weight) {
1299				postponed += 1;
1300				agenda[agenda_index as usize] = Some(task);
1301				break
1302			}
1303			let result = Self::service_task(weight, now, when, agenda_index, is_first, task);
1304			agenda[agenda_index as usize] = match result {
1305				Err((Unavailable, slot)) => {
1306					dropped += 1;
1307					slot
1308				},
1309				Err((Overweight, slot)) => {
1310					postponed += 1;
1311					slot
1312				},
1313				Ok(()) => {
1314					is_first = false;
1315					None
1316				},
1317			};
1318		}
1319		if postponed > 0 || dropped > 0 {
1320			Agenda::<T>::insert(when, agenda);
1321		} else {
1322			Agenda::<T>::remove(when);
1323		}
1324
1325		postponed == 0
1326	}
1327
1328	/// Service (i.e. execute) the given task, being careful not to overflow the `weight` counter.
1329	///
1330	/// This involves:
1331	/// - removing and potentially replacing the `Lookup` entry for the task.
1332	/// - realizing the task's call which can include a preimage lookup.
1333	/// - Rescheduling the task for execution in a later agenda if periodic.
1334	fn service_task(
1335		weight: &mut WeightMeter,
1336		now: BlockNumberFor<T>,
1337		when: BlockNumberFor<T>,
1338		agenda_index: u32,
1339		is_first: bool,
1340		mut task: ScheduledOf<T>,
1341	) -> Result<(), (ServiceTaskError, Option<ScheduledOf<T>>)> {
1342		if let Some(ref id) = task.maybe_id {
1343			Lookup::<T>::remove(id);
1344		}
1345
1346		let (call, lookup_len) = match T::Preimages::peek(&task.call) {
1347			Ok(c) => c,
1348			Err(_) => {
1349				Self::deposit_event(Event::CallUnavailable {
1350					task: (when, agenda_index),
1351					id: task.maybe_id,
1352				});
1353
1354				// It was not available when we needed it, so we don't need to have requested it
1355				// anymore.
1356				T::Preimages::drop(&task.call);
1357
1358				// We don't know why `peek` failed, thus we most account here for the "full weight".
1359				let _ = weight.try_consume(T::WeightInfo::service_task(
1360					task.call.lookup_len().map(|x| x as usize),
1361					task.maybe_id.is_some(),
1362					task.maybe_periodic.is_some(),
1363				));
1364
1365				return Err((Unavailable, Some(task)))
1366			},
1367		};
1368
1369		let _ = weight.try_consume(T::WeightInfo::service_task(
1370			lookup_len.map(|x| x as usize),
1371			task.maybe_id.is_some(),
1372			task.maybe_periodic.is_some(),
1373		));
1374
1375		match Self::execute_dispatch(weight, task.origin.clone(), call) {
1376			Err(()) if is_first => {
1377				T::Preimages::drop(&task.call);
1378				Self::deposit_event(Event::PermanentlyOverweight {
1379					task: (when, agenda_index),
1380					id: task.maybe_id,
1381				});
1382				Err((Unavailable, Some(task)))
1383			},
1384			Err(()) => Err((Overweight, Some(task))),
1385			Ok(result) => {
1386				let failed = result.is_err();
1387				let maybe_retry_config = Retries::<T>::take((when, agenda_index));
1388				Self::deposit_event(Event::Dispatched {
1389					task: (when, agenda_index),
1390					id: task.maybe_id,
1391					result,
1392				});
1393
1394				match maybe_retry_config {
1395					Some(retry_config) if failed => {
1396						Self::schedule_retry(weight, now, when, agenda_index, &task, retry_config);
1397					},
1398					_ => {},
1399				}
1400
1401				if let &Some((period, count)) = &task.maybe_periodic {
1402					if count > 1 {
1403						task.maybe_periodic = Some((period, count - 1));
1404					} else {
1405						task.maybe_periodic = None;
1406					}
1407					let wake = now.saturating_add(period);
1408					match Self::place_task(wake, task) {
1409						Ok(new_address) =>
1410							if let Some(retry_config) = maybe_retry_config {
1411								Retries::<T>::insert(new_address, retry_config);
1412							},
1413						Err((_, task)) => {
1414							// TODO: Leave task in storage somewhere for it to be rescheduled
1415							// manually.
1416							T::Preimages::drop(&task.call);
1417							Self::deposit_event(Event::PeriodicFailed {
1418								task: (when, agenda_index),
1419								id: task.maybe_id,
1420							});
1421						},
1422					}
1423				} else {
1424					T::Preimages::drop(&task.call);
1425				}
1426				Ok(())
1427			},
1428		}
1429	}
1430
1431	/// Make a dispatch to the given `call` from the given `origin`, ensuring that the `weight`
1432	/// counter does not exceed its limit and that it is counted accurately (e.g. accounted using
1433	/// post info if available).
1434	///
1435	/// NOTE: Only the weight for this function will be counted (origin lookup, dispatch and the
1436	/// call itself).
1437	///
1438	/// Returns an error if the call is overweight.
1439	fn execute_dispatch(
1440		weight: &mut WeightMeter,
1441		origin: T::PalletsOrigin,
1442		call: <T as Config>::RuntimeCall,
1443	) -> Result<DispatchResult, ()> {
1444		let base_weight = match origin.as_system_ref() {
1445			Some(&RawOrigin::Signed(_)) => T::WeightInfo::execute_dispatch_signed(),
1446			_ => T::WeightInfo::execute_dispatch_unsigned(),
1447		};
1448		let call_weight = call.get_dispatch_info().call_weight;
1449		// We only allow a scheduled call if it cannot push the weight past the limit.
1450		let max_weight = base_weight.saturating_add(call_weight);
1451
1452		if !weight.can_consume(max_weight) {
1453			return Err(())
1454		}
1455
1456		let dispatch_origin = origin.into();
1457		let (maybe_actual_call_weight, result) = match call.dispatch(dispatch_origin) {
1458			Ok(post_info) => (post_info.actual_weight, Ok(())),
1459			Err(error_and_info) =>
1460				(error_and_info.post_info.actual_weight, Err(error_and_info.error)),
1461		};
1462		let call_weight = maybe_actual_call_weight.unwrap_or(call_weight);
1463		let _ = weight.try_consume(base_weight);
1464		let _ = weight.try_consume(call_weight);
1465		Ok(result)
1466	}
1467
1468	/// Check if a task has a retry configuration in place and, if so, try to reschedule it.
1469	///
1470	/// Possible causes for failure to schedule a retry for a task:
1471	/// - there wasn't enough weight to run the task reschedule logic
1472	/// - there was no retry configuration in place
1473	/// - there were no more retry attempts left
1474	/// - the agenda was full.
1475	fn schedule_retry(
1476		weight: &mut WeightMeter,
1477		now: BlockNumberFor<T>,
1478		when: BlockNumberFor<T>,
1479		agenda_index: u32,
1480		task: &ScheduledOf<T>,
1481		retry_config: RetryConfig<BlockNumberFor<T>>,
1482	) {
1483		if weight
1484			.try_consume(T::WeightInfo::schedule_retry(T::MaxScheduledPerBlock::get()))
1485			.is_err()
1486		{
1487			Self::deposit_event(Event::RetryFailed {
1488				task: (when, agenda_index),
1489				id: task.maybe_id,
1490			});
1491			return;
1492		}
1493
1494		let RetryConfig { total_retries, mut remaining, period } = retry_config;
1495		remaining = match remaining.checked_sub(1) {
1496			Some(n) => n,
1497			None => return,
1498		};
1499		let wake = now.saturating_add(period);
1500		match Self::place_task(wake, task.as_retry()) {
1501			Ok(address) => {
1502				// Reinsert the retry config to the new address of the task after it was
1503				// placed.
1504				Retries::<T>::insert(address, RetryConfig { total_retries, remaining, period });
1505			},
1506			Err((_, task)) => {
1507				// TODO: Leave task in storage somewhere for it to be
1508				// rescheduled manually.
1509				T::Preimages::drop(&task.call);
1510				Self::deposit_event(Event::RetryFailed {
1511					task: (when, agenda_index),
1512					id: task.maybe_id,
1513				});
1514			},
1515		}
1516	}
1517
1518	/// Ensure that `left` has at least the same level of privilege or higher than `right`.
1519	///
1520	/// Returns an error if `left` has a lower level of privilege or the two cannot be compared.
1521	fn ensure_privilege(
1522		left: &<T as Config>::PalletsOrigin,
1523		right: &<T as Config>::PalletsOrigin,
1524	) -> Result<(), DispatchError> {
1525		if matches!(T::OriginPrivilegeCmp::cmp_privilege(left, right), Some(Ordering::Less) | None)
1526		{
1527			return Err(BadOrigin.into());
1528		}
1529		Ok(())
1530	}
1531}
1532
1533#[allow(deprecated)]
1534impl<T: Config> schedule::v2::Anon<BlockNumberFor<T>, <T as Config>::RuntimeCall, T::PalletsOrigin>
1535	for Pallet<T>
1536{
1537	type Address = TaskAddress<BlockNumberFor<T>>;
1538	type Hash = T::Hash;
1539
1540	fn schedule(
1541		when: DispatchTime<BlockNumberFor<T>>,
1542		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1543		priority: schedule::Priority,
1544		origin: T::PalletsOrigin,
1545		call: CallOrHashOf<T>,
1546	) -> Result<Self::Address, DispatchError> {
1547		let call = call.as_value().ok_or(DispatchError::CannotLookup)?;
1548		let call = T::Preimages::bound(call)?.transmute();
1549		Self::do_schedule(when, maybe_periodic, priority, origin, call)
1550	}
1551
1552	fn cancel((when, index): Self::Address) -> Result<(), ()> {
1553		Self::do_cancel(None, (when, index)).map_err(|_| ())
1554	}
1555
1556	fn reschedule(
1557		address: Self::Address,
1558		when: DispatchTime<BlockNumberFor<T>>,
1559	) -> Result<Self::Address, DispatchError> {
1560		Self::do_reschedule(address, when)
1561	}
1562
1563	fn next_dispatch_time((when, index): Self::Address) -> Result<BlockNumberFor<T>, ()> {
1564		Agenda::<T>::get(when).get(index as usize).ok_or(()).map(|_| when)
1565	}
1566}
1567
1568// TODO: migrate `schedule::v2::Anon` to `v3`
1569#[allow(deprecated)]
1570impl<T: Config> schedule::v2::Named<BlockNumberFor<T>, <T as Config>::RuntimeCall, T::PalletsOrigin>
1571	for Pallet<T>
1572{
1573	type Address = TaskAddress<BlockNumberFor<T>>;
1574	type Hash = T::Hash;
1575
1576	fn schedule_named(
1577		id: Vec<u8>,
1578		when: DispatchTime<BlockNumberFor<T>>,
1579		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1580		priority: schedule::Priority,
1581		origin: T::PalletsOrigin,
1582		call: CallOrHashOf<T>,
1583	) -> Result<Self::Address, ()> {
1584		let call = call.as_value().ok_or(())?;
1585		let call = T::Preimages::bound(call).map_err(|_| ())?.transmute();
1586		let name = blake2_256(&id[..]);
1587		Self::do_schedule_named(name, when, maybe_periodic, priority, origin, call).map_err(|_| ())
1588	}
1589
1590	fn cancel_named(id: Vec<u8>) -> Result<(), ()> {
1591		let name = blake2_256(&id[..]);
1592		Self::do_cancel_named(None, name).map_err(|_| ())
1593	}
1594
1595	fn reschedule_named(
1596		id: Vec<u8>,
1597		when: DispatchTime<BlockNumberFor<T>>,
1598	) -> Result<Self::Address, DispatchError> {
1599		let name = blake2_256(&id[..]);
1600		Self::do_reschedule_named(name, when)
1601	}
1602
1603	fn next_dispatch_time(id: Vec<u8>) -> Result<BlockNumberFor<T>, ()> {
1604		let name = blake2_256(&id[..]);
1605		Lookup::<T>::get(name)
1606			.and_then(|(when, index)| Agenda::<T>::get(when).get(index as usize).map(|_| when))
1607			.ok_or(())
1608	}
1609}
1610
1611impl<T: Config> schedule::v3::Anon<BlockNumberFor<T>, <T as Config>::RuntimeCall, T::PalletsOrigin>
1612	for Pallet<T>
1613{
1614	type Address = TaskAddress<BlockNumberFor<T>>;
1615	type Hasher = T::Hashing;
1616
1617	fn schedule(
1618		when: DispatchTime<BlockNumberFor<T>>,
1619		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1620		priority: schedule::Priority,
1621		origin: T::PalletsOrigin,
1622		call: BoundedCallOf<T>,
1623	) -> Result<Self::Address, DispatchError> {
1624		Self::do_schedule(when, maybe_periodic, priority, origin, call)
1625	}
1626
1627	fn cancel((when, index): Self::Address) -> Result<(), DispatchError> {
1628		Self::do_cancel(None, (when, index)).map_err(map_err_to_v3_err::<T>)
1629	}
1630
1631	fn reschedule(
1632		address: Self::Address,
1633		when: DispatchTime<BlockNumberFor<T>>,
1634	) -> Result<Self::Address, DispatchError> {
1635		Self::do_reschedule(address, when).map_err(map_err_to_v3_err::<T>)
1636	}
1637
1638	fn next_dispatch_time(
1639		(when, index): Self::Address,
1640	) -> Result<BlockNumberFor<T>, DispatchError> {
1641		Agenda::<T>::get(when)
1642			.get(index as usize)
1643			.ok_or(DispatchError::Unavailable)
1644			.map(|_| when)
1645	}
1646}
1647
1648use schedule::v3::TaskName;
1649
1650impl<T: Config> schedule::v3::Named<BlockNumberFor<T>, <T as Config>::RuntimeCall, T::PalletsOrigin>
1651	for Pallet<T>
1652{
1653	type Address = TaskAddress<BlockNumberFor<T>>;
1654	type Hasher = T::Hashing;
1655
1656	fn schedule_named(
1657		id: TaskName,
1658		when: DispatchTime<BlockNumberFor<T>>,
1659		maybe_periodic: Option<schedule::Period<BlockNumberFor<T>>>,
1660		priority: schedule::Priority,
1661		origin: T::PalletsOrigin,
1662		call: BoundedCallOf<T>,
1663	) -> Result<Self::Address, DispatchError> {
1664		Self::do_schedule_named(id, when, maybe_periodic, priority, origin, call)
1665	}
1666
1667	fn cancel_named(id: TaskName) -> Result<(), DispatchError> {
1668		Self::do_cancel_named(None, id).map_err(map_err_to_v3_err::<T>)
1669	}
1670
1671	fn reschedule_named(
1672		id: TaskName,
1673		when: DispatchTime<BlockNumberFor<T>>,
1674	) -> Result<Self::Address, DispatchError> {
1675		Self::do_reschedule_named(id, when).map_err(map_err_to_v3_err::<T>)
1676	}
1677
1678	fn next_dispatch_time(id: TaskName) -> Result<BlockNumberFor<T>, DispatchError> {
1679		Lookup::<T>::get(id)
1680			.and_then(|(when, index)| Agenda::<T>::get(when).get(index as usize).map(|_| when))
1681			.ok_or(DispatchError::Unavailable)
1682	}
1683}
1684
1685/// Maps a pallet error to an `schedule::v3` error.
1686fn map_err_to_v3_err<T: Config>(err: DispatchError) -> DispatchError {
1687	if err == DispatchError::from(Error::<T>::NotFound) {
1688		DispatchError::Unavailable
1689	} else {
1690		err
1691	}
1692}