use std::sync::Arc;
use crate::control::security::catalog::SystemCatalog;
use crate::control::security::catalog::sync_producer::StoredProducerRegistration;
use crate::control::sync_producer::allocator::{ProducerHwmPersist, ProducerIdAllocator};
use crate::control::sync_producer::persist::SystemCatalogProducerHwm;
#[derive(Debug, Clone, PartialEq)]
pub struct ProducerRegistration {
pub producer_id: u64,
pub current_epoch: u64,
pub tenant_id: u64,
pub created_ms: i64,
}
impl From<StoredProducerRegistration> for ProducerRegistration {
fn from(s: StoredProducerRegistration) -> Self {
Self {
producer_id: s.producer_id,
current_epoch: s.current_epoch,
tenant_id: s.tenant_id,
created_ms: s.created_ms,
}
}
}
impl From<&ProducerRegistration> for StoredProducerRegistration {
fn from(r: &ProducerRegistration) -> Self {
Self {
producer_id: r.producer_id,
current_epoch: r.current_epoch,
tenant_id: r.tenant_id,
created_ms: r.created_ms,
}
}
}
pub struct SyncProducerRegistry {
catalog: Arc<SystemCatalog>,
alloc: ProducerIdAllocator,
}
impl SyncProducerRegistry {
pub fn open(catalog: Arc<SystemCatalog>) -> crate::Result<Self> {
let hwm = catalog.get_producer_hwm()?;
let alloc = ProducerIdAllocator::from_persisted_hwm(hwm);
Ok(Self { catalog, alloc })
}
pub fn get(&self, lite_id: &str) -> crate::Result<Option<ProducerRegistration>> {
Ok(self
.catalog
.get_producer_registration(lite_id)?
.map(ProducerRegistration::from))
}
pub fn register(
&self,
lite_id: &str,
tenant_id: u64,
epoch: u64,
now_ms: i64,
) -> crate::Result<ProducerRegistration> {
let producer_id = self.alloc.alloc_one().map_err(crate::Error::from)?;
let hwm_persist = SystemCatalogProducerHwm::new(self.catalog.clone());
if self.alloc.should_flush() {
self.alloc.flush(&hwm_persist).map_err(crate::Error::from)?;
} else {
hwm_persist.checkpoint(self.alloc.current_hwm())?;
}
let reg = ProducerRegistration {
producer_id,
current_epoch: epoch,
tenant_id,
created_ms: now_ms,
};
self.catalog
.put_producer_registration(lite_id, &StoredProducerRegistration::from(®))?;
Ok(reg)
}
pub fn fence(&self, lite_id: &str, new_epoch: u64) -> crate::Result<()> {
let existing = self
.catalog
.get_producer_registration(lite_id)?
.ok_or_else(|| crate::Error::BadRequest {
detail: format!("no producer registration for lite_id '{lite_id}'"),
})?;
let updated = StoredProducerRegistration {
current_epoch: new_epoch,
..existing
};
self.catalog.put_producer_registration(lite_id, &updated)
}
pub fn apply_register(
&self,
lite_id: &str,
producer_id: u64,
tenant_id: u64,
epoch: u64,
created_ms: i64,
) -> crate::Result<()> {
self.alloc
.restore_hwm(producer_id)
.map_err(crate::Error::from)?;
let hwm_persist = SystemCatalogProducerHwm::new(self.catalog.clone());
hwm_persist.checkpoint(self.alloc.current_hwm())?;
let row = StoredProducerRegistration {
producer_id,
current_epoch: epoch,
tenant_id,
created_ms,
};
self.catalog.put_producer_registration(lite_id, &row)
}
pub fn apply_fence(&self, lite_id: &str, new_epoch: u64) -> crate::Result<()> {
let Some(existing) = self.catalog.get_producer_registration(lite_id)? else {
return Ok(());
};
let updated = StoredProducerRegistration {
current_epoch: existing.current_epoch.max(new_epoch),
..existing
};
self.catalog.put_producer_registration(lite_id, &updated)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn open_registry() -> (tempfile::TempDir, SyncProducerRegistry) {
let dir = tempfile::tempdir().unwrap();
let catalog = Arc::new(SystemCatalog::open(&dir.path().join("system.redb")).unwrap());
let reg = SyncProducerRegistry::open(catalog).unwrap();
(dir, reg)
}
#[test]
fn register_returns_nonzero_producer_id() {
let (_dir, reg) = open_registry();
let r = reg.register("device-1", 42, 0, 1_700_000_000_000).unwrap();
assert!(r.producer_id > 0, "producer_id must be > 0");
}
#[test]
fn get_after_register_returns_same_record() {
let (_dir, reg) = open_registry();
let created = reg.register("device-1", 42, 0, 1_700_000_000_000).unwrap();
let loaded = reg.get("device-1").unwrap().unwrap();
assert_eq!(loaded.producer_id, created.producer_id);
assert_eq!(loaded.tenant_id, 42);
assert_eq!(loaded.current_epoch, 0);
assert_eq!(loaded.created_ms, 1_700_000_000_000);
}
#[test]
fn get_unknown_lite_id_returns_none() {
let (_dir, reg) = open_registry();
assert!(reg.get("nobody").unwrap().is_none());
}
#[test]
fn distinct_lite_ids_get_distinct_producer_ids() {
let (_dir, reg) = open_registry();
let a = reg.register("device-a", 1, 0, 0).unwrap();
let b = reg.register("device-b", 1, 0, 0).unwrap();
assert_ne!(
a.producer_id, b.producer_id,
"each lite_id must get a unique producer_id"
);
}
#[test]
fn producer_ids_are_monotonically_increasing() {
let (_dir, reg) = open_registry();
let ids: Vec<u64> = (0..20)
.map(|i| {
reg.register(&format!("device-{i}"), 1, 0, 0)
.unwrap()
.producer_id
})
.collect();
for w in ids.windows(2) {
assert!(w[1] > w[0], "producer_ids must be monotonic");
}
}
#[test]
fn fence_advances_epoch() {
let (_dir, reg) = open_registry();
reg.register("device-1", 1, 0, 0).unwrap();
reg.fence("device-1", 5).unwrap();
let loaded = reg.get("device-1").unwrap().unwrap();
assert_eq!(loaded.current_epoch, 5);
}
#[test]
fn fence_unknown_lite_id_errors() {
let (_dir, reg) = open_registry();
let err = reg.fence("nobody", 1).unwrap_err();
assert!(
matches!(err, crate::Error::BadRequest { .. }),
"expected BadRequest, got {err:?}"
);
}
#[test]
fn registrations_survive_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("system.redb");
let producer_id = {
let catalog = Arc::new(SystemCatalog::open(&path).unwrap());
let reg = SyncProducerRegistry::open(catalog).unwrap();
let r = reg.register("device-1", 7, 0, 12345).unwrap();
reg.fence("device-1", 3).unwrap();
r.producer_id
};
let catalog = Arc::new(SystemCatalog::open(&path).unwrap());
let reg = SyncProducerRegistry::open(catalog).unwrap();
let loaded = reg.get("device-1").unwrap().unwrap();
assert_eq!(loaded.producer_id, producer_id);
assert_eq!(loaded.current_epoch, 3);
assert_eq!(loaded.tenant_id, 7);
}
#[test]
fn allocator_hwm_persists_across_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("system.redb");
{
let catalog = Arc::new(SystemCatalog::open(&path).unwrap());
let reg = SyncProducerRegistry::open(catalog).unwrap();
reg.register("d1", 1, 0, 0).unwrap();
reg.register("d2", 1, 0, 0).unwrap();
}
let catalog = Arc::new(SystemCatalog::open(&path).unwrap());
let reg = SyncProducerRegistry::open(catalog).unwrap();
let r = reg.register("d3", 1, 0, 0).unwrap();
assert!(
r.producer_id > 2,
"post-restart ids must be above pre-crash hwm"
);
}
#[test]
fn apply_register_writes_row_and_advances_hwm() {
let (_dir, reg) = open_registry();
reg.apply_register("device-x", 42, 7, 3, 1_700_000_000_000)
.unwrap();
let loaded = reg.get("device-x").unwrap().unwrap();
assert_eq!(loaded.producer_id, 42);
assert_eq!(loaded.current_epoch, 3);
assert_eq!(loaded.tenant_id, 7);
let next = reg.register("device-y", 7, 0, 0).unwrap();
assert!(
next.producer_id > 42,
"local allocation must not reissue an applied producer_id"
);
}
#[test]
fn apply_register_is_idempotent() {
let (_dir, reg) = open_registry();
reg.apply_register("device-x", 42, 7, 3, 100).unwrap();
reg.apply_register("device-x", 42, 7, 3, 100).unwrap();
let loaded = reg.get("device-x").unwrap().unwrap();
assert_eq!(loaded.producer_id, 42);
assert_eq!(loaded.current_epoch, 3);
}
#[test]
fn apply_fence_is_max_wins() {
let (_dir, reg) = open_registry();
reg.apply_register("device-x", 42, 7, 5, 100).unwrap();
reg.apply_fence("device-x", 3).unwrap();
assert_eq!(reg.get("device-x").unwrap().unwrap().current_epoch, 5);
reg.apply_fence("device-x", 9).unwrap();
assert_eq!(reg.get("device-x").unwrap().unwrap().current_epoch, 9);
}
#[test]
fn apply_fence_missing_registration_is_noop() {
let (_dir, reg) = open_registry();
reg.apply_fence("never-registered", 4).unwrap();
assert!(reg.get("never-registered").unwrap().is_none());
}
}