mirror of
https://github.com/NLnetLabs/krill.git
synced 2026-09-27 11:54:52 +02:00
399 lines
15 KiB
Rust
399 lines
15 KiB
Rust
use std::{
|
|
collections::HashMap,
|
|
fmt::{self},
|
|
path::Path,
|
|
str::FromStr,
|
|
sync::{Arc, RwLock},
|
|
};
|
|
|
|
use rpki::ca::idexchange::MyHandle;
|
|
|
|
use crate::commons::eventsourcing::{locks::HandleLocks, KeyStoreKey, KeyValueError, KeyValueStore, Storable};
|
|
|
|
//------------ WalSupport ----------------------------------------------------
|
|
|
|
/// Implement this trait to get write-ahead logging support for a type.
|
|
///
|
|
/// We achieve write-ahead logging support by insisting that implementing
|
|
/// types define the following:
|
|
///
|
|
/// - commands
|
|
///
|
|
/// Commands are used to send an intent to change the state. However, rather
|
|
/// than changing the state, they return a result which can either be an
|
|
/// error or a list of 'events'.
|
|
///
|
|
/// - events
|
|
///
|
|
/// Events contain the data that can be applied to a type to change its
|
|
/// state. We do this as a separate step, because this will allow us to
|
|
/// replay events - from write-ahead logs - to get a stored snapshot to
|
|
/// a current state.
|
|
///
|
|
/// The following caveats apply to this:
|
|
/// -- Events MUST NOT cause side-effects
|
|
/// -- Events MUST NOT return errors when applied
|
|
/// -- All state changes MUST use events
|
|
///
|
|
/// - errors
|
|
///
|
|
/// So that we can have type specific errors.
|
|
///
|
|
/// This is similar to how the [`Aggregate`] trait works, and in fact
|
|
/// we re-use some its definitions here - such as [`Event`] and [`Command`].
|
|
///
|
|
/// But, there is a key difference which is that in this case there are
|
|
/// no guarantees that all past events are kept - or rather they are very
|
|
/// likely NOT kept. And we have no "init" event.
|
|
///
|
|
/// While there are similar concepts being used, the concerns here are
|
|
/// somewhat different.. we use this type to achieve atomicity and durability
|
|
/// by way of the [`WalStore`] defined below, but we can keep things a bit
|
|
/// simpler here compared to the fully event-sourced [`Aggregate`] types.
|
|
pub trait WalSupport: Storable {
|
|
type Command: WalCommand;
|
|
type Change: WalChange;
|
|
type Error: std::error::Error + From<WalStoreError>;
|
|
|
|
/// Returns the current version.
|
|
fn revision(&self) -> u64;
|
|
|
|
/// Applies the event to this. This MUST not result in any errors, and
|
|
/// this MUST be side-effect free. Applying the event just updates the
|
|
/// internal data of the aggregate.
|
|
///
|
|
/// Note the event is moved. This is done because we want to avoid
|
|
/// doing additional allocations where we can.
|
|
fn apply(&mut self, set: WalSet<Self>);
|
|
|
|
/// Processes a command. I.e. validate the command, and return a list of
|
|
/// events that will result in the desired new state, but do not apply
|
|
/// these events here.
|
|
///
|
|
/// The command is moved, because we want to enable moving its data
|
|
/// without reallocating.
|
|
fn process_command(&self, command: Self::Command) -> Result<Vec<Self::Change>, Self::Error>;
|
|
}
|
|
|
|
//------------ WalCommand ----------------------------------------------------
|
|
|
|
pub trait WalCommand: fmt::Display {
|
|
fn handle(&self) -> &MyHandle;
|
|
}
|
|
|
|
//------------ WalEvent ------------------------------------------------------
|
|
|
|
pub trait WalChange: fmt::Display + Eq + PartialEq + Send + Sync + Storable {}
|
|
|
|
//------------ WalSet --------------------------------------------------------
|
|
|
|
/// Describes a set of "write-ahead" changes affecting the specified revision.
|
|
/// Meaning that it can only be applied if the type is of the given revision, and
|
|
/// it will get this revision + 1 after it has been applied.
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
pub struct WalSet<T: WalSupport> {
|
|
revision: u64,
|
|
summary: String,
|
|
changes: Vec<T::Change>,
|
|
}
|
|
|
|
impl<T: WalSupport> WalSet<T> {
|
|
pub fn into_changes(self) -> Vec<T::Change> {
|
|
self.changes
|
|
}
|
|
}
|
|
|
|
//------------ WalStore ------------------------------------------------------
|
|
|
|
/// This type is responsible for loading / saving and updating [`WalSupport`]
|
|
/// capable types.
|
|
///
|
|
/// This is similar to how [`AggregateStore`] is used to manage [`Aggregate`]
|
|
/// types. However, there are some important differences:
|
|
/// - Commands and events for a change are saved as a single file.
|
|
/// - Old commands and events are no longer relevant and will be removed.
|
|
/// (we may want to support archiving those in future).
|
|
/// - We do not have any listeners in this case.
|
|
/// - We cannot replay [`WriteAheadSupport`] types from just events, we
|
|
/// *always* need to start with an existing snapshot.
|
|
#[derive(Debug)]
|
|
pub struct WalStore<T: WalSupport> {
|
|
kv: KeyValueStore,
|
|
cache: RwLock<HashMap<MyHandle, Arc<T>>>,
|
|
locks: HandleLocks,
|
|
}
|
|
|
|
impl<T: WalSupport> WalStore<T> {
|
|
/// Creates a new store using a disk based keystore for the given data
|
|
/// directory and namespace (directory).
|
|
pub fn disk(krill_data_dir: &Path, name_space: &str) -> WalStoreResult<Self> {
|
|
let mut path = krill_data_dir.to_path_buf();
|
|
path.push(name_space);
|
|
|
|
let kv = KeyValueStore::disk(krill_data_dir, name_space)?;
|
|
let cache = RwLock::new(HashMap::new());
|
|
let locks = HandleLocks::default();
|
|
|
|
Ok(WalStore { kv, cache, locks })
|
|
}
|
|
|
|
/// Warms up the store: caches all instances.
|
|
pub fn warm(&self) -> WalStoreResult<()> {
|
|
for handle in self.list()? {
|
|
let latest = self
|
|
.get_latest(&handle)
|
|
.map_err(|e| WalStoreError::WarmupFailed(handle.clone(), e.to_string()))?;
|
|
|
|
self.cache.write().unwrap().insert(handle, latest);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Add a new entity for the given handle. Fails if the handle is in use.
|
|
pub fn add(&self, handle: &MyHandle, instance: T) -> WalStoreResult<()> {
|
|
let handle_lock = self.locks.for_handle(handle.clone());
|
|
let _write = handle_lock.write();
|
|
|
|
let instance = Arc::new(instance);
|
|
let key = Self::key_for_snapshot(handle);
|
|
self.kv.store_new(&key, &instance)?; // Fails if this key exists
|
|
self.cache.write().unwrap().insert(handle.clone(), instance);
|
|
Ok(())
|
|
}
|
|
|
|
/// Checks whether there is an instance for the given handle.
|
|
pub fn has(&self, handle: &MyHandle) -> WalStoreResult<bool> {
|
|
let key = Self::key_for_snapshot(handle);
|
|
self.kv.has(&key).map_err(WalStoreError::KeyStoreError)
|
|
}
|
|
|
|
/// Get the latest revision for the given handle.
|
|
///
|
|
/// This will use the cache if it's available and otherwise get a snapshot
|
|
/// from the keystore. Then it will check whether there are any further
|
|
/// changes.
|
|
pub fn get_latest(&self, handle: &MyHandle) -> WalStoreResult<Arc<T>> {
|
|
let handle_lock = self.locks.for_handle(handle.clone());
|
|
let _read = handle_lock.read();
|
|
|
|
self.get_latest_no_lock(handle)
|
|
}
|
|
|
|
/// Get the latest revision without using a lock.
|
|
///
|
|
/// Intended to be used by public functions which manage the locked read/write access
|
|
/// to this instance for this handle.
|
|
fn get_latest_no_lock(&self, handle: &MyHandle) -> WalStoreResult<Arc<T>> {
|
|
let mut instance = match self.cache.read().unwrap().get(handle).cloned() {
|
|
None => Arc::new(self.get_snapshot(handle)?),
|
|
Some(instance) => instance,
|
|
};
|
|
|
|
if !self.kv.has(&Self::key_for_wal_set(handle, instance.revision()))? {
|
|
// No further changes for this revision exist.
|
|
//
|
|
// Note: this is expected to be the case if our cached instances
|
|
// are kept up-to-date, and we run on a single node. Double
|
|
// checking this should not be too expensive though, and it
|
|
// allows us to use same code path for warming the cache and
|
|
// for getting the latest instance in other cases.
|
|
Ok(instance)
|
|
} else {
|
|
// Changes exist:
|
|
// - apply all of them
|
|
// - update the cache instance
|
|
// - return updated
|
|
let instance = Arc::make_mut(&mut instance);
|
|
|
|
loop {
|
|
let wal_set_key = Self::key_for_wal_set(handle, instance.revision());
|
|
if let Some(set) = self.kv.get(&wal_set_key)? {
|
|
instance.apply(set)
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let instance = Arc::new(instance.clone());
|
|
self.cache.write().unwrap().insert(handle.clone(), instance.clone());
|
|
Ok(instance)
|
|
}
|
|
}
|
|
|
|
/// Remove an instance from this store. Irrevocable.
|
|
pub fn remove(&self, handle: &MyHandle) -> WalStoreResult<()> {
|
|
if !self.has(handle)? {
|
|
Err(WalStoreError::Unknown(handle.clone()))
|
|
} else {
|
|
{
|
|
// First get a lock and remove the object
|
|
let handle_lock = self.locks.for_handle(handle.clone());
|
|
let _write = handle_lock.write();
|
|
self.cache.write().unwrap().remove(handle);
|
|
self.kv.drop_scope(handle.as_str())?;
|
|
}
|
|
|
|
// Then drop the lock for it as well. We could not do this
|
|
// while holding the write lock.
|
|
//
|
|
// Note that the corresponding entity was removed from the key
|
|
// value store while we had a write lock for its handle.
|
|
// So, even if another concurrent thread would now try to update
|
|
// this same entity, that update would fail because the entity
|
|
// no longer exists.
|
|
self.locks.drop_handle(handle);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn get_snapshot(&self, handle: &MyHandle) -> WalStoreResult<T> {
|
|
self.kv
|
|
.get(&Self::key_for_snapshot(handle))?
|
|
.ok_or_else(|| WalStoreError::Unknown(handle.clone()))
|
|
}
|
|
|
|
/// Returns a list of all instances managed in this store.
|
|
pub fn list(&self) -> WalStoreResult<Vec<MyHandle>> {
|
|
let mut res = vec![];
|
|
|
|
for scope in self.kv.scopes()? {
|
|
if let Ok(handle) = MyHandle::from_str(&scope) {
|
|
res.push(handle)
|
|
}
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
/// Process a command:
|
|
/// - gets the instance for the command
|
|
/// - sends the command
|
|
/// - in case the command is successful
|
|
/// - apply the wal set locally
|
|
/// - save the wal set
|
|
/// - if saved properly update the cache
|
|
///
|
|
///
|
|
///
|
|
pub fn send_command(&self, command: T::Command) -> Result<Arc<T>, T::Error> {
|
|
let handle = command.handle().clone();
|
|
|
|
let handle_lock = self.locks.for_handle(handle.clone());
|
|
let _write = handle_lock.write();
|
|
|
|
let mut latest = self.get_latest_no_lock(&handle)?;
|
|
|
|
let summary = command.to_string();
|
|
let revision = latest.revision();
|
|
let changes = latest.process_command(command)?;
|
|
|
|
if changes.is_empty() {
|
|
debug!("No changes need for '{}' when processing command: {}", handle, summary);
|
|
Ok(latest)
|
|
} else {
|
|
// lock the cache first, before writing any updates
|
|
let mut cache = self.cache.write().unwrap();
|
|
|
|
let set: WalSet<T> = WalSet {
|
|
revision,
|
|
summary,
|
|
changes,
|
|
};
|
|
|
|
let key_for_wal_set = Self::key_for_wal_set(&handle, revision);
|
|
self.kv
|
|
.store_new(&key_for_wal_set, &set)
|
|
.map_err(WalStoreError::KeyStoreError)?;
|
|
|
|
let latest = Arc::make_mut(&mut latest);
|
|
latest.apply(set);
|
|
|
|
let latest = Arc::new(latest.clone());
|
|
cache.insert(handle, latest.clone());
|
|
|
|
Ok(latest)
|
|
}
|
|
}
|
|
|
|
/// Update snapshot and archive or delete old wal sets
|
|
///
|
|
/// This is a separate function because serializing a large instance can
|
|
/// be expensive.
|
|
pub fn update_snapshot(&self, handle: &MyHandle, archive: bool) -> WalStoreResult<()> {
|
|
// Note that we do not need to keep a lock for the instance when we update the snapshot.
|
|
// This function just updates the latest snapshot in the key value store, and it removes
|
|
// or archives all write-ahead log ("wal-") changes predating the new snapshot.
|
|
//
|
|
// It is fine if another thread gets the entity for this handle and updates it while we
|
|
// do do this. As it turns out, writing snapshots can be expensive for large objects, so
|
|
// we do not want block updates while we do this.
|
|
//
|
|
// This function is intended to be called in the back-ground at regular (slow) intervals
|
|
// so any updates that were just missed will simply be folded in to the new snapshot when
|
|
// this function is called again.
|
|
let latest = self.get_latest(handle)?;
|
|
let key = Self::key_for_snapshot(handle);
|
|
self.kv.store(&key, &latest)?;
|
|
|
|
// Archive or delete old wal sets
|
|
for key in self.kv.keys(Some(handle.to_string()), "wal-")? {
|
|
// Carefully inspect the key, just ignore keys
|
|
// following a format that is not expected.
|
|
// Who knows what people write in this dir?
|
|
if let Some(remaining) = key.name().strip_prefix("wal-") {
|
|
if let Some(number) = remaining.strip_suffix(".json") {
|
|
if let Ok(revision) = u64::from_str(number) {
|
|
if revision < latest.revision() {
|
|
if archive {
|
|
self.kv.archive(&key)?;
|
|
} else {
|
|
self.kv.drop_key(&key)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn key_for_snapshot(handle: &MyHandle) -> KeyStoreKey {
|
|
KeyStoreKey::scoped(handle.to_string(), "snapshot.json".to_string())
|
|
}
|
|
|
|
fn key_for_wal_set(handle: &MyHandle, revision: u64) -> KeyStoreKey {
|
|
KeyStoreKey::scoped(handle.to_string(), format!("wal-{}.json", revision))
|
|
}
|
|
}
|
|
|
|
//------------ WalStoreResult-------------------------------------------------
|
|
|
|
pub type WalStoreResult<T> = Result<T, WalStoreError>;
|
|
|
|
//------------ WalStoreError -------------------------------------------------
|
|
|
|
/// This type defines possible Errors for the AggregateStore
|
|
#[derive(Debug)]
|
|
pub enum WalStoreError {
|
|
KeyStoreError(KeyValueError),
|
|
Unknown(MyHandle),
|
|
WarmupFailed(MyHandle, String),
|
|
}
|
|
|
|
impl From<KeyValueError> for WalStoreError {
|
|
fn from(e: KeyValueError) -> Self {
|
|
WalStoreError::KeyStoreError(e)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for WalStoreError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
match self {
|
|
WalStoreError::KeyStoreError(e) => write!(f, "KeyStore Error: {}", e),
|
|
WalStoreError::Unknown(handle) => write!(f, "Unknown entity: {}", handle),
|
|
WalStoreError::WarmupFailed(handle, e) => write!(f, "Warmup failed with entity '{}' error: {}", handle, e),
|
|
}
|
|
}
|
|
}
|