Tasks: general system for recognizing and executing service work (#1343)

`polkadot-sdk` version of original tasks PR located here:
https://github.com/paritytech/substrate/pull/14329

Fixes #206

## Status
- [x] Generic `Task` trait
- [x] `RuntimeTask` aggregated enum, compatible with
`construct_runtime!`
- [x] Casting between `Task` and `RuntimeTask` without needing `dyn` or
`Box`
- [x] Tasks Example pallet
- [x] Runtime tests for Tasks example pallet
- [x] Parsing for task-related macros
- [x] Retrofit parsing to make macros optional
- [x] Expansion for task-related macros
- [x] Adds support for args in tasks
- [x] Retrofit tasks example pallet to use macros instead of manual
syntax
- [x] Weights
- [x] Cleanup
- [x] UI tests
- [x] Docs

## Target Syntax
Adapted from
https://github.com/paritytech/polkadot-sdk/issues/206#issue-1865172283

```rust
// NOTE: this enum is optional and is auto-generated by the other macros if not present
#[pallet::task]
pub enum Task<T: Config> {
    AddNumberIntoTotal {
        i: u32,
    }
}

/// Some running total.
#[pallet::storage]
pub(super) type Total<T: Config<I>, I: 'static = ()> =
StorageValue<_, (u32, u32), ValueQuery>;

/// Numbers to be added into the total.
#[pallet::storage]
pub(super) type Numbers<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, u32, u32, OptionQuery>;

#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
	/// Add a pair of numbers into the totals and remove them.
	#[pallet::task_list(Numbers::<T, I>::iter_keys())]
	#[pallet::task_condition(|i| Numbers::<T, I>::contains_key(i))]
	#[pallet::task_index(0)]
	pub fn add_number_into_total(i: u32) -> DispatchResult {
		let v = Numbers::<T, I>::take(i).ok_or(Error::<T, I>::NotFound)?;
		Total::<T, I>::mutate(|(total_keys, total_values)| {
			*total_keys += i;
			*total_values += v;
		});
		Ok(())
	}
}
```

---------

Co-authored-by: Nikhil Gupta <17176722+gupnik@users.noreply.github.com>
Co-authored-by: kianenigma <kian@parity.io>
Co-authored-by: Nikhil Gupta <>
Co-authored-by: Gavin Wood <gavin@parity.io>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: gupnik <nikhilgupta.iitk@gmail.com>
This commit is contained in:
Sam Johnson
2023-12-08 00:40:26 -05:00
committed by GitHub
parent 34c991e2cf
commit ac3f14d23b
75 changed files with 3516 additions and 24 deletions
@@ -31,6 +31,7 @@ mod origin;
mod pallet_struct;
mod storage;
mod store_trait;
mod tasks;
mod tt_default_parts;
mod type_value;
mod validate_unsigned;
@@ -60,6 +61,7 @@ pub fn expand(mut def: Def) -> proc_macro2::TokenStream {
let pallet_struct = pallet_struct::expand_pallet_struct(&mut def);
let config = config::expand_config(&mut def);
let call = call::expand_call(&mut def);
let tasks = tasks::expand_tasks(&mut def);
let error = error::expand_error(&mut def);
let event = event::expand_event(&mut def);
let storages = storage::expand_storages(&mut def);
@@ -100,6 +102,7 @@ storage item. Otherwise, all storage items are listed among [*Type Definitions*]
#pallet_struct
#config
#call
#tasks
#error
#event
#storages
@@ -0,0 +1,267 @@
//! Contains logic for expanding task-related items.
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Home of the expansion code for the Tasks API
use crate::pallet::{parse::tasks::*, Def};
use derive_syn_parse::Parse;
use inflector::Inflector;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, ToTokens};
use syn::{parse_quote, spanned::Spanned, ItemEnum, ItemImpl};
impl TaskEnumDef {
/// Since we optionally allow users to manually specify a `#[pallet::task_enum]`, in the
/// event they _don't_ specify one (which is actually the most common behavior) we have to
/// generate one based on the existing [`TasksDef`]. This method performs that generation.
pub fn generate(
tasks: &TasksDef,
type_decl_bounded_generics: TokenStream2,
type_use_generics: TokenStream2,
) -> Self {
let variants = if tasks.tasks_attr.is_some() {
tasks
.tasks
.iter()
.map(|task| {
let ident = &task.item.sig.ident;
let ident =
format_ident!("{}", ident.to_string().to_class_case(), span = ident.span());
let args = task.item.sig.inputs.iter().collect::<Vec<_>>();
if args.is_empty() {
quote!(#ident)
} else {
quote!(#ident {
#(#args),*
})
}
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
let mut task_enum_def: TaskEnumDef = parse_quote! {
/// Auto-generated enum that encapsulates all tasks defined by this pallet.
///
/// Conceptually similar to the [`Call`] enum, but for tasks. This is only
/// generated if there are tasks present in this pallet.
#[pallet::task_enum]
pub enum Task<#type_decl_bounded_generics> {
#(
#variants,
)*
}
};
task_enum_def.type_use_generics = type_use_generics;
task_enum_def
}
}
impl ToTokens for TaskEnumDef {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let item_enum = &self.item_enum;
let ident = &item_enum.ident;
let vis = &item_enum.vis;
let attrs = &item_enum.attrs;
let generics = &item_enum.generics;
let variants = &item_enum.variants;
let scrate = &self.scrate;
let type_use_generics = &self.type_use_generics;
if self.attr.is_some() {
// `item_enum` is short-hand / generated enum
tokens.extend(quote! {
#(#attrs)*
#[derive(
#scrate::CloneNoBound,
#scrate::EqNoBound,
#scrate::PartialEqNoBound,
#scrate::pallet_prelude::Encode,
#scrate::pallet_prelude::Decode,
#scrate::pallet_prelude::TypeInfo,
)]
#[codec(encode_bound())]
#[codec(decode_bound())]
#[scale_info(skip_type_params(#type_use_generics))]
#vis enum #ident #generics {
#variants
#[doc(hidden)]
#[codec(skip)]
__Ignore(core::marker::PhantomData<T>, #scrate::Never),
}
impl<T: Config> core::fmt::Debug for #ident<#type_use_generics> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct(stringify!(#ident)).field("value", self).finish()
}
}
});
} else {
// `item_enum` is a manually specified enum (no attribute)
tokens.extend(item_enum.to_token_stream());
}
}
}
/// Represents an already-expanded [`TasksDef`].
#[derive(Parse)]
pub struct ExpandedTasksDef {
pub task_item_impl: ItemImpl,
pub task_trait_impl: ItemImpl,
}
impl ToTokens for TasksDef {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let scrate = &self.scrate;
let enum_ident = syn::Ident::new("Task", self.enum_ident.span());
let enum_arguments = &self.enum_arguments;
let enum_use = quote!(#enum_ident #enum_arguments);
let task_fn_idents = self
.tasks
.iter()
.map(|task| {
format_ident!(
"{}",
&task.item.sig.ident.to_string().to_class_case(),
span = task.item.sig.ident.span()
)
})
.collect::<Vec<_>>();
let task_indices = self.tasks.iter().map(|task| &task.index_attr.meta.index);
let task_conditions = self.tasks.iter().map(|task| &task.condition_attr.meta.expr);
let task_weights = self.tasks.iter().map(|task| &task.weight_attr.meta.expr);
let task_iters = self.tasks.iter().map(|task| &task.list_attr.meta.expr);
let task_fn_impls = self.tasks.iter().map(|task| {
let mut task_fn_impl = task.item.clone();
task_fn_impl.attrs = vec![];
task_fn_impl
});
let task_fn_names = self.tasks.iter().map(|task| &task.item.sig.ident);
let task_arg_names = self.tasks.iter().map(|task| &task.arg_names).collect::<Vec<_>>();
let sp_std = quote!(#scrate::__private::sp_std);
let impl_generics = &self.item_impl.generics;
tokens.extend(quote! {
impl #impl_generics #enum_use
{
#(#task_fn_impls)*
}
impl #impl_generics #scrate::traits::Task for #enum_use
{
type Enumeration = #sp_std::vec::IntoIter<#enum_use>;
fn iter() -> Self::Enumeration {
let mut all_tasks = #sp_std::vec![];
#(all_tasks
.extend(#task_iters.map(|(#(#task_arg_names),*)| #enum_ident::#task_fn_idents { #(#task_arg_names: #task_arg_names.clone()),* })
.collect::<#sp_std::vec::Vec<_>>());
)*
all_tasks.into_iter()
}
fn task_index(&self) -> u32 {
match self.clone() {
#(#enum_ident::#task_fn_idents { .. } => #task_indices,)*
Task::__Ignore(_, _) => unreachable!(),
}
}
fn is_valid(&self) -> bool {
match self.clone() {
#(#enum_ident::#task_fn_idents { #(#task_arg_names),* } => (#task_conditions)(#(#task_arg_names),* ),)*
Task::__Ignore(_, _) => unreachable!(),
}
}
fn run(&self) -> Result<(), #scrate::pallet_prelude::DispatchError> {
match self.clone() {
#(#enum_ident::#task_fn_idents { #(#task_arg_names),* } => {
<#enum_use>::#task_fn_names(#( #task_arg_names, )* )
},)*
Task::__Ignore(_, _) => unreachable!(),
}
}
#[allow(unused_variables)]
fn weight(&self) -> #scrate::pallet_prelude::Weight {
match self.clone() {
#(#enum_ident::#task_fn_idents { #(#task_arg_names),* } => #task_weights,)*
Task::__Ignore(_, _) => unreachable!(),
}
}
}
});
}
}
/// Expands the [`TasksDef`] in the enclosing [`Def`], if present, and returns its tokens.
///
/// This modifies the underlying [`Def`] in addition to returning any tokens that were added.
pub fn expand_tasks_impl(def: &mut Def) -> TokenStream2 {
let Some(tasks) = &mut def.tasks else { return quote!() };
let ExpandedTasksDef { task_item_impl, task_trait_impl } = parse_quote!(#tasks);
quote! {
#task_item_impl
#task_trait_impl
}
}
/// Represents a fully-expanded [`TaskEnumDef`].
#[derive(Parse)]
pub struct ExpandedTaskEnum {
pub item_enum: ItemEnum,
pub debug_impl: ItemImpl,
}
/// Modifies a [`Def`] to expand the underlying [`TaskEnumDef`] if present, and also returns
/// its tokens. A blank [`TokenStream2`] is returned if no [`TaskEnumDef`] has been generated
/// or defined.
pub fn expand_task_enum(def: &mut Def) -> TokenStream2 {
let Some(task_enum) = &mut def.task_enum else { return quote!() };
let ExpandedTaskEnum { item_enum, debug_impl } = parse_quote!(#task_enum);
quote! {
#item_enum
#debug_impl
}
}
/// Modifies a [`Def`] to expand the underlying [`TasksDef`] and also generate a
/// [`TaskEnumDef`] if applicable. The tokens for these items are returned if they are created.
pub fn expand_tasks(def: &mut Def) -> TokenStream2 {
if let Some(tasks_def) = &def.tasks {
if def.task_enum.is_none() {
def.task_enum = Some(TaskEnumDef::generate(
&tasks_def,
def.type_decl_bounded_generics(tasks_def.item_impl.span()),
def.type_use_generics(tasks_def.item_impl.span()),
));
}
}
let tasks_extra_output = expand_tasks_impl(def);
let task_enum_extra_output = expand_task_enum(def);
quote! {
#tasks_extra_output
#task_enum_extra_output
}
}
@@ -31,6 +31,8 @@ pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream {
let call_part = def.call.as_ref().map(|_| quote::quote!(Call,));
let task_part = def.task_enum.as_ref().map(|_| quote::quote!(Task,));
let storage_part = (!def.storages.is_empty()).then(|| quote::quote!(Storage,));
let event_part = def.event.as_ref().map(|event| {
@@ -99,7 +101,7 @@ pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream {
tokens = [{
expanded::{
Pallet, #call_part #storage_part #event_part #error_part #origin_part #config_part
#inherent_part #validate_unsigned_part #freeze_reason_part
#inherent_part #validate_unsigned_part #freeze_reason_part #task_part
#hold_reason_part #lock_id_part #slash_reason_part
}
}]