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