Skip to content

Commit 5aaf5f4

Browse files
muharemggwpez
andauthored
Scheduler: remove empty agenda on cancel (paritytech#12989)
* Scheduler: remove empty agenda on cancel * use iter any * fix benches * remove trailing None * Add CleanupAgendas migration Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * fix ci Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Count non-empty agendas in migration Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
1 parent 8e76398 commit 5aaf5f4

File tree

4 files changed

+353
-81
lines changed

4 files changed

+353
-81
lines changed

frame/scheduler/src/benchmarking.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,17 @@ benchmarks! {
244244
}: _<SystemOrigin<T>>(schedule_origin, when, 0)
245245
verify {
246246
ensure!(
247-
Lookup::<T>::get(u32_to_name(0)).is_none(),
248-
"didn't remove from lookup"
247+
s == 1 || Lookup::<T>::get(u32_to_name(0)).is_none(),
248+
"didn't remove from lookup if more than 1 task scheduled for `when`"
249249
);
250250
// Removed schedule is NONE
251251
ensure!(
252-
Agenda::<T>::get(when)[0].is_none(),
253-
"didn't remove from schedule"
252+
s == 1 || Agenda::<T>::get(when)[0].is_none(),
253+
"didn't remove from schedule if more than 1 task scheduled for `when`"
254+
);
255+
ensure!(
256+
s > 1 || Agenda::<T>::get(when).len() == 0,
257+
"remove from schedule if only 1 task scheduled for `when`"
254258
);
255259
}
256260

@@ -280,13 +284,17 @@ benchmarks! {
280284
}: _(RawOrigin::Root, u32_to_name(0))
281285
verify {
282286
ensure!(
283-
Lookup::<T>::get(u32_to_name(0)).is_none(),
284-
"didn't remove from lookup"
287+
s == 1 || Lookup::<T>::get(u32_to_name(0)).is_none(),
288+
"didn't remove from lookup if more than 1 task scheduled for `when`"
285289
);
286290
// Removed schedule is NONE
287291
ensure!(
288-
Agenda::<T>::get(when)[0].is_none(),
289-
"didn't remove from schedule"
292+
s == 1 || Agenda::<T>::get(when)[0].is_none(),
293+
"didn't remove from schedule if more than 1 task scheduled for `when`"
294+
);
295+
ensure!(
296+
s > 1 || Agenda::<T>::get(when).len() == 0,
297+
"remove from schedule if only 1 task scheduled for `when`"
290298
);
291299
}
292300

frame/scheduler/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,22 @@ impl<T: Config> Pallet<T> {
752752
Ok(index)
753753
}
754754

755+
/// Remove trailing `None` items of an agenda at `when`. If all items are `None` remove the
756+
/// agenda record entirely.
757+
fn cleanup_agenda(when: T::BlockNumber) {
758+
let mut agenda = Agenda::<T>::get(when);
759+
match agenda.iter().rposition(|i| i.is_some()) {
760+
Some(i) if agenda.len() > i + 1 => {
761+
agenda.truncate(i + 1);
762+
Agenda::<T>::insert(when, agenda);
763+
},
764+
Some(_) => {},
765+
None => {
766+
Agenda::<T>::remove(when);
767+
},
768+
}
769+
}
770+
755771
fn do_schedule(
756772
when: DispatchTime<T::BlockNumber>,
757773
maybe_periodic: Option<schedule::Period<T::BlockNumber>>,
@@ -802,6 +818,7 @@ impl<T: Config> Pallet<T> {
802818
if let Some(id) = s.maybe_id {
803819
Lookup::<T>::remove(id);
804820
}
821+
Self::cleanup_agenda(when);
805822
Self::deposit_event(Event::Canceled { when, index });
806823
Ok(())
807824
} else {
@@ -824,6 +841,7 @@ impl<T: Config> Pallet<T> {
824841
ensure!(!matches!(task, Some(Scheduled { maybe_id: Some(_), .. })), Error::<T>::Named);
825842
task.take().ok_or(Error::<T>::NotFound)
826843
})?;
844+
Self::cleanup_agenda(when);
827845
Self::deposit_event(Event::Canceled { when, index });
828846

829847
Self::place_task(new_time, task).map_err(|x| x.0)
@@ -880,6 +898,7 @@ impl<T: Config> Pallet<T> {
880898
}
881899
Ok(())
882900
})?;
901+
Self::cleanup_agenda(when);
883902
Self::deposit_event(Event::Canceled { when, index });
884903
Ok(())
885904
} else {
@@ -905,6 +924,7 @@ impl<T: Config> Pallet<T> {
905924
let task = agenda.get_mut(index as usize).ok_or(Error::<T>::NotFound)?;
906925
task.take().ok_or(Error::<T>::NotFound)
907926
})?;
927+
Self::cleanup_agenda(when);
908928
Self::deposit_event(Event::Canceled { when, index });
909929
Self::place_task(new_time, task).map_err(|x| x.0)
910930
}

frame/scheduler/src/migration.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,119 @@ pub mod v3 {
198198
}
199199
}
200200

201+
mod v4 {
202+
use super::*;
203+
use frame_support::pallet_prelude::*;
204+
205+
/// This migration cleans up empty agendas of the V4 scheduler.
206+
///
207+
/// This should be run on a scheduler that does not have
208+
/// <https://github.com/paritytech/substrate/pull/12989> since it piles up `None`-only agendas. This does not modify the pallet version.
209+
pub struct CleanupAgendas<T>(sp_std::marker::PhantomData<T>);
210+
211+
impl<T: Config> OnRuntimeUpgrade for CleanupAgendas<T> {
212+
#[cfg(feature = "try-runtime")]
213+
fn pre_upgrade() -> Result<Vec<u8>, &'static str> {
214+
assert_eq!(
215+
StorageVersion::get::<Pallet<T>>(),
216+
4,
217+
"Can only cleanup agendas of the V4 scheduler"
218+
);
219+
220+
let agendas = Agenda::<T>::iter_keys().count();
221+
let non_empty_agendas =
222+
Agenda::<T>::iter_values().filter(|a| a.iter().any(|s| s.is_some())).count();
223+
log::info!(
224+
target: TARGET,
225+
"There are {} total and {} non-empty agendas",
226+
agendas,
227+
non_empty_agendas
228+
);
229+
230+
Ok((agendas as u32, non_empty_agendas as u32).encode())
231+
}
232+
233+
fn on_runtime_upgrade() -> Weight {
234+
let version = StorageVersion::get::<Pallet<T>>();
235+
if version != 4 {
236+
log::warn!(target: TARGET, "Skipping CleanupAgendas migration since it was run on the wrong version: {:?} != 4", version);
237+
return T::DbWeight::get().reads(1)
238+
}
239+
240+
let keys = Agenda::<T>::iter_keys().collect::<Vec<_>>();
241+
let mut writes = 0;
242+
for k in &keys {
243+
let mut schedules = Agenda::<T>::get(k);
244+
let all_schedules = schedules.len();
245+
let suffix_none_schedules =
246+
schedules.iter().rev().take_while(|s| s.is_none()).count();
247+
248+
match all_schedules.checked_sub(suffix_none_schedules) {
249+
Some(0) => {
250+
log::info!(
251+
"Deleting None-only agenda {:?} with {} entries",
252+
k,
253+
all_schedules
254+
);
255+
Agenda::<T>::remove(k);
256+
writes.saturating_inc();
257+
},
258+
Some(ne) if ne > 0 => {
259+
log::info!(
260+
"Removing {} schedules of {} from agenda {:?}, now {:?}",
261+
suffix_none_schedules,
262+
all_schedules,
263+
ne,
264+
k
265+
);
266+
schedules.truncate(ne);
267+
Agenda::<T>::insert(k, schedules);
268+
writes.saturating_inc();
269+
},
270+
Some(_) => {
271+
frame_support::defensive!(
272+
// Bad but let's not panic.
273+
"Cannot have more None suffix schedules that schedules in total"
274+
);
275+
},
276+
None => {
277+
log::info!("Agenda {:?} does not have any None suffix schedules", k);
278+
},
279+
}
280+
}
281+
282+
// We don't modify the pallet version.
283+
284+
T::DbWeight::get().reads_writes(1 + keys.len().saturating_mul(2) as u64, writes)
285+
}
286+
287+
#[cfg(feature = "try-runtime")]
288+
fn post_upgrade(state: Vec<u8>) -> Result<(), &'static str> {
289+
assert_eq!(StorageVersion::get::<Pallet<T>>(), 4, "Version must not change");
290+
291+
let (old_agendas, non_empty_agendas): (u32, u32) =
292+
Decode::decode(&mut state.as_ref()).expect("Must decode pre_upgrade state");
293+
let new_agendas = Agenda::<T>::iter_keys().count() as u32;
294+
295+
match old_agendas.checked_sub(new_agendas) {
296+
Some(0) => log::warn!(
297+
target: TARGET,
298+
"Did not clean up any agendas. v4::CleanupAgendas can be removed."
299+
),
300+
Some(n) =>
301+
log::info!(target: TARGET, "Cleaned up {} agendas, now {}", n, new_agendas),
302+
None => unreachable!(
303+
"Number of agendas cannot increase, old {} new {}",
304+
old_agendas, new_agendas
305+
),
306+
}
307+
assert_eq!(new_agendas, non_empty_agendas, "Expected to keep all non-empty agendas");
308+
309+
Ok(())
310+
}
311+
}
312+
}
313+
201314
#[cfg(test)]
202315
#[cfg(feature = "try-runtime")]
203316
mod test {
@@ -396,6 +509,64 @@ mod test {
396509
});
397510
}
398511

512+
#[test]
513+
fn cleanup_agendas_works() {
514+
use sp_core::bounded_vec;
515+
new_test_ext().execute_with(|| {
516+
StorageVersion::new(4).put::<Scheduler>();
517+
518+
let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![] });
519+
let bounded_call = Preimage::bound(call).unwrap();
520+
let some = Some(ScheduledOf::<Test> {
521+
maybe_id: None,
522+
priority: 1,
523+
call: bounded_call,
524+
maybe_periodic: None,
525+
origin: root(),
526+
_phantom: Default::default(),
527+
});
528+
529+
// Put some empty, and some non-empty agendas in there.
530+
let test_data: Vec<(
531+
BoundedVec<Option<ScheduledOf<Test>>, <Test as Config>::MaxScheduledPerBlock>,
532+
Option<
533+
BoundedVec<Option<ScheduledOf<Test>>, <Test as Config>::MaxScheduledPerBlock>,
534+
>,
535+
)> = vec![
536+
(bounded_vec![some.clone()], Some(bounded_vec![some.clone()])),
537+
(bounded_vec![None, some.clone()], Some(bounded_vec![None, some.clone()])),
538+
(bounded_vec![None, some.clone(), None], Some(bounded_vec![None, some.clone()])),
539+
(bounded_vec![some.clone(), None, None], Some(bounded_vec![some.clone()])),
540+
(bounded_vec![None, None], None),
541+
(bounded_vec![None, None, None], None),
542+
(bounded_vec![], None),
543+
];
544+
545+
// Insert all the agendas.
546+
for (i, test) in test_data.iter().enumerate() {
547+
Agenda::<Test>::insert(i as u64, test.0.clone());
548+
}
549+
550+
// Run the migration.
551+
let data = v4::CleanupAgendas::<Test>::pre_upgrade().unwrap();
552+
let _w = v4::CleanupAgendas::<Test>::on_runtime_upgrade();
553+
v4::CleanupAgendas::<Test>::post_upgrade(data).unwrap();
554+
555+
// Check that the post-state is correct.
556+
for (i, test) in test_data.iter().enumerate() {
557+
match test.1.clone() {
558+
None => assert!(
559+
!Agenda::<Test>::contains_key(i as u64),
560+
"Agenda {} should be removed",
561+
i
562+
),
563+
Some(new) =>
564+
assert_eq!(Agenda::<Test>::get(i as u64), new, "Agenda wrong {}", i),
565+
}
566+
}
567+
});
568+
}
569+
399570
fn signed(i: u64) -> OriginCaller {
400571
system::RawOrigin::Signed(i).into()
401572
}

0 commit comments

Comments
 (0)