Cache block time in Blockstore (#11955)

* Add blockstore column to cache block times

* Add method to cache block time

* Add service to cache block time

* Update rpc getBlockTime to use new method, and refactor blockstore slightly

* Return block_time with confirmed block, if available

* Add measure and warning to cache-block-time
This commit is contained in:
Tyera Eulberg
2020-09-09 09:33:14 -06:00
committed by GitHub
parent 28f2fa3fd5
commit 05db41fe9c
9 changed files with 316 additions and 106 deletions

View File

@ -78,7 +78,6 @@ thread_local!(static PAR_THREAD_POOL_ALL_CPUS: RefCell<ThreadPool> = RefCell::ne
pub const MAX_COMPLETED_SLOTS_IN_CHANNEL: usize = 100_000;
pub const MAX_TURBINE_PROPAGATION_IN_MS: u64 = 100;
pub const MAX_TURBINE_DELAY_IN_TICKS: u64 = MAX_TURBINE_PROPAGATION_IN_MS / MS_PER_TICK;
const TIMESTAMP_SLOT_INTERVAL: u64 = 4500;
const TIMESTAMP_SLOT_RANGE: usize = 16;
// An upper bound on maximum number of data shreds we can handle in a slot
@ -137,6 +136,7 @@ pub struct Blockstore {
transaction_status_index_cf: LedgerColumn<cf::TransactionStatusIndex>,
active_transaction_status_index: RwLock<u64>,
rewards_cf: LedgerColumn<cf::Rewards>,
blocktime_cf: LedgerColumn<cf::Blocktime>,
last_root: Arc<RwLock<Slot>>,
insert_shreds_lock: Arc<Mutex<()>>,
pub new_shreds_signals: Vec<SyncSender<bool>>,
@ -292,6 +292,7 @@ impl Blockstore {
let address_signatures_cf = db.column();
let transaction_status_index_cf = db.column();
let rewards_cf = db.column();
let blocktime_cf = db.column();
let db = Arc::new(db);
@ -336,6 +337,7 @@ impl Blockstore {
transaction_status_index_cf,
active_transaction_status_index: RwLock::new(active_transaction_status_index),
rewards_cf,
blocktime_cf,
new_shreds_signals: vec![],
completed_slots_senders: vec![],
insert_shreds_lock: Arc::new(Mutex::new(())),
@ -1557,12 +1559,7 @@ impl Blockstore {
}
}
pub fn get_block_time(
&self,
slot: Slot,
slot_duration: Duration,
stakes: &HashMap<Pubkey, (u64, Account)>,
) -> Result<Option<UnixTimestamp>> {
pub fn get_block_time(&self, slot: Slot) -> Result<Option<UnixTimestamp>> {
datapoint_info!(
"blockstore-rpc-api",
("method", "get_block_time".to_string(), String)
@ -1573,18 +1570,56 @@ impl Blockstore {
if *lowest_cleanup_slot > 0 && *lowest_cleanup_slot >= slot {
return Err(BlockstoreError::SlotCleanedUp);
}
self.blocktime_cf.get(slot)
}
fn get_timestamp_slots(&self, slot: Slot, timestamp_sample_range: usize) -> Vec<Slot> {
let root_iterator = self
.db
.iter::<cf::Root>(IteratorMode::From(slot, IteratorDirection::Reverse));
if !self.is_root(slot) || root_iterator.is_err() {
return vec![];
}
let mut get_slots = Measure::start("get_slots");
let mut timestamp_slots: Vec<Slot> = root_iterator
.unwrap()
.map(|(iter_slot, _)| iter_slot)
.take(timestamp_sample_range)
.collect();
timestamp_slots.sort();
get_slots.stop();
datapoint_info!(
"blockstore-get-timestamp-slots",
("slot", slot as i64, i64),
("get_slots_us", get_slots.as_us() as i64, i64)
);
timestamp_slots
}
pub fn cache_block_time(
&self,
slot: Slot,
slot_duration: Duration,
stakes: &HashMap<Pubkey, (u64, Account)>,
) -> Result<()> {
if !self.is_root(slot) {
return Err(BlockstoreError::SlotNotRooted);
}
let mut get_unique_timestamps = Measure::start("get_unique_timestamps");
let unique_timestamps: HashMap<Pubkey, (Slot, UnixTimestamp)> = self
.get_timestamp_slots(slot, TIMESTAMP_SLOT_INTERVAL, TIMESTAMP_SLOT_RANGE)
.get_timestamp_slots(slot, TIMESTAMP_SLOT_RANGE)
.into_iter()
.flat_map(|query_slot| self.get_block_timestamps(query_slot).unwrap_or_default())
.collect();
get_unique_timestamps.stop();
if unique_timestamps.is_empty() {
return Err(BlockstoreError::NoVoteTimestampsInRange);
}
let mut calculate_timestamp = Measure::start("calculate_timestamp");
let stake_weighted_timestamps =
calculate_stake_weighted_timestamp(unique_timestamps, stakes, slot, slot_duration);
let stake_weighted_timestamp =
calculate_stake_weighted_timestamp(unique_timestamps, stakes, slot, slot_duration)
.ok_or(BlockstoreError::EmptyEpochStakes)?;
calculate_timestamp.stop();
datapoint_info!(
"blockstore-get-block-time",
@ -1600,52 +1635,7 @@ impl Blockstore {
i64
)
);
Ok(stake_weighted_timestamps)
}
fn get_timestamp_slots(
&self,
slot: Slot,
timestamp_interval: u64,
timestamp_sample_range: usize,
) -> Vec<Slot> {
let baseline_slot = slot - (slot % timestamp_interval);
let root_iterator = self.db.iter::<cf::Root>(IteratorMode::From(
baseline_slot,
IteratorDirection::Forward,
));
if !self.is_root(slot) || root_iterator.is_err() {
return vec![];
}
let mut get_slots = Measure::start("get_slots");
let mut slots: Vec<Slot> = root_iterator
.unwrap()
.map(|(iter_slot, _)| iter_slot)
.take(timestamp_sample_range)
.filter(|&iter_slot| iter_slot <= slot)
.collect();
if slots.len() < timestamp_sample_range && baseline_slot >= timestamp_interval {
let earlier_baseline = baseline_slot - timestamp_interval;
let earlier_root_iterator = self.db.iter::<cf::Root>(IteratorMode::From(
earlier_baseline,
IteratorDirection::Forward,
));
if let Ok(iterator) = earlier_root_iterator {
slots = iterator
.map(|(iter_slot, _)| iter_slot)
.take(timestamp_sample_range)
.collect();
}
}
get_slots.stop();
datapoint_info!(
"blockstore-get-timestamp-slots",
("slot", slot as i64, i64),
("get_slots_us", get_slots.as_us() as i64, i64)
);
slots
self.blocktime_cf.put(slot, &stake_weighted_timestamp)
}
pub fn get_first_available_block(&self) -> Result<Slot> {
@ -1698,6 +1688,7 @@ impl Blockstore {
.unwrap_or_else(|| panic!("Rooted slot {:?} must have blockhash", slot));
let rewards = self.rewards_cf.get(slot)?.unwrap_or_else(Vec::new);
let block_time = self.blocktime_cf.get(slot)?;
let block = ConfirmedBlock {
previous_blockhash: previous_blockhash.to_string(),
@ -1709,7 +1700,7 @@ impl Blockstore {
slot_transaction_iterator,
),
rewards,
block_time: None, // See https://github.com/solana-labs/solana/issues/10089
block_time,
};
return Ok(block);
}
@ -5561,8 +5552,6 @@ pub mod tests {
fn test_get_timestamp_slots() {
let timestamp_sample_range = 5;
let ticks_per_slot = 5;
// Smaller interval than TIMESTAMP_SLOT_INTERVAL for convenience of building blockstore
let timestamp_interval = 7;
/*
Build a blockstore with < TIMESTAMP_SLOT_RANGE roots
*/
@ -5589,11 +5578,11 @@ pub mod tests {
blockstore.set_roots(&[1, 2, 3]).unwrap();
assert_eq!(
blockstore.get_timestamp_slots(2, timestamp_interval, timestamp_sample_range),
blockstore.get_timestamp_slots(2, timestamp_sample_range),
vec![0, 1, 2]
);
assert_eq!(
blockstore.get_timestamp_slots(3, timestamp_interval, timestamp_sample_range),
blockstore.get_timestamp_slots(3, timestamp_sample_range),
vec![0, 1, 2, 3]
);
@ -5601,14 +5590,13 @@ pub mod tests {
Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction");
/*
Build a blockstore in the ledger with the following rooted slots:
[0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 14, 15, 16, 17]
Build a blockstore in the ledger with gaps in rooted slot sequence
*/
let blockstore_path = get_tmp_ledger_path!();
let blockstore = Blockstore::open(&blockstore_path).unwrap();
blockstore.set_roots(&[0]).unwrap();
let desired_roots = vec![1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19];
let desired_roots = vec![1, 2, 3, 5, 6, 8, 11];
let mut last_entry_hash = Hash::default();
for (i, slot) in desired_roots.iter().enumerate() {
let parent = {
@ -5629,28 +5617,20 @@ pub mod tests {
blockstore.set_roots(&desired_roots).unwrap();
assert_eq!(
blockstore.get_timestamp_slots(2, timestamp_interval, timestamp_sample_range),
blockstore.get_timestamp_slots(2, timestamp_sample_range),
vec![0, 1, 2]
);
assert_eq!(
blockstore.get_timestamp_slots(6, timestamp_interval, timestamp_sample_range),
vec![0, 1, 2, 3, 4]
blockstore.get_timestamp_slots(6, timestamp_sample_range),
vec![1, 2, 3, 5, 6]
);
assert_eq!(
blockstore.get_timestamp_slots(8, timestamp_interval, timestamp_sample_range),
vec![0, 1, 2, 3, 4]
blockstore.get_timestamp_slots(8, timestamp_sample_range),
vec![2, 3, 5, 6, 8]
);
assert_eq!(
blockstore.get_timestamp_slots(13, timestamp_interval, timestamp_sample_range),
vec![8, 9, 10, 11, 12]
);
assert_eq!(
blockstore.get_timestamp_slots(18, timestamp_interval, timestamp_sample_range),
vec![8, 9, 10, 11, 12]
);
assert_eq!(
blockstore.get_timestamp_slots(19, timestamp_interval, timestamp_sample_range),
vec![14, 16, 17, 18, 19]
blockstore.get_timestamp_slots(11, timestamp_sample_range),
vec![3, 5, 6, 8, 11]
);
}
@ -5754,7 +5734,7 @@ pub mod tests {
let confirmed_block = ledger.get_confirmed_block(slot + 1, None).unwrap();
assert_eq!(confirmed_block.transactions.len(), 100);
let expected_block = ConfirmedBlock {
let mut expected_block = ConfirmedBlock {
transactions: expected_transactions
.iter()
.cloned()
@ -5774,6 +5754,14 @@ pub mod tests {
let not_root = ledger.get_confirmed_block(slot + 2, None).unwrap_err();
assert_matches!(not_root, BlockstoreError::SlotNotRooted);
// Test block_time returns, if available
let timestamp = 1_576_183_541;
ledger.blocktime_cf.put(slot + 1, &timestamp).unwrap();
expected_block.block_time = Some(timestamp);
let confirmed_block = ledger.get_confirmed_block(slot + 1, None).unwrap();
assert_eq!(confirmed_block, expected_block);
drop(ledger);
Blockstore::destroy(&ledger_path).expect("Expected successful database destruction");
}
@ -5821,14 +5809,25 @@ pub mod tests {
);
assert_eq!(blockstore.get_block_timestamps(2).unwrap(), vec![]);
// Build epoch vote_accounts HashMap to test stake-weighted block time
blockstore.set_roots(&[3, 8]).unwrap();
let mut stakes = HashMap::new();
let slot_duration = Duration::from_millis(400);
for slot in &[1, 2, 3, 8] {
assert!(blockstore
.cache_block_time(*slot, slot_duration, &stakes)
.is_err());
}
// Build epoch vote_accounts HashMap to test stake-weighted block time
for (i, keypair) in vote_keypairs.iter().enumerate() {
stakes.insert(keypair.pubkey(), (1 + i as u64, Account::default()));
}
let slot_duration = Duration::from_millis(400);
let block_time_slot_3 = blockstore.get_block_time(3, slot_duration, &stakes);
for slot in &[1, 2, 3, 8] {
blockstore
.cache_block_time(*slot, slot_duration, &stakes)
.unwrap();
}
let block_time_slot_3 = blockstore.get_block_time(3);
let mut total_stake = 0;
let mut expected_time: u64 = (0..6)
@ -5844,14 +5843,53 @@ pub mod tests {
expected_time /= total_stake;
assert_eq!(block_time_slot_3.unwrap().unwrap() as u64, expected_time);
assert_eq!(
blockstore
.get_block_time(8, slot_duration, &stakes)
.unwrap()
.unwrap() as u64,
blockstore.get_block_time(8).unwrap().unwrap() as u64,
expected_time + 2 // At 400ms block duration, 5 slots == 2sec
);
}
#[test]
fn test_get_block_time_no_timestamps() {
let vote_keypairs: Vec<Keypair> = (0..6).map(|_| Keypair::new()).collect();
// Populate slot 1 with vote transactions, none of which have timestamps
let mut vote_entries: Vec<Entry> = Vec::new();
for (i, keypair) in vote_keypairs.iter().enumerate() {
let vote = Vote {
slots: vec![1],
hash: Hash::default(),
timestamp: None,
};
let vote_ix = vote_instruction::vote(&keypair.pubkey(), &keypair.pubkey(), vote);
let vote_msg = Message::new(&[vote_ix], Some(&keypair.pubkey()));
let vote_tx = Transaction::new(&[keypair], vote_msg, Hash::default());
vote_entries.push(next_entry_mut(&mut Hash::default(), 0, vec![vote_tx]));
let mut tick = create_ticks(1, 0, hash(&serialize(&i).unwrap()));
vote_entries.append(&mut tick);
}
let shreds = entries_to_test_shreds(vote_entries, 1, 0, true, 0);
let ledger_path = get_tmp_ledger_path!();
let blockstore = Blockstore::open(&ledger_path).unwrap();
blockstore.insert_shreds(shreds, None, false).unwrap();
// Populate slot 2 with ticks only
fill_blockstore_slot_with_ticks(&blockstore, 6, 2, 1, Hash::default());
blockstore.set_roots(&[0, 1, 2]).unwrap();
// Build epoch vote_accounts HashMap to test stake-weighted block time
let mut stakes = HashMap::new();
for (i, keypair) in vote_keypairs.iter().enumerate() {
stakes.insert(keypair.pubkey(), (1 + i as u64, Account::default()));
}
let slot_duration = Duration::from_millis(400);
for slot in &[1, 2, 3, 8] {
assert!(blockstore
.cache_block_time(*slot, slot_duration, &stakes)
.is_err());
assert_eq!(blockstore.get_block_time(*slot).unwrap(), None);
}
}
#[test]
fn test_calculate_stake_weighted_timestamp() {
let recent_timestamp: UnixTimestamp = 1_578_909_061;

View File

@ -133,6 +133,10 @@ impl Blockstore {
& self
.db
.delete_range_cf::<cf::Rewards>(&mut write_batch, from_slot, to_slot)
.is_ok()
& self
.db
.delete_range_cf::<cf::Blocktime>(&mut write_batch, from_slot, to_slot)
.is_ok();
let mut w_active_transaction_status_index =
self.active_transaction_status_index.write().unwrap();
@ -223,6 +227,10 @@ impl Blockstore {
&& self
.rewards_cf
.compact_range(from_slot, to_slot)
.unwrap_or(false)
&& self
.blocktime_cf
.compact_range(from_slot, to_slot)
.unwrap_or(false);
compact_timer.stop();
if !result {

View File

@ -10,7 +10,11 @@ use rocksdb::{
use serde::de::DeserializeOwned;
use serde::Serialize;
use solana_runtime::hardened_unpack::UnpackError;
use solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature};
use solana_sdk::{
clock::{Slot, UnixTimestamp},
pubkey::Pubkey,
signature::Signature,
};
use solana_transaction_status::{Rewards, TransactionStatusMeta};
use std::{collections::HashMap, fs, marker::PhantomData, path::Path, sync::Arc};
use thiserror::Error;
@ -46,6 +50,8 @@ const ADDRESS_SIGNATURES_CF: &str = "address_signatures";
const TRANSACTION_STATUS_INDEX_CF: &str = "transaction_status_index";
/// Column family for Rewards
const REWARDS_CF: &str = "rewards";
/// Column family for Blocktime
const BLOCKTIME_CF: &str = "blocktime";
#[derive(Error, Debug)]
pub enum BlockstoreError {
@ -61,6 +67,8 @@ pub enum BlockstoreError {
UnpackError(#[from] UnpackError),
UnableToSetOpenFileDescriptorLimit,
TransactionStatusSlotMismatch,
EmptyEpochStakes,
NoVoteTimestampsInRange,
}
pub type Result<T> = std::result::Result<T, BlockstoreError>;
@ -128,6 +136,10 @@ pub mod columns {
#[derive(Debug)]
/// The rewards column
pub struct Rewards;
#[derive(Debug)]
/// The blocktime column
pub struct Blocktime;
}
pub enum AccessType {
@ -187,8 +199,9 @@ impl Rocks {
recovery_mode: Option<BlockstoreRecoveryMode>,
) -> Result<Rocks> {
use columns::{
AddressSignatures, DeadSlots, DuplicateSlots, ErasureMeta, Index, Orphans, Rewards,
Root, ShredCode, ShredData, SlotMeta, TransactionStatus, TransactionStatusIndex,
AddressSignatures, Blocktime, DeadSlots, DuplicateSlots, ErasureMeta, Index, Orphans,
Rewards, Root, ShredCode, ShredData, SlotMeta, TransactionStatus,
TransactionStatusIndex,
};
fs::create_dir_all(&path)?;
@ -221,6 +234,8 @@ impl Rocks {
let transaction_status_index_cf_descriptor =
ColumnFamilyDescriptor::new(TransactionStatusIndex::NAME, get_cf_options());
let rewards_cf_descriptor = ColumnFamilyDescriptor::new(Rewards::NAME, get_cf_options());
let blocktime_cf_descriptor =
ColumnFamilyDescriptor::new(Blocktime::NAME, get_cf_options());
let cfs = vec![
(SlotMeta::NAME, meta_cf_descriptor),
@ -239,6 +254,7 @@ impl Rocks {
transaction_status_index_cf_descriptor,
),
(Rewards::NAME, rewards_cf_descriptor),
(Blocktime::NAME, blocktime_cf_descriptor),
];
// Open the database
@ -276,8 +292,9 @@ impl Rocks {
fn columns(&self) -> Vec<&'static str> {
use columns::{
AddressSignatures, DeadSlots, DuplicateSlots, ErasureMeta, Index, Orphans, Rewards,
Root, ShredCode, ShredData, SlotMeta, TransactionStatus, TransactionStatusIndex,
AddressSignatures, Blocktime, DeadSlots, DuplicateSlots, ErasureMeta, Index, Orphans,
Rewards, Root, ShredCode, ShredData, SlotMeta, TransactionStatus,
TransactionStatusIndex,
};
vec![
@ -294,6 +311,7 @@ impl Rocks {
AddressSignatures::NAME,
TransactionStatusIndex::NAME,
Rewards::NAME,
Blocktime::NAME,
]
}
@ -519,6 +537,14 @@ impl TypedColumn for columns::Rewards {
type Type = Rewards;
}
impl SlotColumn for columns::Blocktime {}
impl ColumnName for columns::Blocktime {
const NAME: &'static str = BLOCKTIME_CF;
}
impl TypedColumn for columns::Blocktime {
type Type = UnixTimestamp;
}
impl Column for columns::ShredCode {
type Index = (u64, u64);