feat: add capnp

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2026-02-27 12:15:35 +01:00
parent 3162971c89
commit 749ae245c7
115 changed files with 16596 additions and 31 deletions

View File

@@ -0,0 +1,118 @@
use std::path::PathBuf;
use std::time::Duration;
use crate::message::TopicName;
/// Controls when fsync is called on WAL segment files.
#[derive(Clone, Debug, PartialEq)]
pub enum SyncPolicy {
/// Fsync after every write batch (maximum durability, lower throughput).
EveryBatch,
/// Fsync at a fixed interval via a background task. Writes go to OS page
/// cache immediately. Data written within the interval window is at risk
/// if the machine crashes without replication.
Interval(Duration),
/// Never explicitly fsync. Rely on OS page cache flush + replication.
/// Similar to Kafka's default.
None,
}
impl Default for SyncPolicy {
fn default() -> Self {
SyncPolicy::EveryBatch
}
}
/// Configuration for the Write-Ahead Log.
#[derive(Clone, Debug)]
pub struct WalConfig {
/// Maximum segment file size in bytes before rotation (default: 64MB).
pub max_segment_bytes: u64,
/// Maximum segment age in seconds before rotation (default: 60s).
pub max_segment_age_secs: u64,
/// Root data directory for WAL files.
pub data_dir: PathBuf,
/// When to fsync WAL segments (default: EveryBatch).
pub sync_policy: SyncPolicy,
}
impl Default for WalConfig {
fn default() -> Self {
Self {
max_segment_bytes: 64 * 1024 * 1024, // 64MB
max_segment_age_secs: 60,
data_dir: PathBuf::from("./data"),
sync_policy: SyncPolicy::default(),
}
}
}
/// Configuration for a topic.
#[derive(Clone, Debug)]
pub struct TopicConfig {
pub name: TopicName,
/// Number of partitions (default: 1).
pub partitions: u32,
/// Replication factor across cluster nodes (default: 3).
pub replication_factor: u32,
}
impl TopicConfig {
pub fn new(name: impl Into<TopicName>) -> Self {
Self {
name: name.into(),
partitions: 1,
replication_factor: 3,
}
}
pub fn with_partitions(mut self, partitions: u32) -> Self {
self.partitions = partitions;
self
}
pub fn with_replication_factor(mut self, factor: u32) -> Self {
self.replication_factor = factor;
self
}
}
/// Configuration for the cluster node.
#[derive(Clone, Debug)]
pub struct NodeConfig {
pub node_id: String,
pub grpc_host: std::net::SocketAddr,
pub http_host: std::net::SocketAddr,
pub seeds: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wal_config_defaults() {
let config = WalConfig::default();
assert_eq!(config.max_segment_bytes, 64 * 1024 * 1024);
assert_eq!(config.max_segment_age_secs, 60);
assert_eq!(config.data_dir, PathBuf::from("./data"));
}
#[test]
fn test_topic_config_builder() {
let config = TopicConfig::new("orders")
.with_partitions(4)
.with_replication_factor(3);
assert_eq!(config.name.as_str(), "orders");
assert_eq!(config.partitions, 4);
assert_eq!(config.replication_factor, 3);
}
#[test]
fn test_topic_config_defaults() {
let config = TopicConfig::new("events");
assert_eq!(config.partitions, 1);
assert_eq!(config.replication_factor, 3);
}
}

View File

@@ -0,0 +1,5 @@
pub mod config;
pub mod message;
pub use config::*;
pub use message::*;

View File

@@ -0,0 +1,195 @@
use std::fmt;
/// A single message in the queue.
#[derive(Clone, Debug, PartialEq)]
pub struct Message {
/// Monotonically increasing within a topic-partition. Assigned by the server.
pub offset: u64,
/// Topic this message belongs to.
pub topic: TopicName,
/// Partition within the topic.
pub partition: u32,
/// Optional partitioning key.
pub key: Option<Vec<u8>>,
/// The payload.
pub value: Vec<u8>,
/// User-defined headers (metadata).
pub headers: Vec<Header>,
/// Server-assigned wall-clock timestamp (millis since epoch).
pub timestamp_ms: u64,
}
/// A key-value header attached to a message.
#[derive(Clone, Debug, PartialEq)]
pub struct Header {
pub key: String,
pub value: Vec<u8>,
}
/// A topic name wrapper.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TopicName(pub String);
impl TopicName {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for TopicName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&str> for TopicName {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl From<String> for TopicName {
fn from(s: String) -> Self {
Self(s)
}
}
/// Information about a closed WAL segment ready for shipping.
#[derive(Clone, Debug)]
pub struct ClosedSegment {
pub path: std::path::PathBuf,
pub topic: TopicName,
pub partition: u32,
pub base_offset: u64,
pub end_offset: u64,
pub size_bytes: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_construction() {
let msg = Message {
offset: 42,
topic: TopicName::from("orders"),
partition: 0,
key: Some(b"user-123".to_vec()),
value: b"hello world".to_vec(),
headers: vec![Header {
key: "content-type".to_string(),
value: b"text/plain".to_vec(),
}],
timestamp_ms: 1700000000000,
};
assert_eq!(msg.offset, 42);
assert_eq!(msg.topic.as_str(), "orders");
assert_eq!(msg.partition, 0);
assert_eq!(msg.key.as_deref(), Some(b"user-123".as_slice()));
assert_eq!(msg.value, b"hello world");
assert_eq!(msg.headers.len(), 1);
assert_eq!(msg.headers[0].key, "content-type");
}
#[test]
fn test_message_no_key_no_headers() {
let msg = Message {
offset: 0,
topic: TopicName::from("events"),
partition: 1,
key: None,
value: b"payload".to_vec(),
headers: vec![],
timestamp_ms: 0,
};
assert!(msg.key.is_none());
assert!(msg.headers.is_empty());
}
#[test]
fn test_message_clone_eq() {
let msg = Message {
offset: 1,
topic: TopicName::from("test"),
partition: 0,
key: None,
value: b"data".to_vec(),
headers: vec![],
timestamp_ms: 100,
};
let cloned = msg.clone();
assert_eq!(msg, cloned);
}
#[test]
fn test_topic_name_ordering() {
let a = TopicName::from("alpha");
let b = TopicName::from("beta");
assert!(a < b);
}
#[test]
fn test_topic_name_display() {
let t = TopicName::from("my-topic");
assert_eq!(format!("{t}"), "my-topic");
}
#[test]
fn test_message_empty_value() {
let msg = Message {
offset: 0,
topic: TopicName::from("t"),
partition: 0,
key: None,
value: vec![],
headers: vec![],
timestamp_ms: 0,
};
assert!(msg.value.is_empty());
}
#[test]
fn test_message_large_value() {
let large = vec![0xFFu8; 1024 * 1024]; // 1MB
let msg = Message {
offset: 0,
topic: TopicName::from("t"),
partition: 0,
key: None,
value: large.clone(),
headers: vec![],
timestamp_ms: 0,
};
assert_eq!(msg.value.len(), 1024 * 1024);
assert_eq!(msg.value, large);
}
#[test]
fn test_message_many_headers() {
let headers: Vec<Header> = (0..100)
.map(|i| Header {
key: format!("header-{i}"),
value: format!("value-{i}").into_bytes(),
})
.collect();
let msg = Message {
offset: 0,
topic: TopicName::from("t"),
partition: 0,
key: None,
value: vec![],
headers,
timestamp_ms: 0,
};
assert_eq!(msg.headers.len(), 100);
assert_eq!(msg.headers[99].key, "header-99");
}
}