Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

Commit b1f5b0d

Browse files
authored
Find and load missing programs in LoadedPrograms cache (#30275)
* Find and load missing programs in LoadedPrograms cache - filter program accounts in a transaction batch - filter the accounts that are missing in LoadedPrograms cache - load the programs before processing the transactions - unit tests * address review comments * fix clippy * address review comments * fix test * fix more tests
1 parent b0f7b78 commit b1f5b0d

File tree

5 files changed

+203
-33
lines changed

5 files changed

+203
-33
lines changed

program-runtime/src/executor_cache.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ impl TransactionExecutorCache {
4141
self.visible.get(key).cloned()
4242
}
4343

44-
pub fn set_tombstone(&mut self, key: Pubkey) {
44+
pub fn set_tombstone(&mut self, key: Pubkey, slot: Slot) {
4545
self.visible
46-
.insert(key, Arc::new(LoadedProgram::new_tombstone()));
46+
.insert(key, Arc::new(LoadedProgram::new_tombstone(slot)));
4747
}
4848

4949
pub fn set(
@@ -52,12 +52,13 @@ impl TransactionExecutorCache {
5252
executor: Arc<LoadedProgram>,
5353
upgrade: bool,
5454
delay_visibility_of_program_deployment: bool,
55+
current_slot: Slot,
5556
) {
5657
if upgrade {
5758
if delay_visibility_of_program_deployment {
5859
// Place a tombstone in the cache so that
5960
// we don't load the new version from the database as it should remain invisible
60-
self.set_tombstone(key);
61+
self.set_tombstone(key, current_slot);
6162
} else {
6263
self.visible.insert(key, executor.clone());
6364
}

program-runtime/src/loaded_programs.rs

Lines changed: 107 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,12 @@ impl LoadedProgram {
180180
}
181181
}
182182

183-
pub fn new_tombstone() -> Self {
183+
pub fn new_tombstone(slot: Slot) -> Self {
184184
Self {
185185
program: LoadedProgramType::Invalid,
186186
account_size: 0,
187-
deployment_slot: 0,
188-
effective_slot: 0,
187+
deployment_slot: slot,
188+
effective_slot: slot,
189189
usage_counter: AtomicU64::default(),
190190
}
191191
}
@@ -219,8 +219,10 @@ pub enum LoadedProgramEntry {
219219
}
220220

221221
impl LoadedPrograms {
222-
/// Inserts a single entry
223-
pub fn insert_entry(&mut self, key: Pubkey, entry: LoadedProgram) -> LoadedProgramEntry {
222+
/// Refill the cache with a single entry. It's typically called during transaction processing,
223+
/// when the cache doesn't contain the entry corresponding to program `key`.
224+
/// The function dedupes the cache, in case some other thread replenished the the entry in parallel.
225+
pub fn replenish(&mut self, key: Pubkey, entry: Arc<LoadedProgram>) -> LoadedProgramEntry {
224226
let second_level = self.entries.entry(key).or_insert_with(Vec::new);
225227
let index = second_level
226228
.iter()
@@ -235,9 +237,32 @@ impl LoadedPrograms {
235237
return LoadedProgramEntry::WasOccupied(existing.clone());
236238
}
237239
}
238-
let new_entry = Arc::new(entry);
239-
second_level.insert(index.unwrap_or(second_level.len()), new_entry.clone());
240-
LoadedProgramEntry::WasVacant(new_entry)
240+
second_level.insert(index.unwrap_or(second_level.len()), entry.clone());
241+
LoadedProgramEntry::WasVacant(entry)
242+
}
243+
244+
/// Assign the program `entry` to the given `key` in the cache.
245+
/// This is typically called when a deployed program is managed (upgraded/un/reddeployed) via
246+
/// bpf loader instructions.
247+
/// The program management is not expected to overlap with initial program deployment slot.
248+
/// Note: Do not call this function to replenish cache with a missing entry. As that use-case can
249+
/// cause the cache to have duplicates. Use `replenish()` API for that use-case.
250+
pub fn assign_program(&mut self, key: Pubkey, entry: Arc<LoadedProgram>) -> Arc<LoadedProgram> {
251+
let second_level = self.entries.entry(key).or_insert_with(Vec::new);
252+
let index = second_level
253+
.iter()
254+
.position(|at| at.effective_slot >= entry.effective_slot);
255+
if let Some(index) = index {
256+
let existing = second_level
257+
.get(index)
258+
.expect("Missing entry, even though position was found");
259+
assert!(
260+
existing.deployment_slot != entry.deployment_slot
261+
|| existing.effective_slot != entry.effective_slot
262+
);
263+
}
264+
second_level.insert(index.unwrap_or(second_level.len()), entry.clone());
265+
entry
241266
}
242267

243268
/// Before rerooting the blockstore this removes all programs of orphan forks
@@ -310,6 +335,7 @@ mod tests {
310335
BlockRelation, ForkGraph, LoadedProgram, LoadedProgramEntry, LoadedProgramType,
311336
LoadedPrograms, WorkingSlot,
312337
},
338+
solana_rbpf::vm::BuiltInProgram,
313339
solana_sdk::{clock::Slot, pubkey::Pubkey},
314340
std::{
315341
collections::HashMap,
@@ -318,11 +344,70 @@ mod tests {
318344
},
319345
};
320346

347+
fn new_test_builtin_program(deployment_slot: Slot, effective_slot: Slot) -> Arc<LoadedProgram> {
348+
Arc::new(LoadedProgram {
349+
program: LoadedProgramType::BuiltIn(BuiltInProgram::default()),
350+
account_size: 0,
351+
deployment_slot,
352+
effective_slot,
353+
usage_counter: AtomicU64::default(),
354+
})
355+
}
356+
357+
fn set_tombstone(cache: &mut LoadedPrograms, key: Pubkey, slot: Slot) -> Arc<LoadedProgram> {
358+
cache.assign_program(key, Arc::new(LoadedProgram::new_tombstone(slot)))
359+
}
360+
321361
#[test]
322362
fn test_tombstone() {
323-
let tombstone = LoadedProgram::new_tombstone();
363+
let tombstone = LoadedProgram::new_tombstone(0);
364+
assert!(matches!(tombstone.program, LoadedProgramType::Invalid));
365+
assert!(tombstone.is_tombstone());
366+
assert_eq!(tombstone.deployment_slot, 0);
367+
assert_eq!(tombstone.effective_slot, 0);
368+
369+
let tombstone = LoadedProgram::new_tombstone(100);
324370
assert!(matches!(tombstone.program, LoadedProgramType::Invalid));
325371
assert!(tombstone.is_tombstone());
372+
assert_eq!(tombstone.deployment_slot, 100);
373+
assert_eq!(tombstone.effective_slot, 100);
374+
375+
let mut cache = LoadedPrograms::default();
376+
let program1 = Pubkey::new_unique();
377+
let tombstone = set_tombstone(&mut cache, program1, 10);
378+
let second_level = &cache
379+
.entries
380+
.get(&program1)
381+
.expect("Failed to find the entry");
382+
assert_eq!(second_level.len(), 1);
383+
assert!(second_level.get(0).unwrap().is_tombstone());
384+
assert_eq!(tombstone.deployment_slot, 10);
385+
assert_eq!(tombstone.effective_slot, 10);
386+
387+
// Add a program at slot 50, and a tombstone for the program at slot 60
388+
let program2 = Pubkey::new_unique();
389+
assert!(matches!(
390+
cache.replenish(program2, new_test_builtin_program(50, 51)),
391+
LoadedProgramEntry::WasVacant(_)
392+
));
393+
let second_level = &cache
394+
.entries
395+
.get(&program2)
396+
.expect("Failed to find the entry");
397+
assert_eq!(second_level.len(), 1);
398+
assert!(!second_level.get(0).unwrap().is_tombstone());
399+
400+
let tombstone = set_tombstone(&mut cache, program2, 60);
401+
let second_level = &cache
402+
.entries
403+
.get(&program2)
404+
.expect("Failed to find the entry");
405+
assert_eq!(second_level.len(), 2);
406+
assert!(!second_level.get(0).unwrap().is_tombstone());
407+
assert!(second_level.get(1).unwrap().is_tombstone());
408+
assert!(tombstone.is_tombstone());
409+
assert_eq!(tombstone.deployment_slot, 60);
410+
assert_eq!(tombstone.effective_slot, 60);
326411
}
327412

328413
struct TestForkGraph {
@@ -464,14 +549,14 @@ mod tests {
464549
}
465550
}
466551

467-
fn new_test_loaded_program(deployment_slot: Slot, effective_slot: Slot) -> LoadedProgram {
468-
LoadedProgram {
552+
fn new_test_loaded_program(deployment_slot: Slot, effective_slot: Slot) -> Arc<LoadedProgram> {
553+
Arc::new(LoadedProgram {
469554
program: LoadedProgramType::Invalid,
470555
account_size: 0,
471556
deployment_slot,
472557
effective_slot,
473558
usage_counter: AtomicU64::default(),
474-
}
559+
})
475560
}
476561

477562
fn match_slot(
@@ -511,52 +596,52 @@ mod tests {
511596

512597
let program1 = Pubkey::new_unique();
513598
assert!(matches!(
514-
cache.insert_entry(program1, new_test_loaded_program(0, 1)),
599+
cache.replenish(program1, new_test_loaded_program(0, 1)),
515600
LoadedProgramEntry::WasVacant(_)
516601
));
517602
assert!(matches!(
518-
cache.insert_entry(program1, new_test_loaded_program(10, 11)),
603+
cache.replenish(program1, new_test_loaded_program(10, 11)),
519604
LoadedProgramEntry::WasVacant(_)
520605
));
521606
assert!(matches!(
522-
cache.insert_entry(program1, new_test_loaded_program(20, 21)),
607+
cache.replenish(program1, new_test_loaded_program(20, 21)),
523608
LoadedProgramEntry::WasVacant(_)
524609
));
525610

526611
// Test: inserting duplicate entry return pre existing entry from the cache
527612
assert!(matches!(
528-
cache.insert_entry(program1, new_test_loaded_program(20, 21)),
613+
cache.replenish(program1, new_test_loaded_program(20, 21)),
529614
LoadedProgramEntry::WasOccupied(_)
530615
));
531616

532617
let program2 = Pubkey::new_unique();
533618
assert!(matches!(
534-
cache.insert_entry(program2, new_test_loaded_program(5, 6)),
619+
cache.replenish(program2, new_test_loaded_program(5, 6)),
535620
LoadedProgramEntry::WasVacant(_)
536621
));
537622
assert!(matches!(
538-
cache.insert_entry(program2, new_test_loaded_program(11, 12)),
623+
cache.replenish(program2, new_test_loaded_program(11, 12)),
539624
LoadedProgramEntry::WasVacant(_)
540625
));
541626

542627
let program3 = Pubkey::new_unique();
543628
assert!(matches!(
544-
cache.insert_entry(program3, new_test_loaded_program(25, 26)),
629+
cache.replenish(program3, new_test_loaded_program(25, 26)),
545630
LoadedProgramEntry::WasVacant(_)
546631
));
547632

548633
let program4 = Pubkey::new_unique();
549634
assert!(matches!(
550-
cache.insert_entry(program4, new_test_loaded_program(0, 1)),
635+
cache.replenish(program4, new_test_loaded_program(0, 1)),
551636
LoadedProgramEntry::WasVacant(_)
552637
));
553638
assert!(matches!(
554-
cache.insert_entry(program4, new_test_loaded_program(5, 6)),
639+
cache.replenish(program4, new_test_loaded_program(5, 6)),
555640
LoadedProgramEntry::WasVacant(_)
556641
));
557642
// The following is a special case, where effective slot is 4 slots in the future
558643
assert!(matches!(
559-
cache.insert_entry(program4, new_test_loaded_program(15, 19)),
644+
cache.replenish(program4, new_test_loaded_program(15, 19)),
560645
LoadedProgramEntry::WasVacant(_)
561646
));
562647

programs/bpf_loader/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ pub fn load_program_from_account(
258258
loaded_program.clone(),
259259
false,
260260
feature_set.is_active(&delay_visibility_of_program_deployment::id()),
261+
deployment_slot,
261262
);
262263
}
263264

@@ -291,6 +292,7 @@ macro_rules! deploy_program {
291292
Arc::new(executor),
292293
true,
293294
delay_visibility_of_program_deployment,
295+
$slot,
294296
);
295297
}};
296298
}
@@ -1183,10 +1185,11 @@ fn process_loader_upgradeable_instruction(
11831185
.feature_set
11841186
.is_active(&delay_visibility_of_program_deployment::id())
11851187
{
1188+
let clock = invoke_context.get_sysvar_cache().get_clock()?;
11861189
invoke_context
11871190
.tx_executor_cache
11881191
.borrow_mut()
1189-
.set_tombstone(program_key);
1192+
.set_tombstone(program_key, clock.slot);
11901193
}
11911194
}
11921195
_ => {

runtime/src/bank.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ use {
9494
compute_budget::{self, ComputeBudget},
9595
executor_cache::{BankExecutorCache, TransactionExecutorCache, MAX_CACHED_EXECUTORS},
9696
invoke_context::{BuiltinProgram, ProcessInstructionWithContext},
97-
loaded_programs::{LoadedProgram, LoadedPrograms, WorkingSlot},
97+
loaded_programs::{LoadedProgram, LoadedProgramEntry, LoadedPrograms, WorkingSlot},
9898
log_collector::LogCollector,
9999
sysvar_cache::SysvarCache,
100100
timings::{ExecuteTimingType, ExecuteTimings},
@@ -105,6 +105,7 @@ use {
105105
AccountSharedData, InheritableAccountFields, ReadableAccount, WritableAccount,
106106
},
107107
account_utils::StateMut,
108+
bpf_loader, bpf_loader_deprecated,
108109
bpf_loader_upgradeable::{self, UpgradeableLoaderState},
109110
clock::{
110111
BankId, Epoch, Slot, SlotCount, SlotIndex, UnixTimestamp, DEFAULT_HASHES_PER_TICK,
@@ -4338,6 +4339,68 @@ impl Bank {
43384339
|| self.cluster_type() != ClusterType::MainnetBeta
43394340
}
43404341

4342+
#[allow(dead_code)] // Preparation for BankExecutorCache rework
4343+
fn load_and_get_programs_from_cache<'a>(
4344+
&self,
4345+
program_owners: &[&'a Pubkey],
4346+
sanitized_txs: &[SanitizedTransaction],
4347+
check_results: &mut [TransactionCheckResult],
4348+
) -> (
4349+
HashMap<Pubkey, &'a Pubkey>,
4350+
HashMap<Pubkey, Arc<LoadedProgram>>,
4351+
) {
4352+
let mut filter_programs_time = Measure::start("filter_programs_accounts");
4353+
let program_accounts_map = self.rc.accounts.filter_executable_program_accounts(
4354+
&self.ancestors,
4355+
sanitized_txs,
4356+
check_results,
4357+
program_owners,
4358+
&self.blockhash_queue.read().unwrap(),
4359+
);
4360+
filter_programs_time.stop();
4361+
4362+
let mut filter_missing_programs_time = Measure::start("filter_missing_programs_accounts");
4363+
let (mut loaded_programs_for_txs, missing_programs) = self
4364+
.loaded_programs_cache
4365+
.read()
4366+
.unwrap()
4367+
.extract(self, program_accounts_map.keys().cloned());
4368+
filter_missing_programs_time.stop();
4369+
4370+
missing_programs
4371+
.iter()
4372+
.for_each(|pubkey| match self.load_program(pubkey) {
4373+
Ok(program) => {
4374+
match self
4375+
.loaded_programs_cache
4376+
.write()
4377+
.unwrap()
4378+
.replenish(*pubkey, program)
4379+
{
4380+
LoadedProgramEntry::WasOccupied(entry) => {
4381+
loaded_programs_for_txs.insert(*pubkey, entry);
4382+
}
4383+
LoadedProgramEntry::WasVacant(new_entry) => {
4384+
loaded_programs_for_txs.insert(*pubkey, new_entry);
4385+
}
4386+
}
4387+
}
4388+
4389+
Err(e) => {
4390+
// Create a tombstone for the program in the cache
4391+
debug!("Failed to load program {}, error {:?}", pubkey, e);
4392+
let tombstone = self
4393+
.loaded_programs_cache
4394+
.write()
4395+
.unwrap()
4396+
.assign_program(*pubkey, Arc::new(LoadedProgram::new_tombstone(self.slot)));
4397+
loaded_programs_for_txs.insert(*pubkey, tombstone);
4398+
}
4399+
});
4400+
4401+
(program_accounts_map, loaded_programs_for_txs)
4402+
}
4403+
43414404
#[allow(clippy::type_complexity)]
43424405
pub fn load_and_execute_transactions(
43434406
&self,
@@ -4400,6 +4463,24 @@ impl Bank {
44004463
);
44014464
check_time.stop();
44024465

4466+
let program_owners: Vec<Pubkey> = vec![
4467+
bpf_loader_upgradeable::id(),
4468+
bpf_loader::id(),
4469+
bpf_loader_deprecated::id(),
4470+
native_loader::id(),
4471+
];
4472+
4473+
let _program_owners_refs: Vec<&Pubkey> = program_owners.iter().collect();
4474+
// The following code is currently commented out. This is how the new cache will
4475+
// finally be used, once rest of the code blocks are in place.
4476+
/*
4477+
let (program_accounts_map, loaded_programs_map) = self.load_and_get_programs_from_cache(
4478+
&program_owners_refs,
4479+
sanitized_txs,
4480+
&check_results,
4481+
);
4482+
*/
4483+
44034484
let mut load_time = Measure::start("accounts_load");
44044485
let mut loaded_transactions = self.rc.accounts.load_accounts(
44054486
&self.ancestors,

0 commit comments

Comments
 (0)