lighthouse/beacon_node/store/src/hot_cold_store.rs
Michael Sproul bf2eeae3f2 Implement freezer database (#508)
* Implement freezer database for state vectors

* Improve BeaconState safe accessors

And fix a bug in the compact committees accessor.

* Banish dodgy type bounds back to gRPC

* Clean up

* Switch to exclusive end points in chunked vec

* Cleaning up and start of tests

* Randao fix, more tests

* Fix unsightly hack

* Resolve test FIXMEs

* Config file support

* More clean-ups, migrator beginnings

* Finish migrator, integrate into BeaconChain

* Fixups

* Fix store tests

* Fix BeaconChain tests

* Fix LMD GHOST tests

* Address review comments, delete 'static bounds

* Cargo format

* Address review comments

* Fix LMD ghost tests

* Update to spec v0.9.0

* Update to v0.9.1

* Bump spec tags for v0.9.1

* Formatting, fix CI failures

* Resolve accidental KeyPair merge conflict

* Document new BeaconState functions

* Fix incorrect cache drops in `advance_caches`

* Update fork choice for v0.9.1

* Clean up some FIXMEs

* Fix a few docs/logs

* Update for new builder paradigm, spec changes

* Freezer DB integration into BeaconNode

* Cleaning up

* This works, clean it up

* Cleanups

* Fix and improve store tests

* Refine store test

* Delete unused beacon_chain_builder.rs

* Fix CLI

* Store state at split slot in hot database

* Make fork choice lookup fast again

* Store freezer DB split slot in the database

* Handle potential div by 0 in chunked_vector

* Exclude committee caches from freezer DB

* Remove FIXME about long-running test
2019-11-27 10:54:46 +11:00

261 lines
8.0 KiB
Rust

use crate::chunked_vector::{
store_updated_vector, BlockRoots, HistoricalRoots, RandaoMixes, StateRoots,
};
use crate::iter::StateRootsIterator;
use crate::{
leveldb_store::LevelDB, DBColumn, Error, PartialBeaconState, SimpleStoreItem, Store, StoreItem,
};
use parking_lot::RwLock;
use slog::{info, trace, Logger};
use ssz::{Decode, Encode};
use std::convert::TryInto;
use std::path::Path;
use std::sync::Arc;
use types::*;
/// 32-byte key for accessing the `split_slot` of the freezer DB.
pub const SPLIT_SLOT_DB_KEY: &str = "FREEZERDBSPLITSLOTFREEZERDBSPLIT";
pub struct HotColdDB {
/// The slot before which all data is stored in the cold database.
///
/// Data for slots less than `split_slot` is in the cold DB, while data for slots
/// greater than or equal is in the hot DB.
split_slot: RwLock<Slot>,
/// Cold database containing compact historical data.
cold_db: LevelDB,
/// Hot database containing duplicated but quick-to-access recent data.
hot_db: LevelDB,
/// Chain spec.
spec: ChainSpec,
/// Logger.
pub(crate) log: Logger,
}
#[derive(Debug, PartialEq)]
pub enum HotColdDbError {
FreezeSlotError {
current_split_slot: Slot,
proposed_split_slot: Slot,
},
}
impl Store for HotColdDB {
// Defer to the hot database for basic operations (including blocks for now)
fn get_bytes(&self, column: &str, key: &[u8]) -> Result<Option<Vec<u8>>, Error> {
self.hot_db.get_bytes(column, key)
}
fn put_bytes(&self, column: &str, key: &[u8], value: &[u8]) -> Result<(), Error> {
self.hot_db.put_bytes(column, key, value)
}
fn key_exists(&self, column: &str, key: &[u8]) -> Result<bool, Error> {
self.hot_db.key_exists(column, key)
}
fn key_delete(&self, column: &str, key: &[u8]) -> Result<(), Error> {
self.hot_db.key_delete(column, key)
}
/// Store a state in the store.
fn put_state<E: EthSpec>(
&self,
state_root: &Hash256,
state: &BeaconState<E>,
) -> Result<(), Error> {
if state.slot < self.get_split_slot() {
self.store_archive_state(state_root, state)
} else {
self.hot_db.put_state(state_root, state)
}
}
/// Fetch a state from the store.
fn get_state<E: EthSpec>(
&self,
state_root: &Hash256,
slot: Option<Slot>,
) -> Result<Option<BeaconState<E>>, Error> {
if let Some(slot) = slot {
if slot < self.get_split_slot() {
self.load_archive_state(state_root)
} else {
self.hot_db.get_state(state_root, None)
}
} else {
match self.hot_db.get_state(state_root, None)? {
Some(state) => Ok(Some(state)),
None => self.load_archive_state(state_root),
}
}
}
fn freeze_to_state<E: EthSpec>(
store: Arc<Self>,
_frozen_head_root: Hash256,
frozen_head: &BeaconState<E>,
) -> Result<(), Error> {
info!(
store.log,
"Freezer migration started";
"slot" => frozen_head.slot
);
// 1. Copy all of the states between the head and the split slot, from the hot DB
// to the cold DB.
let current_split_slot = store.get_split_slot();
if frozen_head.slot < current_split_slot {
Err(HotColdDbError::FreezeSlotError {
current_split_slot,
proposed_split_slot: frozen_head.slot,
})?;
}
let state_root_iter = StateRootsIterator::new(store.clone(), frozen_head);
let mut to_delete = vec![];
for (state_root, slot) in
state_root_iter.take_while(|&(_, slot)| slot >= current_split_slot)
{
trace!(store.log, "Freezing";
"slot" => slot,
"state_root" => format!("{}", state_root));
let state: BeaconState<E> = match store.hot_db.get_state(&state_root, None)? {
Some(s) => s,
// If there's no state it could be a skip slot, which is fine, our job is just
// to move everything that was in the hot DB to the cold.
None => continue,
};
to_delete.push(state_root);
store.store_archive_state(&state_root, &state)?;
}
// 2. Update the split slot
*store.split_slot.write() = frozen_head.slot;
store.store_split_slot()?;
// 3. Delete from the hot DB
for state_root in to_delete {
store
.hot_db
.key_delete(DBColumn::BeaconState.into(), state_root.as_bytes())?;
}
info!(
store.log,
"Freezer migration complete";
"slot" => frozen_head.slot
);
Ok(())
}
}
impl HotColdDB {
pub fn open(
hot_path: &Path,
cold_path: &Path,
spec: ChainSpec,
log: Logger,
) -> Result<Self, Error> {
let db = HotColdDB {
split_slot: RwLock::new(Slot::new(0)),
cold_db: LevelDB::open(cold_path)?,
hot_db: LevelDB::open(hot_path)?,
spec,
log,
};
// Load the previous split slot from the database (if any). This ensures we can
// stop and restart correctly.
if let Some(split_slot) = db.load_split_slot()? {
*db.split_slot.write() = split_slot;
}
Ok(db)
}
pub fn store_archive_state<E: EthSpec>(
&self,
state_root: &Hash256,
state: &BeaconState<E>,
) -> Result<(), Error> {
trace!(
self.log,
"Freezing state";
"slot" => state.slot.as_u64(),
"state_root" => format!("{:?}", state_root)
);
// 1. Convert to PartialBeaconState and store that in the DB.
let partial_state = PartialBeaconState::from_state_forgetful(state);
partial_state.db_put(&self.cold_db, state_root)?;
// 2. Store updated vector entries.
let db = &self.cold_db;
store_updated_vector(BlockRoots, db, state, &self.spec)?;
store_updated_vector(StateRoots, db, state, &self.spec)?;
store_updated_vector(HistoricalRoots, db, state, &self.spec)?;
store_updated_vector(RandaoMixes, db, state, &self.spec)?;
Ok(())
}
pub fn load_archive_state<E: EthSpec>(
&self,
state_root: &Hash256,
) -> Result<Option<BeaconState<E>>, Error> {
let mut partial_state = match PartialBeaconState::db_get(&self.cold_db, state_root)? {
Some(s) => s,
None => return Ok(None),
};
// Fill in the fields of the partial state.
partial_state.load_block_roots(&self.cold_db, &self.spec)?;
partial_state.load_state_roots(&self.cold_db, &self.spec)?;
partial_state.load_historical_roots(&self.cold_db, &self.spec)?;
partial_state.load_randao_mixes(&self.cold_db, &self.spec)?;
let state: BeaconState<E> = partial_state.try_into()?;
Ok(Some(state))
}
pub fn get_split_slot(&self) -> Slot {
*self.split_slot.read()
}
fn load_split_slot(&self) -> Result<Option<Slot>, Error> {
let key = Hash256::from_slice(SPLIT_SLOT_DB_KEY.as_bytes());
let split_slot: Option<SplitSlot> = self.hot_db.get(&key)?;
Ok(split_slot.map(|s| Slot::new(s.0)))
}
fn store_split_slot(&self) -> Result<(), Error> {
let key = Hash256::from_slice(SPLIT_SLOT_DB_KEY.as_bytes());
self.hot_db
.put(&key, &SplitSlot(self.get_split_slot().as_u64()))?;
Ok(())
}
}
/// Struct for storing the split slot in the database.
#[derive(Clone, Copy)]
struct SplitSlot(u64);
impl SimpleStoreItem for SplitSlot {
fn db_column() -> DBColumn {
DBColumn::BeaconMeta
}
fn as_store_bytes(&self) -> Vec<u8> {
self.0.as_ssz_bytes()
}
fn from_store_bytes(bytes: &[u8]) -> Result<Self, Error> {
Ok(SplitSlot(u64::from_ssz_bytes(bytes)?))
}
}