Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.

Commit 9bf8442

Browse files
committed
runtime/parachains: Add pruning logic
1 parent 58f8b5f commit 9bf8442

File tree

3 files changed

+233
-29
lines changed

3 files changed

+233
-29
lines changed

Diff for: runtime/parachains/src/disputes.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -619,10 +619,12 @@ impl<T: Config> Pallet<T> {
619619
for to_prune in to_prune {
620620
// This should be small, as disputes are rare, so `None` is fine.
621621
<Disputes<T>>::remove_prefix(to_prune, None);
622-
623-
// This is larger, and will be extracted to the `shared` module for more proper pruning.
624-
// TODO: https://github.com/paritytech/polkadot/issues/3469
625-
shared::Pallet::<T>::prune_included_candidates(to_prune);
622+
// Mark the session as pruneable so that its candidates can be incrementally
623+
// removed over the course of many block inclusions.
624+
shared::Pallet::<T>::mark_session_pruneable(to_prune);
625+
// TODO(ladi): remove this call, currently allows unit tests to pass. Need to
626+
// figure out how to invoke paras_inherent::enter in run_to_block.
627+
shared::Pallet::<T>::prune_ancient_sessions(shared::MAX_CANDIDATES_TO_PRUNE);
626628
SpamSlots::<T>::remove(to_prune);
627629
}
628630

Diff for: runtime/parachains/src/paras_inherent.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,7 @@ pub mod pallet {
222222
None => continue,
223223
};
224224

225-
T::DisputesHandler::process_included(
226-
current_session,
227-
*candidate_hash,
228-
revert_to,
229-
);
225+
T::DisputesHandler::process_included(current_session, *candidate_hash, revert_to);
230226
}
231227

232228
// Handle timeouts for any availability core work.
@@ -281,6 +277,9 @@ pub mod pallet {
281277
// And track that we've finished processing the inherent for this block.
282278
Included::<T>::set(Some(()));
283279

280+
// Prune candidates incrementally with each block inclusion.
281+
shared::Pallet::<T>::prune_ancient_sessions(shared::MAX_CANDIDATES_TO_PRUNE);
282+
284283
Ok(Some(
285284
MINIMAL_INCLUSION_INHERENT_WEIGHT +
286285
(backed_candidates_len * BACKED_CANDIDATE_WEIGHT),

Diff for: runtime/parachains/src/shared.rs

+223-20
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ pub use pallet::*;
3636
// which guarantees that at least one full session has passed before any changes are applied.
3737
pub(crate) const SESSION_DELAY: SessionIndex = 2;
3838

39+
// `MAX_CANDIDATES_TO_PRUNE` is used to upper bound the number of ancient candidates that can be
40+
// pruned when a new block is included. This is used to distribute the cost of pruning over
41+
// multiple blocks rather than pruning all historical candidates upon starting a new session.
42+
pub(crate) const MAX_CANDIDATES_TO_PRUNE: usize = 200;
43+
3944
#[frame_support::pallet]
4045
pub mod pallet {
4146
use super::*;
@@ -79,6 +84,15 @@ pub mod pallet {
7984
(T::BlockNumber, CoreIndex),
8085
>;
8186

87+
/// The set of all past sessions that have yet to fully pruned. Sessions are added to this set
88+
/// upon moving to new current session, and removed after all of their included candidates
89+
/// have been removed. This set is tracked independently so that the cost of removing included
90+
/// candidates can be amortized over multiple blocks rather than removing all candidates upon
91+
/// transitioning to a new session.
92+
#[pallet::storage]
93+
#[pallet::getter(fn pruneable_sessions)]
94+
pub(super) type PruneableSessions<T: Config> = StorageMap<_, Twox64Concat, SessionIndex, ()>;
95+
8296
#[pallet::call]
8397
impl<T: Config> Pallet<T> {}
8498
}
@@ -129,6 +143,55 @@ impl<T: Config> Pallet<T> {
129143
Self::session_index().saturating_add(SESSION_DELAY)
130144
}
131145

146+
/// Adds a session that is no longer in the dispute window as pruneable. Subsequent block
147+
/// inclusions will incrementally remove old candidates to avoid taking the performance hit of
148+
/// removing all candidates at once.
149+
pub(crate) fn mark_session_pruneable(session: SessionIndex) {
150+
PruneableSessions::<T>::insert(session, ());
151+
}
152+
153+
/// Prunes up to `max_candidates_to_prune` candidates from `IncludedCandidates` that belong to
154+
/// non-active sessions.
155+
pub(crate) fn prune_ancient_sessions(max_candidates_to_prune: usize) {
156+
let mut to_prune = Vec::new();
157+
let mut n_candidates = 0;
158+
let mut incomplete_session = None;
159+
for session in PruneableSessions::<T>::iter_keys() {
160+
let mut hashes = Vec::new();
161+
for candidate_hash in IncludedCandidates::<T>::iter_key_prefix(session) {
162+
// Exit condition when this session still has more candidates to prune; mark the
163+
// session as incomplete so the remaining candidates can be pruned in a subsequent
164+
// invocation.
165+
if n_candidates >= max_candidates_to_prune {
166+
incomplete_session = Some(session);
167+
break
168+
}
169+
hashes.push(candidate_hash);
170+
n_candidates += 1;
171+
}
172+
173+
to_prune.push((session, hashes));
174+
// Exit condition when all candidates from this session were selected for pruning.
175+
if n_candidates >= max_candidates_to_prune {
176+
break
177+
}
178+
}
179+
180+
for (session, candidate_hashes) in to_prune {
181+
for candidate_hash in candidate_hashes {
182+
IncludedCandidates::<T>::remove(session, candidate_hash);
183+
}
184+
185+
// Prune the session only if it was not marked as incomplete.
186+
match incomplete_session {
187+
Some(incomplete_session) if incomplete_session != session =>
188+
PruneableSessions::<T>::remove(session),
189+
None => PruneableSessions::<T>::remove(session),
190+
_ => {},
191+
}
192+
}
193+
}
194+
132195
/// Records an included candidate, returning the block height that should be reverted to if the
133196
/// block is found to be invalid. This method will return `None` if and only if `included_in`
134197
/// is zero.
@@ -164,9 +227,9 @@ impl<T: Config> Pallet<T> {
164227
<IncludedCandidates<T>>::get(session, candidate_hash)
165228
}
166229

167-
/// Prunes all candidates that were included in the `to_prune` session.
168-
pub(crate) fn prune_included_candidates(to_prune: SessionIndex) {
169-
<IncludedCandidates<T>>::remove_prefix(to_prune, None);
230+
#[cfg(test)]
231+
pub(crate) fn is_pruneable_session(session: &SessionIndex) -> bool {
232+
<PruneableSessions<T>>::contains_key(session)
170233
}
171234

172235
#[cfg(test)]
@@ -327,34 +390,174 @@ mod tests {
327390
}
328391

329392
#[test]
330-
fn prune_included_candidate_removes_all_candidates_with_same_session() {
393+
fn prune_ancient_sessions_no_incomplete_session() {
331394
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
395+
let session = 1;
332396
let candidate_hash1 = CandidateHash(sp_core::H256::repeat_byte(1));
333397
let candidate_hash2 = CandidateHash(sp_core::H256::repeat_byte(2));
334-
let candidate_hash3 = CandidateHash(sp_core::H256::repeat_byte(3));
398+
let block_number = 1;
399+
let core_index = CoreIndex(0);
335400

336-
assert!(
337-
ParasShared::note_included_candidate(1, candidate_hash1, 1, CoreIndex(0)).is_some()
401+
assert_eq!(
402+
ParasShared::note_included_candidate(
403+
session,
404+
candidate_hash1,
405+
block_number,
406+
core_index,
407+
),
408+
Some(block_number - 1),
338409
);
339-
assert!(
340-
ParasShared::note_included_candidate(1, candidate_hash2, 1, CoreIndex(0)).is_some()
410+
assert_eq!(
411+
ParasShared::note_included_candidate(
412+
session,
413+
candidate_hash2,
414+
block_number,
415+
core_index,
416+
),
417+
Some(block_number - 1),
418+
);
419+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash1));
420+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash2));
421+
422+
// Prune before any sessions are marked pruneable.
423+
ParasShared::prune_ancient_sessions(2);
424+
425+
// Both candidates should still exist.
426+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash1));
427+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash2));
428+
429+
// Mark the candidates' session as pruneable.
430+
ParasShared::mark_session_pruneable(session);
431+
assert!(ParasShared::is_pruneable_session(&session));
432+
433+
// Prune the sessions, which should remove both candidates. The session should not be
434+
// marked as incomplete since there are exactly two candidates.
435+
ParasShared::prune_ancient_sessions(2);
436+
437+
assert!(!ParasShared::is_pruneable_session(&session));
438+
assert!(!ParasShared::is_included_candidate(&session, &candidate_hash1));
439+
assert!(!ParasShared::is_included_candidate(&session, &candidate_hash2));
440+
})
441+
}
442+
443+
#[test]
444+
fn prune_ancient_sessions_incomplete_session() {
445+
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
446+
let session = 1;
447+
let candidate_hash1 = CandidateHash(sp_core::H256::repeat_byte(1));
448+
let candidate_hash2 = CandidateHash(sp_core::H256::repeat_byte(2));
449+
let block_number = 1;
450+
let core_index = CoreIndex(0);
451+
452+
assert_eq!(
453+
ParasShared::note_included_candidate(
454+
session,
455+
candidate_hash1,
456+
block_number,
457+
core_index,
458+
),
459+
Some(block_number - 1),
341460
);
342-
assert!(
343-
ParasShared::note_included_candidate(2, candidate_hash3, 2, CoreIndex(0)).is_some()
461+
assert_eq!(
462+
ParasShared::note_included_candidate(
463+
session,
464+
candidate_hash2,
465+
block_number,
466+
core_index,
467+
),
468+
Some(block_number - 1),
344469
);
470+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash1));
471+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash2));
345472

346-
assert_eq!(ParasShared::included_candidates_iter_prefix(1).count(), 2);
347-
assert_eq!(ParasShared::included_candidates_iter_prefix(2).count(), 1);
473+
// Prune before any sessions are marked pruneable.
474+
ParasShared::prune_ancient_sessions(1);
348475

349-
ParasShared::prune_included_candidates(1);
476+
// Both candidates should still exist.
477+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash1));
478+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash2));
350479

351-
assert_eq!(ParasShared::included_candidates_iter_prefix(1).count(), 0);
352-
assert_eq!(ParasShared::included_candidates_iter_prefix(2).count(), 1);
480+
// Mark the candidates' session as pruneable.
481+
ParasShared::mark_session_pruneable(session);
482+
assert!(ParasShared::is_pruneable_session(&session));
353483

354-
ParasShared::prune_included_candidates(2);
484+
// Prune the sessions, which should remove one of the candidates. The session will be
485+
// marked as incomplete so the session should remain unpruned.
486+
ParasShared::prune_ancient_sessions(1);
355487

356-
assert_eq!(ParasShared::included_candidates_iter_prefix(1).count(), 0);
357-
assert_eq!(ParasShared::included_candidates_iter_prefix(2).count(), 0);
358-
});
488+
assert!(ParasShared::is_pruneable_session(&session));
489+
assert!(!ParasShared::is_included_candidate(&session, &candidate_hash1));
490+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash2));
491+
})
492+
}
493+
494+
#[test]
495+
fn prune_ancient_sessions_complete_and_incomplete_sessions() {
496+
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
497+
let session1 = 1;
498+
let session2 = 2;
499+
let candidate_hash1 = CandidateHash(sp_core::H256::repeat_byte(1));
500+
let candidate_hash2 = CandidateHash(sp_core::H256::repeat_byte(2));
501+
let candidate_hash3 = CandidateHash(sp_core::H256::repeat_byte(3));
502+
let block_number1 = 1;
503+
let block_number2 = 2;
504+
let core_index = CoreIndex(0);
505+
506+
assert_eq!(
507+
ParasShared::note_included_candidate(
508+
session1,
509+
candidate_hash1,
510+
block_number1,
511+
core_index,
512+
),
513+
Some(block_number1 - 1),
514+
);
515+
assert_eq!(
516+
ParasShared::note_included_candidate(
517+
session2,
518+
candidate_hash2,
519+
block_number2,
520+
core_index,
521+
),
522+
Some(block_number2 - 1),
523+
);
524+
assert_eq!(
525+
ParasShared::note_included_candidate(
526+
session2,
527+
candidate_hash3,
528+
block_number2,
529+
core_index,
530+
),
531+
Some(block_number2 - 1),
532+
);
533+
assert!(ParasShared::is_included_candidate(&session1, &candidate_hash1));
534+
assert!(ParasShared::is_included_candidate(&session2, &candidate_hash2));
535+
assert!(ParasShared::is_included_candidate(&session2, &candidate_hash3));
536+
537+
// Prune before any sessions are marked pruneable.
538+
ParasShared::prune_ancient_sessions(2);
539+
540+
// Both candidates should still exist.
541+
assert!(ParasShared::is_included_candidate(&session1, &candidate_hash1));
542+
assert!(ParasShared::is_included_candidate(&session2, &candidate_hash2));
543+
assert!(ParasShared::is_included_candidate(&session2, &candidate_hash3));
544+
545+
// Mark the candidates' session as pruneable.
546+
ParasShared::mark_session_pruneable(session1);
547+
ParasShared::mark_session_pruneable(session2);
548+
assert!(ParasShared::is_pruneable_session(&session1));
549+
assert!(ParasShared::is_pruneable_session(&session2));
550+
551+
// Prune the sessions, which should remove one candidate from each session. The first
552+
// session should be pruned while the second session will be marked as incomplete, and
553+
// so should remain in the set of pruneable sessions.
554+
ParasShared::prune_ancient_sessions(2);
555+
556+
assert!(!ParasShared::is_pruneable_session(&session1));
557+
assert!(ParasShared::is_pruneable_session(&session2));
558+
assert!(!ParasShared::is_included_candidate(&session1, &candidate_hash1));
559+
assert!(ParasShared::is_included_candidate(&session2, &candidate_hash2));
560+
assert!(!ParasShared::is_included_candidate(&session2, &candidate_hash3));
561+
})
359562
}
360563
}

0 commit comments

Comments
 (0)