use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::path::{Path, PathBuf};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::grid::elasticity::RegionId;
use crate::grid::persistence_policy::{
NamespacePersistenceRule, NamespacePersistenceSettings, PersistenceDurability,
PersistenceEviction, PersistenceInMemoryFormat, PersistenceMaintenance, PersistencePolicy,
PersistencePolicyError, RegionSelector,
};
use crate::grid::recovery::{RecoveryMode, RecoveryPolicy};
pub const OTHER_NAMESPACE_METRIC_LABEL: &str = "other";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct PersistenceConfig {
pub storage_dir: Option<PathBuf>,
pub snapshot_interval_default_secs: Option<u64>,
pub recovery: PersistenceRecoveryConfig,
pub namespaces: BTreeMap<String, PersistenceNamespaceConfig>,
}
impl PersistenceConfig {
pub fn requests_persistence(&self) -> bool {
self.namespaces.values().any(|namespace| namespace.persist)
}
pub fn to_policy(&self) -> Result<PersistencePolicy, PersistenceConfigError> {
let rules = self
.namespaces
.iter()
.map(|(pattern, namespace)| {
let mut settings = namespace.to_settings();
if settings.persist && settings.snapshot_interval.is_none() {
settings.snapshot_interval =
self.snapshot_interval_default_secs.map(Duration::from_secs);
}
NamespacePersistenceRule::new(pattern.clone(), settings)
.map_err(PersistenceConfigError::policy)
})
.collect::<Result<Vec<_>, _>>()?;
PersistencePolicy::try_new(rules).map_err(PersistenceConfigError::policy)
}
pub fn to_recovery_policy(&self) -> RecoveryPolicy {
self.recovery.to_policy()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn validate_startup(
&self,
durable_engine_available: bool,
) -> Result<(), PersistenceConfigError> {
let policy = self.to_policy()?;
policy
.validate_engine_available(durable_engine_available)
.map_err(|error| {
PersistenceConfigError::new(
PersistenceConfigErrorKind::DurableEngineUnavailable,
error.to_string(),
)
})?;
if !self.requests_persistence() {
return Ok(());
}
let Some(storage_dir) = &self.storage_dir else {
return Err(PersistenceConfigError::new(
PersistenceConfigErrorKind::MissingStorageDir,
"persistence requested but no storage_dir is configured",
));
};
validate_storage_dir(storage_dir)?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct PersistenceRecoveryConfig {
pub mode: RecoveryMode,
pub validation_timeout_secs: u64,
pub data_load_timeout_secs: u64,
pub auto_remove_stale_data: bool,
}
impl PersistenceRecoveryConfig {
pub fn to_policy(&self) -> RecoveryPolicy {
RecoveryPolicy {
mode: self.mode,
validation_timeout: Duration::from_secs(self.validation_timeout_secs),
data_load_timeout: Duration::from_secs(self.data_load_timeout_secs),
auto_remove_stale_data: self.auto_remove_stale_data,
}
}
}
impl Default for PersistenceRecoveryConfig {
fn default() -> Self {
Self {
mode: RecoveryMode::FullRecoveryOnly,
validation_timeout_secs: 30,
data_load_timeout_secs: 30,
auto_remove_stale_data: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct PersistenceNamespaceConfig {
pub persist: bool,
pub durability: PersistenceDurabilityConfig,
pub snapshot_interval_secs: Option<u64>,
pub eviction: PersistenceEviction,
pub backup_count: usize,
pub in_memory_format: PersistenceInMemoryFormat,
pub regions: PersistenceRegionSelectorConfig,
pub maintenance: PersistenceMaintenanceConfig,
}
impl PersistenceNamespaceConfig {
pub fn ram_only() -> Self {
Self::default()
}
pub fn persistent() -> Self {
Self {
persist: true,
..Self::default()
}
}
fn to_settings(&self) -> NamespacePersistenceSettings {
let mut settings = if self.persist {
NamespacePersistenceSettings::persistent()
} else {
NamespacePersistenceSettings::ram_only()
};
settings.durability = self.durability.clone().into();
settings.snapshot_interval = self.snapshot_interval_secs.map(Duration::from_secs);
settings.eviction = self.eviction;
settings.backup_count = self.backup_count;
settings.in_memory_format = self.in_memory_format;
settings.persist_in_regions = self.regions.clone().into();
settings.maintenance = self.maintenance.into();
settings
}
}
impl Default for PersistenceNamespaceConfig {
fn default() -> Self {
Self {
persist: false,
durability: PersistenceDurabilityConfig::default(),
snapshot_interval_secs: None,
eviction: PersistenceEviction::None,
backup_count: 0,
in_memory_format: PersistenceInMemoryFormat::Binary,
regions: PersistenceRegionSelectorConfig::All,
maintenance: PersistenceMaintenanceConfig::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct PersistenceMaintenanceConfig {
pub tombstone_gc_interval_secs: Option<u64>,
pub compaction_interval_secs: Option<u64>,
pub gc_records_per_cycle: usize,
}
impl From<PersistenceMaintenanceConfig> for PersistenceMaintenance {
fn from(value: PersistenceMaintenanceConfig) -> Self {
PersistenceMaintenance::new(
value.tombstone_gc_interval_secs.map(Duration::from_secs),
value.compaction_interval_secs.map(Duration::from_secs),
value.gc_records_per_cycle,
)
}
}
impl From<PersistenceMaintenance> for PersistenceMaintenanceConfig {
fn from(value: PersistenceMaintenance) -> Self {
Self {
tombstone_gc_interval_secs: value
.tombstone_gc_interval
.map(|duration| duration.as_secs().max(1)),
compaction_interval_secs: value
.compaction_interval
.map(|duration| duration.as_secs().max(1)),
gc_records_per_cycle: value.gc_records_per_cycle,
}
}
}
impl Default for PersistenceMaintenanceConfig {
fn default() -> Self {
Self {
tombstone_gc_interval_secs: None,
compaction_interval_secs: None,
gc_records_per_cycle: 128,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PersistenceDurabilityConfig {
Sync,
AsyncBounded {
max_lag: usize,
},
}
impl From<PersistenceDurabilityConfig> for PersistenceDurability {
fn from(value: PersistenceDurabilityConfig) -> Self {
match value {
PersistenceDurabilityConfig::Sync => Self::Sync,
PersistenceDurabilityConfig::AsyncBounded { max_lag } => Self::AsyncBounded { max_lag },
}
}
}
impl From<PersistenceDurability> for PersistenceDurabilityConfig {
fn from(value: PersistenceDurability) -> Self {
match value {
PersistenceDurability::Sync => Self::Sync,
PersistenceDurability::AsyncBounded { max_lag } => Self::AsyncBounded { max_lag },
}
}
}
impl Default for PersistenceDurabilityConfig {
fn default() -> Self {
Self::AsyncBounded { max_lag: 1024 }
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PersistenceRegionSelectorConfig {
#[default]
All,
HomeRegionOnly,
Only(Vec<String>),
}
impl From<PersistenceRegionSelectorConfig> for RegionSelector {
fn from(value: PersistenceRegionSelectorConfig) -> Self {
match value {
PersistenceRegionSelectorConfig::All => Self::All,
PersistenceRegionSelectorConfig::HomeRegionOnly => Self::HomeRegionOnly,
PersistenceRegionSelectorConfig::Only(regions) => {
Self::only(regions.into_iter().map(RegionId::new))
}
}
}
}
impl From<RegionSelector> for PersistenceRegionSelectorConfig {
fn from(value: RegionSelector) -> Self {
match value {
RegionSelector::All => Self::All,
RegionSelector::HomeRegionOnly => Self::HomeRegionOnly,
RegionSelector::Only(regions) => Self::Only(
regions
.into_iter()
.map(|region| region.as_str().to_owned())
.collect(),
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NamespaceMetricLabels {
roster: BTreeSet<String>,
}
impl NamespaceMetricLabels {
pub fn from_policy(policy: &PersistencePolicy) -> Self {
Self {
roster: policy.persistable_namespace_roster().clone(),
}
}
pub fn label_for<'a>(&'a self, namespace: &'a str) -> Cow<'a, str> {
if self.roster.contains(namespace) {
Cow::Borrowed(namespace)
} else {
Cow::Borrowed(OTHER_NAMESPACE_METRIC_LABEL)
}
}
pub fn registered_labels(&self) -> BTreeSet<String> {
let mut labels = self.roster.clone();
labels.insert(OTHER_NAMESPACE_METRIC_LABEL.to_owned());
labels
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PersistenceConfigError {
kind: PersistenceConfigErrorKind,
message: String,
}
impl PersistenceConfigError {
fn new(kind: PersistenceConfigErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
fn policy(error: PersistencePolicyError) -> Self {
Self::new(PersistenceConfigErrorKind::Policy, error.to_string())
}
pub fn kind(&self) -> PersistenceConfigErrorKind {
self.kind
}
}
impl fmt::Display for PersistenceConfigError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for PersistenceConfigError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PersistenceConfigErrorKind {
MissingStorageDir,
StorageDirUnavailable,
DurableEngineUnavailable,
Policy,
}
#[cfg(not(target_arch = "wasm32"))]
fn validate_storage_dir(path: &Path) -> Result<(), PersistenceConfigError> {
std::fs::create_dir_all(path).map_err(|error| {
PersistenceConfigError::new(
PersistenceConfigErrorKind::StorageDirUnavailable,
format!(
"persistence storage_dir '{}' is not writable: {error}",
path.display()
),
)
})?;
let probe = path.join(".hydracache-persistence-write-test");
std::fs::write(&probe, b"ok").map_err(|error| {
PersistenceConfigError::new(
PersistenceConfigErrorKind::StorageDirUnavailable,
format!(
"persistence storage_dir '{}' is not writable: {error}",
path.display()
),
)
})?;
let _ = std::fs::remove_file(probe);
Ok(())
}