use std::borrow::Cow;
use lz4_flex::compress_prepend_size;
use serde::{Deserialize, Serialize};
use strum::EnumIter;
pub const DEFAULT_BLOCK_SIZE_BYTES: usize = 128;
pub const DEFAULT_PAGE_SIZE_BYTES: usize = 32 * 1024 * 1024;
pub const DEFAULT_REGION_SIZE_BLOCKS: usize = 8_192;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Default)]
pub enum Compression {
None,
#[default]
LZ4,
}
impl Compression {
pub(crate) fn compress(self, value: Vec<u8>) -> Vec<u8> {
match self {
Compression::None => value,
Compression::LZ4 => compress_lz4(&value),
}
}
pub(crate) fn decompress(self, value: Cow<'_, [u8]>) -> Cow<'_, [u8]> {
match self {
Compression::None => value,
Compression::LZ4 => decompress_lz4(&value).into(),
}
}
}
#[inline]
pub(crate) fn compress_lz4(value: &[u8]) -> Vec<u8> {
compress_prepend_size(value)
}
#[inline]
pub(crate) fn decompress_lz4(value: &[u8]) -> Vec<u8> {
lz4_flex::decompress_size_prepended(value).unwrap()
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
#[default]
Mutable,
AppendOnly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GridstoreConfig {
pub page_size_bytes: usize,
pub block_size_bytes: usize,
pub region_size_blocks: usize,
#[serde(default)]
pub compression: Compression,
}
impl GridstoreConfig {
pub const DEFAULT: Self = Self {
page_size_bytes: DEFAULT_PAGE_SIZE_BYTES,
block_size_bytes: DEFAULT_BLOCK_SIZE_BYTES,
region_size_blocks: DEFAULT_REGION_SIZE_BLOCKS,
compression: Compression::LZ4,
};
fn validate(&self) -> Result<(), String> {
if self.block_size_bytes == 0 {
return Err("block size must be greater than 0".to_string());
}
if self.region_size_blocks == 0 {
return Err("region size must be greater than 0".to_string());
}
if self.region_size_blocks > usize::from(u16::MAX) {
return Err(format!(
"region size must fit in 16 bits, got {}",
self.region_size_blocks,
));
}
if self.page_size_bytes == 0 {
return Err("page size must be greater than 0".to_string());
}
let region_size_bytes = self.block_size_bytes * self.region_size_blocks;
if self.page_size_bytes < region_size_bytes {
return Err(
"page size must be greater than or equal to (block size * region size)".to_string(),
);
}
if !self.page_size_bytes.is_multiple_of(region_size_bytes) {
return Err("page size must be a multiple of (block size * region size)".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogstoreConfig {
#[serde(rename = "page_size_bytes")]
pub page_capacity_bytes: usize,
#[serde(default)]
pub compression: Compression,
}
impl LogstoreConfig {
pub const DEFAULT: Self = Self {
page_capacity_bytes: DEFAULT_PAGE_SIZE_BYTES,
compression: Compression::LZ4,
};
fn validate(&self) -> Result<(), String> {
if self.page_capacity_bytes == 0 {
return Err("page size must be greater than 0".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(
tag = "mode",
rename_all = "snake_case",
try_from = "serde_json::Value"
)]
pub enum StorageConfig {
Mutable(GridstoreConfig),
AppendOnly(LogstoreConfig),
}
impl TryFrom<serde_json::Value> for StorageConfig {
type Error = serde_json::Error;
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
let mode = match value.get("mode") {
Some(mode) => Mode::deserialize(mode)?,
None => Mode::default(),
};
match mode {
Mode::Mutable => GridstoreConfig::deserialize(value).map(Self::Mutable),
Mode::AppendOnly => LogstoreConfig::deserialize(value).map(Self::AppendOnly),
}
}
}
impl StorageConfig {
pub(crate) fn validate(&self) -> Result<(), String> {
match self {
Self::Mutable(config) => config.validate(),
Self::AppendOnly(config) => config.validate(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_without_mode_is_mutable() {
let json = r#"{
"page_size_bytes": 33554432,
"block_size_bytes": 128,
"region_size_blocks": 8192,
"compression": "LZ4"
}"#;
let StorageConfig::Mutable(config) = serde_json::from_str(json).unwrap() else {
panic!("expected a mutable config");
};
assert_eq!(config.page_size_bytes, 33554432);
assert_eq!(config.block_size_bytes, 128);
assert_eq!(config.region_size_blocks, 8192);
}
#[test]
fn test_config_serializes_mode() {
let config = GridstoreConfig::DEFAULT;
let json = serde_json::to_string(&StorageConfig::Mutable(config)).unwrap();
assert!(json.contains(r#""mode":"mutable""#));
assert!(json.contains(r#""block_size_bytes""#));
let config = LogstoreConfig::DEFAULT;
let json = serde_json::to_string(&StorageConfig::AppendOnly(config)).unwrap();
assert!(json.contains(r#""mode":"append_only""#));
assert!(json.contains(r#""page_size_bytes""#));
assert!(!json.contains(r#""page_capacity_bytes""#));
assert!(!json.contains(r#""block_size_bytes""#));
assert!(!json.contains(r#""region_size_blocks""#));
}
#[test]
fn test_configs_survive_storage_config_round_trip() {
let config = GridstoreConfig::DEFAULT;
let json = serde_json::to_vec(&StorageConfig::Mutable(config.clone())).unwrap();
let StorageConfig::Mutable(restored) = serde_json::from_slice(&json).unwrap() else {
panic!("expected a mutable config");
};
assert_eq!(restored.page_size_bytes, config.page_size_bytes);
assert_eq!(restored.block_size_bytes, config.block_size_bytes);
assert_eq!(restored.region_size_blocks, config.region_size_blocks);
let config = LogstoreConfig::DEFAULT;
let json = serde_json::to_vec(&StorageConfig::AppendOnly(config.clone())).unwrap();
let StorageConfig::AppendOnly(restored) = serde_json::from_slice(&json).unwrap() else {
panic!("expected an append-only config");
};
assert_eq!(restored.page_capacity_bytes, config.page_capacity_bytes);
}
#[test]
fn test_gridstore_config_requires_block_and_region_sizes() {
let json = r#"{"page_size_bytes": 33554432, "mode": "mutable"}"#;
assert!(serde_json::from_str::<StorageConfig>(json).is_err());
let json = r#"{"page_size_bytes": 33554432, "mode": "append_only"}"#;
assert!(matches!(
serde_json::from_str(json).unwrap(),
StorageConfig::AppendOnly(_)
));
let json = r#"{
"page_size_bytes": 33554432,
"block_size_bytes": 128,
"region_size_blocks": 8192,
"mode": "append_only"
}"#;
assert!(matches!(
serde_json::from_str(json).unwrap(),
StorageConfig::AppendOnly(_)
));
}
}