use std::collections::HashMap;
use std::io::Write;
use std::ops::{Bound, Range};
use std::path::{Path, PathBuf};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use crate::branch::{BranchCatalog, BranchInfo, BranchName, BranchStatus};
use crate::commit::CommitPolicy;
use crate::event::{Body, Event};
use crate::format::{
derive_stream_id, generate_id_bytes, BatchId, BranchId, CodecId, EventId, EventType, Metadata,
OwnedStoredRecord, RecordEnvelopeV2, StreamId, StreamRevision,
};
use crate::log::reader::{FrameFilter, ResolvedFilter};
use crate::log::{Log, LogReader, RecordReader, ReplayEnd, ReplayPlan, StreamSelector};
use crate::projection::{decode_stored_event, replay_into, NamespaceScoped, Projection};
use crate::stream::{event_fingerprint, StreamCatalog};
use crate::view::{catch_up, View};
use crate::{
AppendReceipt, AppendRequest, Durability, ExpectedRevision, ReceiptDurability, Result,
SalamanderError, StreamName,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetentionSegment {
pub base_position: u64,
pub bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RetentionBlocker {
EngineAnchorUnavailable,
BranchRequiresBootstrap {
branch: BranchName,
fork_position: u64,
},
ProjectionRequiresBootstrap {
name: String,
},
ConsumerRequiresBootstrap {
consumer_id: String,
position: u64,
},
MaintenanceHandlesOpen {
readers: usize,
feeds: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetentionPlan {
pub plan_id: [u8; 16],
pub generation: u64,
pub requested_floor: u64,
pub effective_floor: u64,
pub current_floor: u64,
pub durable_head: u64,
pub reclaimable_segments: Vec<RetentionSegment>,
pub reclaimable_bytes: u64,
pub blockers: Vec<RetentionBlocker>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetentionApplyResult {
pub generation: u64,
pub floor: u64,
pub reclaimed_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetentionCleanupStatus {
pub pending_segments: Vec<RetentionSegment>,
pub pending_bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetentionPolicy {
KeepFrom(u64),
KeepLatestEvents(u64),
KeepNewerThan(i64),
TargetLogBytes(u64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetentionPolicyPreview {
pub policy: RetentionPolicy,
pub selected_floor: u64,
pub target_satisfied: bool,
pub explanation: String,
pub plan: RetentionPlan,
}
pub struct Salamander<B> {
pub(crate) log: Log,
views: HashMap<String, Box<dyn View<B>>>,
policy: CommitPolicy,
pending_bytes: u64,
pending_count: u64,
last_commit: Instant,
catalog: StreamCatalog,
branches: BranchCatalog,
retention_anchor: Option<crate::RetentionAnchorInfo>,
retention_projection_coverage: Vec<crate::RetentionProjectionCoverage>,
retention_branch_bootstraps: Vec<crate::RetentionBranchBootstrap>,
retention_consumer_bootstraps: Vec<crate::RetentionConsumerBootstrap>,
retention_system_records: Vec<crate::retention::AnchoredSystemRecord>,
durable_head: u64,
root: PathBuf,
}
fn load_core_catalog(root: &Path, database_id: [u8; 16], head: u64) -> Option<StreamCatalog> {
let bytes = std::fs::read(root.join("core-catalog.bin")).ok()?;
if bytes.len() > 256 * 1024 * 1024 {
return None;
}
let checkpoint: CoreCatalogCheckpoint = bincode::deserialize(&bytes).ok()?;
if checkpoint.database_id != database_id || checkpoint.head != head {
return None;
}
let payload =
bincode::serialize(&(checkpoint.database_id, checkpoint.head, &checkpoint.catalog)).ok()?;
(crc32c::crc32c(&payload) == checkpoint.checksum).then_some(checkpoint.catalog)
}
fn persist_core_catalog(
root: &Path,
database_id: [u8; 16],
head: u64,
catalog: &StreamCatalog,
) -> Result<()> {
let payload = bincode::serialize(&(database_id, head, catalog))
.map_err(|error| SalamanderError::Serialization(error.to_string()))?;
let checkpoint = CoreCatalogCheckpoint {
database_id,
head,
catalog: catalog.clone(),
checksum: crc32c::crc32c(&payload),
};
let bytes = bincode::serialize(&checkpoint)
.map_err(|error| SalamanderError::Serialization(error.to_string()))?;
let temporary = root.join("core-catalog.tmp");
let final_path = root.join("core-catalog.bin");
{
let mut file = std::fs::File::create(&temporary)?;
file.write_all(&bytes)?;
file.sync_all()?;
}
std::fs::rename(temporary, final_path)?;
Ok(())
}
#[derive(serde::Serialize, serde::Deserialize)]
struct CoreCatalogCheckpoint {
database_id: [u8; 16],
head: u64,
catalog: StreamCatalog,
checksum: u32,
}
impl<B: Body> Salamander<B> {
pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
Self::open_with_policy(dir, CommitPolicy::default())
}
pub fn open_with_policy(dir: impl AsRef<Path>, policy: CommitPolicy) -> Result<Self> {
Self::open_internal(dir.as_ref(), policy, None)
}
pub(crate) fn open_with_policy_and_segment_max(
dir: impl AsRef<Path>,
policy: CommitPolicy,
segment_max_bytes: u64,
) -> Result<Self> {
Self::open_internal(dir.as_ref(), policy, Some(segment_max_bytes))
}
fn open_internal(
dir: &Path,
policy: CommitPolicy,
segment_max_bytes: Option<u64>,
) -> Result<Self> {
let root = dir.to_path_buf();
let log = match segment_max_bytes {
Some(maximum) => Log::open_with_segment_max_bytes(&root, maximum)?,
None => Log::open(&root)?,
};
let database_id = log.database_id().into_bytes();
let authoritative_anchor = log.retention_anchor_checksum().is_some();
let loaded_anchor = match crate::retention::load(&root) {
Ok(anchor) => anchor,
Err(error) if log.has_complete_prefix() && !authoritative_anchor => {
eprintln!("salamander: ignoring non-authoritative retention anchor: {error}");
None
}
Err(error) => return Err(error),
};
let exact_anchor = loaded_anchor.and_then(|(anchor, info)| {
(crate::retention::validate_identity(
&anchor,
database_id,
log.retention_floor(),
log.head(),
)
.is_ok()
&& log
.retention_anchor_checksum()
.is_none_or(|checksum| checksum == info.checksum))
.then_some((anchor, info))
});
let (
catalog,
branches,
retention_anchor,
retention_projection_coverage,
retention_branch_bootstraps,
retention_consumer_bootstraps,
retention_system_records,
) = if let Some((anchor, info)) = exact_anchor {
(
anchor.stream_catalog,
anchor.branch_catalog,
Some(info),
anchor.projection_coverage,
anchor.branch_bootstraps,
anchor.consumer_bootstraps,
anchor.system_records,
)
} else if authoritative_anchor {
return Err(SalamanderError::InvalidFormat(
"manifest has no matching authoritative retention anchor".into(),
));
} else if log.has_complete_prefix() {
let catalog = match load_core_catalog(&root, database_id, log.head()) {
Some(catalog) => catalog,
None => {
let catalog = StreamCatalog::rebuild(log.records_from(0))?;
let _ = persist_core_catalog(&root, database_id, log.head(), &catalog);
catalog
}
};
let branches = BranchCatalog::rebuild(log.system_records())?;
(
catalog,
branches,
None,
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
)
} else {
return Err(SalamanderError::InvalidFormat(
"retained database has no compatible authoritative core anchor".into(),
));
};
let durable_head = log.head();
Ok(Salamander {
log,
views: HashMap::new(),
policy,
pending_bytes: 0,
pending_count: 0,
last_commit: Instant::now(),
catalog,
branches,
retention_anchor,
retention_projection_coverage,
retention_branch_bootstraps,
retention_consumer_bootstraps,
retention_system_records,
durable_head,
root,
})
}
pub fn set_commit_policy(&mut self, policy: CommitPolicy) {
self.policy = policy;
}
pub fn commit_policy(&self) -> CommitPolicy {
self.policy
}
pub fn retention_floor(&self) -> u64 {
self.log.retention_floor()
}
pub fn plan_retention(&self, requested_floor: u64) -> Result<RetentionPlan> {
let head = self.durable_head();
if requested_floor > head {
return Err(SalamanderError::OffsetBeyondHead(requested_floor));
}
if requested_floor < self.retention_floor() {
return Err(SalamanderError::InvalidArgument(format!(
"retention floor cannot move backward from {} to {requested_floor}",
self.retention_floor()
)));
}
let (effective_floor, segments) = self.log.retention_boundary(requested_floor);
let reclaimable_segments: Vec<_> = segments
.into_iter()
.map(|(base_position, bytes)| RetentionSegment {
base_position,
bytes,
})
.collect();
let anchor_ready = self.retention_anchor.as_ref().is_some_and(|anchor| {
anchor.database_id == self.log.database_id().into_bytes()
&& anchor.floor == effective_floor
&& anchor.head == head
});
let mut blockers = Vec::new();
if !anchor_ready {
blockers.push(RetentionBlocker::EngineAnchorUnavailable);
}
blockers.extend(self.branches.all().filter_map(|branch| {
let fork_position = branch.fork_position?;
(fork_position < effective_floor
&& !self.has_retention_branch_bootstrap(branch.id.into_bytes(), effective_floor))
.then(|| RetentionBlocker::BranchRequiresBootstrap {
branch: branch.name.clone(),
fork_position,
})
}));
blockers.extend(
self.views
.keys()
.cloned()
.map(|name| RetentionBlocker::ProjectionRequiresBootstrap { name }),
);
Ok(RetentionPlan {
plan_id: generate_id_bytes(),
generation: self.log.retention_generation(),
requested_floor,
effective_floor,
current_floor: self.retention_floor(),
durable_head: head,
reclaimable_bytes: reclaimable_segments
.iter()
.map(|segment| segment.bytes)
.sum(),
reclaimable_segments,
blockers,
})
}
pub fn preview_retention_policy(
&self,
policy: RetentionPolicy,
) -> Result<RetentionPolicyPreview> {
let current = self.retention_floor();
let head = self.durable_head();
let (selected_floor, target_satisfied, explanation) = match policy {
RetentionPolicy::KeepFrom(position) => (
position,
true,
format!("selected explicit position {position}"),
),
RetentionPolicy::KeepLatestEvents(count) => {
let available = head.saturating_sub(current);
let selected = head.saturating_sub(count.min(available)).max(current);
(
selected,
true,
format!("selected position {selected} to keep the latest {count} event(s)"),
)
}
RetentionPolicy::KeepNewerThan(cutoff) => {
let mut selected = head;
for item in self.log.records_from(current) {
let record = item?;
if record.envelope.timestamp_unix_nanos >= cutoff {
selected = selected.min(record.position);
}
}
(
selected,
true,
format!("selected earliest event at or after unix-nanosecond cutoff {cutoff}"),
)
}
RetentionPolicy::TargetLogBytes(bytes) => {
let (selected, retained, satisfied) = self.log.retention_floor_for_bytes(bytes)?;
(
selected,
satisfied,
format!(
"selected position {selected} with {retained} retained segment byte(s) for target {bytes}"
),
)
}
};
let plan = self.plan_retention(selected_floor)?;
Ok(RetentionPolicyPreview {
policy,
selected_floor,
target_satisfied,
explanation,
plan,
})
}
pub(crate) fn apply_retention_prevalidated(
&mut self,
effective_floor: u64,
durable_head: u64,
) -> Result<RetentionApplyResult> {
if self.durable_head() != durable_head {
return Err(SalamanderError::InvalidArgument(
"retention plan is stale because durable head changed".into(),
));
}
let anchor = self.retention_anchor.as_ref().ok_or_else(|| {
SalamanderError::InvalidArgument("retention plan has no verified anchor".into())
})?;
if anchor.floor != effective_floor || anchor.head != durable_head {
return Err(SalamanderError::InvalidArgument(
"retention plan anchor identity is stale".into(),
));
}
crate::retention::crash_point("before_manifest_switch");
self.log
.activate_retention(effective_floor, anchor.checksum)?;
crate::retention::crash_point("after_manifest_switch");
let reclaimed_bytes = self.log.reclaim_below_retention_floor();
crate::retention::crash_point("after_cleanup");
Ok(RetentionApplyResult {
generation: self.log.retention_generation(),
floor: self.retention_floor(),
reclaimed_bytes,
})
}
pub fn create_retention_anchor(
&mut self,
requested_floor: u64,
) -> Result<crate::RetentionAnchorInfo> {
self.create_retention_anchor_with_coverage(requested_floor, Vec::new())
}
pub(crate) fn create_retention_anchor_with_coverage(
&mut self,
requested_floor: u64,
projection_coverage: Vec<crate::RetentionProjectionCoverage>,
) -> Result<crate::RetentionAnchorInfo> {
self.create_retention_anchor_with_all_coverage(
requested_floor,
projection_coverage,
Vec::new(),
Vec::new(),
)
}
pub(crate) fn create_retention_anchor_with_all_coverage(
&mut self,
requested_floor: u64,
projection_coverage: Vec<crate::RetentionProjectionCoverage>,
branch_bootstraps: Vec<crate::RetentionBranchBootstrap>,
consumer_bootstraps: Vec<crate::RetentionConsumerBootstrap>,
) -> Result<crate::RetentionAnchorInfo> {
self.commit()?;
let plan = self.plan_retention(requested_floor)?;
let catalog = StreamCatalog::rebuild(self.log.records_from(0))?;
let branches = BranchCatalog::rebuild(self.log.system_records())?;
let system_records = self
.log
.system_records()
.map(|item| {
item.map(|record| crate::retention::AnchoredSystemRecord {
event_type: record.envelope.event_type.as_str().to_string(),
payload: record.payload,
})
})
.collect::<Result<Vec<_>>>()?;
let info = crate::retention::publish(
&self.root,
crate::retention::CoreRetentionAnchor {
format_version: crate::retention::FORMAT_VERSION,
database_id: self.log.database_id().into_bytes(),
floor: plan.effective_floor,
head: self.durable_head(),
stream_catalog: catalog.clone(),
branch_catalog: branches.clone(),
projection_coverage: projection_coverage.clone(),
branch_bootstraps: branch_bootstraps.clone(),
consumer_bootstraps: consumer_bootstraps.clone(),
system_records: system_records.clone(),
},
)?;
self.catalog = catalog;
self.branches = branches;
self.retention_anchor = Some(info.clone());
self.retention_projection_coverage = projection_coverage;
self.retention_branch_bootstraps = branch_bootstraps;
self.retention_consumer_bootstraps = consumer_bootstraps;
self.retention_system_records = system_records;
Ok(info)
}
pub(crate) fn has_retention_branch_bootstrap(&self, branch_id: [u8; 16], floor: u64) -> bool {
self.retention_branch_bootstraps.iter().any(|item| {
item.branch_id == branch_id
&& item.floor == floor
&& crc32c::crc32c(&item.checkpoint) == item.checksum
})
}
pub(crate) fn has_retention_consumer_bootstrap(&self, consumer_id: &str, floor: u64) -> bool {
self.retention_consumer_bootstraps.iter().any(|item| {
item.consumer_id == consumer_id
&& item.floor == floor
&& crc32c::crc32c(&item.checkpoint) == item.checksum
})
}
pub(crate) fn retention_branch_bootstrap(&self, branch_id: [u8; 16]) -> Option<Vec<u8>> {
self.retention_branch_bootstraps
.iter()
.find(|item| {
item.branch_id == branch_id && crc32c::crc32c(&item.checkpoint) == item.checksum
})
.map(|item| item.checkpoint.clone())
}
pub(crate) fn retention_consumer_bootstrap(&self, consumer_id: &str) -> Option<Vec<u8>> {
self.retention_consumer_bootstraps
.iter()
.find(|item| {
item.consumer_id == consumer_id && crc32c::crc32c(&item.checkpoint) == item.checksum
})
.map(|item| item.checkpoint.clone())
}
pub(crate) fn retention_consumer_bootstrap_info(
&self,
consumer_id: &str,
) -> Option<crate::RetentionConsumerBootstrap> {
self.retention_consumer_bootstraps
.iter()
.find(|item| {
item.consumer_id == consumer_id && crc32c::crc32c(&item.checkpoint) == item.checksum
})
.cloned()
}
pub(crate) fn retention_identity(&self) -> ([u8; 16], u64) {
(
self.log.database_id().into_bytes(),
self.log.retention_generation(),
)
}
pub(crate) fn retention_cleanup_status(&self) -> RetentionCleanupStatus {
let (_, segments) = self.log.retention_boundary(self.retention_floor());
let pending_segments = segments
.into_iter()
.map(|(base_position, bytes)| RetentionSegment {
base_position,
bytes,
})
.collect::<Vec<_>>();
let pending_bytes = pending_segments.iter().map(|segment| segment.bytes).sum();
RetentionCleanupStatus {
pending_segments,
pending_bytes,
}
}
pub(crate) fn system_metadata(&self) -> Result<Vec<(String, Vec<u8>)>> {
let mut records = self
.retention_system_records
.iter()
.map(|record| (record.event_type.clone(), record.payload.clone()))
.collect::<Vec<_>>();
for item in self.log.system_records() {
let record = item?;
records.push((
record.envelope.event_type.as_str().to_string(),
record.payload,
));
}
Ok(records)
}
pub(crate) fn has_retention_projection_coverage(
&self,
name: &str,
descriptor_fingerprint: [u8; 16],
branch_id: [u8; 16],
partitions: u32,
) -> bool {
self.retention_anchor.as_ref().is_some_and(|anchor| {
self.retention_projection_coverage.iter().any(|coverage| {
let mut covered_partitions = std::collections::BTreeSet::new();
coverage.name == name
&& coverage.descriptor_fingerprint == descriptor_fingerprint
&& coverage.branch_id == branch_id
&& coverage.cursor == anchor.head
&& coverage.snapshot_ids.len() == partitions as usize
&& coverage.snapshot_ids.iter().all(|id| {
crate::snapshot::verify(&self.root, id).is_ok_and(|info| {
info.manifest.projection_name == name
&& info.manifest.descriptor_fingerprint == descriptor_fingerprint
&& info.manifest.branch_id == branch_id
&& info.manifest.cursor.position == anchor.head
&& info.manifest.partition_count.unwrap_or(1) == partitions
&& covered_partitions.insert(info.manifest.partition.unwrap_or(0))
})
})
&& covered_partitions.len() == partitions as usize
})
})
}
pub fn append(&mut self, namespace: &str, body: B) -> Result<u64> {
let request = AppendRequest {
branch: BranchId::ZERO,
stream: crate::StreamName::new(namespace)?,
expected: ExpectedRevision::Any,
idempotency_key: None,
events: vec![crate::NewEvent::new(
EventType::new(std::any::type_name::<B>())?,
body,
)],
durability: Durability::Buffered,
};
Ok(self.append_batch(request)?.first_position)
}
pub fn append_on_branch(&mut self, branch: BranchId, namespace: &str, body: B) -> Result<u64> {
let request = AppendRequest {
branch,
stream: crate::StreamName::new(namespace)?,
expected: ExpectedRevision::Any,
idempotency_key: None,
events: vec![crate::NewEvent::new(
EventType::new(std::any::type_name::<B>())?,
body,
)],
durability: Durability::Buffered,
};
Ok(self.append_batch(request)?.first_position)
}
#[allow(dead_code)]
fn append_wp01_compat(&mut self, namespace: &str, body: B) -> Result<u64> {
let timestamp_ms = current_timestamp_ms();
let mut event = Event {
offset: 0,
timestamp_ms,
namespace: namespace.to_string(),
body,
};
let bytes = bincode::serialize(&event.body)
.map_err(|e| SalamanderError::Serialization(e.to_string()))?;
let database_id = self.log.database_id();
let branch_id = BranchId::ZERO;
let id_bytes = generate_id_bytes();
let mut metadata = Metadata::new();
metadata.insert(
"salamander.stream_name".to_string(),
namespace.as_bytes().to_vec(),
);
let envelope = RecordEnvelopeV2 {
event_id: EventId::from_bytes(id_bytes),
database_id,
branch_id,
stream_id: derive_stream_id(database_id, branch_id, namespace),
stream_revision: StreamRevision(self.log.head()),
timestamp_unix_nanos: (timestamp_ms as i64).saturating_mul(1_000_000),
event_type: EventType::new(std::any::type_name::<B>())?,
schema_version: 1,
codec: CodecId::RUST_BINCODE_V1,
batch_id: BatchId::from_bytes(id_bytes),
batch_index: 0,
metadata,
};
let (offset, last) = self.log.append_batch(&[(envelope, bytes.clone())])?;
debug_assert_eq!(offset, last);
event.offset = offset;
for view in self.views.values_mut() {
view.apply(&event);
}
self.pending_bytes += bytes.len() as u64;
self.pending_count += 1;
if self.policy.should_commit(
self.pending_bytes,
self.pending_count,
self.last_commit.elapsed(),
) {
self.commit()?;
}
self.catalog = StreamCatalog::rebuild(self.log.records_from(0))?;
if self.durable_head == self.log.head() {
persist_core_catalog(
&self.root,
self.log.database_id().into_bytes(),
self.durable_head,
&self.catalog,
)?;
}
Ok(offset)
}
pub fn append_batch(&mut self, request: AppendRequest<B>) -> Result<AppendReceipt> {
self.append_batch_with_id(request, None)
}
pub(crate) fn append_batch_with_id(
&mut self,
request: AppendRequest<B>,
supplied_batch_id: Option<BatchId>,
) -> Result<AppendReceipt> {
request.validate()?;
let branch = self
.branches
.get(request.branch)
.ok_or_else(|| SalamanderError::BranchNotFound(format!("{:?}", request.branch)))?;
if branch.status == BranchStatus::Archived {
return Err(SalamanderError::BranchArchived(
branch.name.as_str().to_string(),
));
}
let previous = self.catalog.revision(request.branch, &request.stream);
let serialized: Vec<Vec<u8>> = request
.events
.iter()
.map(|event| {
bincode::serialize(&event.body)
.map_err(|error| SalamanderError::Serialization(error.to_string()))
})
.collect::<Result<_>>()?;
let request_digest = request_fingerprint(&request, &serialized);
if let Some(key) = &request.idempotency_key {
if let Some((digest, mut receipt)) = self.catalog.idempotent(request.branch, key) {
if digest != request_digest {
return Err(SalamanderError::IdempotencyConflict);
}
if request.durability == Durability::Sync
&& receipt.durability != ReceiptDurability::Synced
{
self.commit()?;
receipt.durability = ReceiptDurability::Synced;
}
return Ok(receipt);
}
}
validate_expected(request.expected, previous)?;
let database_id = self.log.database_id();
let stream_id = self
.catalog
.stream_id(request.branch, &request.stream)
.unwrap_or_else(|| {
derive_stream_id(database_id, request.branch, request.stream.as_str())
});
let batch_id =
supplied_batch_id.unwrap_or_else(|| BatchId::from_bytes(generate_id_bytes()));
let first_revision = previous.map_or(0, |revision| revision.0 + 1);
let timestamp_ms = current_timestamp_ms();
let mut stored = Vec::with_capacity(request.events.len());
let mut digests = Vec::with_capacity(request.events.len());
for (index, (event, body)) in request.events.iter().zip(&serialized).enumerate() {
let event_id = event
.event_id
.unwrap_or_else(|| EventId::from_bytes(generate_id_bytes()));
let mut metadata = event.metadata.clone();
metadata.insert(
"salamander.stream_name".into(),
request.stream.as_str().as_bytes().to_vec(),
);
if let Some(key) = &request.idempotency_key {
metadata.insert("salamander.idempotency_key".into(), key.as_bytes().to_vec());
metadata.insert(
"salamander.request_digest".into(),
request_digest.to_le_bytes().to_vec(),
);
}
let envelope = RecordEnvelopeV2 {
event_id,
database_id,
branch_id: request.branch,
stream_id,
stream_revision: StreamRevision(first_revision + index as u64),
timestamp_unix_nanos: (timestamp_ms as i64).saturating_mul(1_000_000),
event_type: event.event_type.clone(),
schema_version: event.schema_version,
codec: CodecId::RUST_BINCODE_V1,
batch_id,
batch_index: index as u32,
metadata,
};
let record = crate::format::OwnedStoredRecord {
kind: crate::format::FrameKind::Event,
flags: 0,
position: self.log.head() + index as u64,
envelope: envelope.clone(),
payload: body.clone(),
};
let digest = event_fingerprint(&record);
if self
.catalog
.event_digest(event_id)
.is_some_and(|old| old != digest)
|| digests
.iter()
.any(|(id, old)| *id == event_id && *old != digest)
{
return Err(SalamanderError::EventIdConflict);
}
digests.push((event_id, digest));
stored.push((envelope, body.clone()));
}
let supplied_ids: Vec<_> = request.events.iter().map(|event| event.event_id).collect();
if supplied_ids
.iter()
.flatten()
.any(|id| self.catalog.event_receipt(*id).is_some())
{
let mut original: Option<AppendReceipt> = None;
for (supplied, (_, digest)) in supplied_ids.iter().zip(&digests) {
let Some(id) = supplied else {
return Err(SalamanderError::EventIdConflict);
};
let Some((stored_digest, receipt)) = self.catalog.event_receipt(*id) else {
return Err(SalamanderError::EventIdConflict);
};
if stored_digest != *digest
|| original
.as_ref()
.is_some_and(|existing| existing.batch_id != receipt.batch_id)
{
return Err(SalamanderError::EventIdConflict);
}
original = Some(receipt);
}
let mut original = original.ok_or(SalamanderError::EventIdConflict)?;
if supplied_batch_id.is_some_and(|id| id != original.batch_id) {
return Err(SalamanderError::BatchIdConflict);
}
if request.durability == Durability::Sync
&& original.durability != ReceiptDurability::Synced
{
self.commit()?;
original.durability = ReceiptDurability::Synced;
}
return Ok(original);
}
if self.catalog.batch_receipt(batch_id).is_some() {
return Err(SalamanderError::BatchIdConflict);
}
let (first_position, last_position) = self.log.append_batch(&stored)?;
for (index, event) in request.events.iter().enumerate() {
let runtime = Event {
offset: first_position + index as u64,
timestamp_ms,
namespace: request.stream.as_str().to_string(),
body: event.body.clone(),
};
for view in self.views.values_mut() {
view.apply(&runtime);
}
}
self.pending_bytes += serialized.iter().map(Vec::len).sum::<usize>() as u64;
self.pending_count += request.events.len() as u64;
let sync = request.durability == Durability::Sync
|| self.policy.should_commit(
self.pending_bytes,
self.pending_count,
self.last_commit.elapsed(),
);
if sync {
self.commit()?;
}
let receipt = AppendReceipt {
batch_id,
first_position,
last_position,
stream_id,
previous_revision: previous,
current_revision: StreamRevision(first_revision + request.events.len() as u64 - 1),
durability: if sync {
ReceiptDurability::Synced
} else if request.durability == Durability::Flush {
ReceiptDurability::Flushed
} else {
ReceiptDurability::Buffered
},
};
self.catalog.record_batch(
request.branch,
&request.stream,
stream_id,
digests,
request
.idempotency_key
.as_ref()
.map(|key| (key, request_digest)),
receipt.clone(),
);
if sync {
persist_core_catalog(
&self.root,
self.log.database_id().into_bytes(),
self.durable_head,
&self.catalog,
)?;
}
Ok(receipt)
}
pub fn branch(&self, id: BranchId) -> Option<&BranchInfo> {
self.branches.get(id)
}
pub fn branch_named(&self, name: &str) -> Option<&BranchInfo> {
self.branches.named(name)
}
pub fn branch_ancestry(&self, id: BranchId) -> Result<Vec<BranchInfo>> {
self.branches.ancestry(id)
}
pub fn branch_children(&self, id: BranchId) -> Vec<BranchInfo> {
self.branches.children(id)
}
pub fn branch_common_ancestor(&self, left: BranchId, right: BranchId) -> Result<BranchInfo> {
self.branches.common_ancestor(left, right)
}
pub fn diff(&self, request: DiffRequest) -> Result<TimelineDiff> {
request.streams.validate()?;
let head = self.log.head();
let resolve = |end: ReplayEnd| match end {
ReplayEnd::Head => Ok(head),
ReplayEnd::At(position) if position > head => {
Err(SalamanderError::OffsetBeyondHead(position))
}
ReplayEnd::At(position) => Ok(position),
};
let left_until = resolve(request.left_until)?;
let right_until = resolve(request.right_until)?;
let branch = |id: BranchId| {
self.branches
.get(id)
.cloned()
.ok_or_else(|| SalamanderError::BranchNotFound(format!("{id:?}")))
};
let left = branch(request.left)?;
let right = branch(request.right)?;
let (common_ancestor, divergence) =
self.branches
.divergence(request.left, left_until, request.right, right_until)?;
let floor = self.retention_floor();
if divergence < floor {
return Err(self.position_unavailable(divergence));
}
let plan = |branch: BranchId, from: u64, until: u64| ReplayPlan {
branch,
streams: request.streams.clone(),
from: Bound::Included(from),
until: ReplayEnd::At(until),
..ReplayPlan::default()
};
Ok(TimelineDiff {
shared: plan(common_ancestor.id, floor, divergence),
common_ancestor,
divergence,
left: DiffSide {
suffix: plan(left.id, divergence, left_until),
branch: left,
until: left_until,
},
right: DiffSide {
suffix: plan(right.id, divergence, right_until),
branch: right,
until: right_until,
},
})
}
pub fn read(&self, plan: ReplayPlan) -> Result<LogReader<'_>> {
plan.streams.validate()?;
let head = self.log.head();
let until = match plan.until {
ReplayEnd::Head => head,
ReplayEnd::At(position) => {
if position > head {
return Err(SalamanderError::OffsetBeyondHead(position));
}
position
}
};
let from = match plan.from {
Bound::Unbounded => 0,
Bound::Included(position) => position,
Bound::Excluded(position) => position.saturating_add(1),
};
if until < self.retention_floor() {
return Err(self.position_unavailable(until));
}
if from < self.retention_floor() {
return Err(self.position_unavailable(from));
}
let scopes = self.branches.replay_scopes(plan.branch, until)?;
Ok(self.log.plan_reader(ResolvedFilter {
from,
until,
selector: plan.streams,
scopes: Some(scopes),
time: plan.time,
kinds: FrameFilter::UserEvents,
max_events: plan.max_events,
verification: plan.verification,
}))
}
pub fn replay_branch(
&self,
branch: BranchId,
namespace: &str,
range: Range<u64>,
mut f: impl FnMut(&Event<B>),
) -> Result<()> {
let mut reader = self.read(ReplayPlan {
branch,
from: Bound::Included(range.start),
until: ReplayEnd::At(range.end),
..ReplayPlan::default()
})?;
while let Some(record) = reader.next()? {
let record = OwnedStoredRecord::from(record);
let event = decode_stored_event::<B>(&record)?;
if event.namespace == namespace {
f(&event);
}
}
Ok(())
}
pub fn fork_branch(
&mut self,
parent: BranchId,
at: u64,
name: BranchName,
metadata: Metadata,
) -> Result<BranchInfo> {
if self.branches.get(parent).is_none() {
return Err(SalamanderError::BranchNotFound(format!("{parent:?}")));
}
if at > self.head() {
return Err(SalamanderError::OffsetBeyondHead(at));
}
if at < self.retention_floor() {
return Err(self.position_unavailable(at));
}
if !self.is_batch_boundary(at)? {
return Err(SalamanderError::NotBatchBoundary(at));
}
let id = BranchId::from_bytes(generate_id_bytes());
let info = BranchInfo {
id,
name,
parent: Some(parent),
fork_position: Some(at),
created_at_unix_nanos: (current_timestamp_ms() as i64).saturating_mul(1_000_000),
metadata,
status: BranchStatus::Active,
};
let mut updated_branches = self.branches.clone();
updated_branches.insert(info.clone())?;
let payload = serde_json::to_vec(&info)
.map_err(|error| SalamanderError::Serialization(error.to_string()))?;
let event_bytes = generate_id_bytes();
let envelope = RecordEnvelopeV2 {
event_id: EventId::from_bytes(event_bytes),
database_id: self.log.database_id(),
branch_id: id,
stream_id: crate::StreamId::ZERO,
stream_revision: StreamRevision(0),
timestamp_unix_nanos: info.created_at_unix_nanos,
event_type: EventType::new("salamander.branch.created")?,
schema_version: 1,
codec: CodecId::JSON_UTF8,
batch_id: BatchId::from_bytes(event_bytes),
batch_index: 0,
metadata: Metadata::new(),
};
self.log.append_system(&envelope, &payload)?;
if let Err(error) = self.commit() {
self.branches = BranchCatalog::rebuild(self.log.system_records())?;
return Err(error);
}
self.branches = updated_branches;
Ok(info)
}
pub fn archive_branch(&mut self, id: BranchId) -> Result<BranchInfo> {
let mut info = self
.branches
.get(id)
.cloned()
.ok_or_else(|| SalamanderError::BranchNotFound(format!("{id:?}")))?;
if id == BranchId::ZERO {
return Err(SalamanderError::InvalidArgument(
"the default branch cannot be archived".into(),
));
}
if info.status == BranchStatus::Archived {
return Ok(info);
}
info.status = BranchStatus::Archived;
let mut updated_branches = self.branches.clone();
updated_branches.archive(info.clone())?;
let payload = serde_json::to_vec(&info)
.map_err(|error| SalamanderError::Serialization(error.to_string()))?;
let event_bytes = generate_id_bytes();
let envelope = RecordEnvelopeV2 {
event_id: EventId::from_bytes(event_bytes),
database_id: self.log.database_id(),
branch_id: id,
stream_id: crate::StreamId::ZERO,
stream_revision: StreamRevision(0),
timestamp_unix_nanos: (current_timestamp_ms() as i64).saturating_mul(1_000_000),
event_type: EventType::new("salamander.branch.archived")?,
schema_version: 1,
codec: CodecId::JSON_UTF8,
batch_id: BatchId::from_bytes(event_bytes),
batch_index: 0,
metadata: Metadata::new(),
};
self.log.append_system(&envelope, &payload)?;
if let Err(error) = self.commit() {
self.branches = BranchCatalog::rebuild(self.log.system_records())?;
return Err(error);
}
self.branches = updated_branches;
Ok(info)
}
fn is_batch_boundary(&self, at: u64) -> Result<bool> {
if at == 0 || at == self.head() {
return Ok(true);
}
let mut before = None;
let mut after = None;
for item in self.log.records_from(at - 1) {
let record = item?;
if record.position == at - 1 {
before = Some(record.envelope.batch_id);
} else if record.position >= at {
after = (record.position == at).then_some(record.envelope.batch_id);
break;
}
}
Ok(matches!((before, after), (Some(left), Some(right)) if left != right))
}
fn position_unavailable(&self, requested: u64) -> SalamanderError {
SalamanderError::PositionUnavailable {
requested,
floor: self.retention_floor(),
head: self.head(),
bootstrap_available: false,
}
}
pub fn commit(&mut self) -> Result<u64> {
let head = self.log.commit()?;
self.durable_head = head;
self.pending_bytes = 0;
self.pending_count = 0;
self.last_commit = Instant::now();
persist_core_catalog(
&self.root,
self.log.database_id().into_bytes(),
head,
&self.catalog,
)?;
Ok(head)
}
pub fn uncommitted_bytes(&self) -> u64 {
self.pending_bytes
}
pub fn uncommitted_count(&self) -> u64 {
self.pending_count
}
pub fn projection<P: Projection<Body = B> + Default>(&self) -> Result<P> {
self.require_full_history()?;
let mut p = P::default();
replay_into(&mut p, &self.log, self.log.head())?;
Ok(p)
}
pub fn projection_for<P: NamespaceScoped<Body = B>>(&self, namespace: &str) -> Result<P> {
self.require_full_history()?;
let mut p = P::new_for(namespace);
replay_into(&mut p, &self.log, self.log.head())?;
Ok(p)
}
pub fn view_at<P: Projection<Body = B> + Default>(&self, n: u64) -> Result<P> {
if n > self.log.head() {
return Err(SalamanderError::OffsetBeyondHead(n));
}
self.require_full_history()?;
let mut p = P::default();
replay_into(&mut p, &self.log, n)?;
Ok(p)
}
pub fn register(&mut self, name: &str, mut view: Box<dyn View<B>>) -> Result<()> {
self.require_full_history()?;
catch_up(view.as_mut(), &self.log, self.log.head())?;
self.views.insert(name.to_string(), view);
Ok(())
}
pub fn deregister(&mut self, name: &str) -> Option<Box<dyn View<B>>> {
self.views.remove(name)
}
pub fn view<T: View<B>>(&self, name: &str) -> Option<&T> {
self.views.get(name)?.as_any().downcast_ref::<T>()
}
pub fn replay_to<P: Projection<Body = B>>(&self, mut view: P, n: u64) -> Result<P> {
if n > self.log.head() {
return Err(SalamanderError::OffsetBeyondHead(n));
}
self.require_full_history()?;
replay_into(&mut view, &self.log, n)?;
Ok(view)
}
pub fn head(&self) -> u64 {
self.log.head()
}
pub fn durable_head(&self) -> u64 {
self.durable_head
}
pub(crate) fn stream_id(&self, branch: BranchId, stream: &StreamName) -> Option<StreamId> {
self.catalog.stream_id(branch, stream)
}
pub fn replay(
&self,
namespace: &str,
range: Range<u64>,
f: impl FnMut(&Event<B>),
) -> Result<()> {
if range.start < self.retention_floor() {
return Err(self.position_unavailable(range.start));
}
crate::introspect::replay(&self.log, namespace, range, f)
}
fn require_full_history(&self) -> Result<()> {
if self.retention_floor() > 0 {
return Err(self.position_unavailable(0));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffRequest {
pub left: BranchId,
pub right: BranchId,
pub left_until: ReplayEnd,
pub right_until: ReplayEnd,
pub streams: StreamSelector,
}
impl DiffRequest {
pub fn new(left: BranchId, right: BranchId) -> Self {
Self {
left,
right,
left_until: ReplayEnd::Head,
right_until: ReplayEnd::Head,
streams: StreamSelector::All,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffSide {
pub branch: BranchInfo,
pub until: u64,
pub suffix: ReplayPlan,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimelineDiff {
pub common_ancestor: BranchInfo,
pub divergence: u64,
pub shared: ReplayPlan,
pub left: DiffSide,
pub right: DiffSide,
}
fn current_timestamp_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
fn validate_expected(expected: ExpectedRevision, actual: Option<StreamRevision>) -> Result<()> {
let matches = match expected {
ExpectedRevision::Any => true,
ExpectedRevision::NoStream => actual.is_none(),
ExpectedRevision::Exact(expected) => actual == Some(expected),
};
if matches {
return Ok(());
}
Err(SalamanderError::RevisionConflict {
expected: format!("{expected:?}"),
actual: format!("{actual:?}"),
})
}
fn request_fingerprint<B>(request: &AppendRequest<B>, bodies: &[Vec<u8>]) -> u32 {
let mut bytes = Vec::new();
bytes.extend_from_slice(request.branch.as_bytes());
bytes.extend_from_slice(request.stream.as_str().as_bytes());
bytes.extend_from_slice(format!("{:?}", request.expected).as_bytes());
for (event, body) in request.events.iter().zip(bodies) {
bytes.extend_from_slice(event.event_id.unwrap_or(EventId::ZERO).as_bytes());
bytes.extend_from_slice(event.event_type.as_str().as_bytes());
bytes.extend_from_slice(&event.schema_version.to_le_bytes());
for (key, value) in &event.metadata {
bytes.extend_from_slice(key.as_bytes());
bytes.extend_from_slice(value);
}
bytes.extend_from_slice(body);
}
crc32c::crc32c(&bytes)
}