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

Commit 9f33380

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

File tree

3 files changed

+236
-29
lines changed

3 files changed

+236
-29
lines changed

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

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),

runtime/parachains/src/shared.rs

+226-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,58 @@ 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) =>
188+
if incomplete_session != session {
189+
PruneableSessions::<T>::remove(session);
190+
},
191+
None => {
192+
PruneableSessions::<T>::remove(session);
193+
},
194+
}
195+
}
196+
}
197+
132198
/// Records an included candidate, returning the block height that should be reverted to if the
133199
/// block is found to be invalid. This method will return `None` if and only if `included_in`
134200
/// is zero.
@@ -164,9 +230,9 @@ impl<T: Config> Pallet<T> {
164230
<IncludedCandidates<T>>::get(session, candidate_hash)
165231
}
166232

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);
233+
#[cfg(test)]
234+
pub(crate) fn is_pruneable_session(session: &SessionIndex) -> bool {
235+
<PruneableSessions<T>>::contains_key(session)
170236
}
171237

172238
#[cfg(test)]
@@ -327,34 +393,174 @@ mod tests {
327393
}
328394

329395
#[test]
330-
fn prune_included_candidate_removes_all_candidates_with_same_session() {
396+
fn prune_ancient_sessions_no_incomplete_session() {
331397
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
398+
let session = 1;
332399
let candidate_hash1 = CandidateHash(sp_core::H256::repeat_byte(1));
333400
let candidate_hash2 = CandidateHash(sp_core::H256::repeat_byte(2));
334-
let candidate_hash3 = CandidateHash(sp_core::H256::repeat_byte(3));
401+
let block_number = 1;
402+
let core_index = CoreIndex(0);
335403

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

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

349-
ParasShared::prune_included_candidates(1);
479+
// Both candidates should still exist.
480+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash1));
481+
assert!(ParasShared::is_included_candidate(&session, &candidate_hash2));
350482

351-
assert_eq!(ParasShared::included_candidates_iter_prefix(1).count(), 0);
352-
assert_eq!(ParasShared::included_candidates_iter_prefix(2).count(), 1);
483+
// Mark the candidates' session as pruneable.
484+
ParasShared::mark_session_pruneable(session);
485+
assert!(ParasShared::is_pruneable_session(&session));
353486

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

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

0 commit comments

Comments
 (0)