use crate::bus::KvStore;
use crate::error::BusError;
use bytes::Bytes;
use std::sync::Arc;
#[derive(Clone)]
pub struct OwnershipRegistry {
kv: Arc<dyn KvStore>,
bucket: String,
}
impl OwnershipRegistry {
pub fn new(kv: Arc<dyn KvStore>, bucket: String) -> Self {
Self { kv, bucket }
}
pub fn kv(&self) -> &Arc<dyn KvStore> {
&self.kv
}
pub fn bucket(&self) -> &str {
&self.bucket
}
#[tracing::instrument(
skip_all,
fields(
klieo.tenants.key = %key,
klieo.principal_hash = %crate::principal_hash(&principal),
db.system = "klieo-kv",
db.namespace = %self.bucket,
db.operation = "put",
),
err,
)]
pub async fn claim(&self, key: String, principal: String) -> Result<OwnershipHandle, BusError> {
self.kv
.put(&self.bucket, &key, Bytes::from(principal))
.await?;
Ok(OwnershipHandle {
kv: self.kv.clone(),
bucket: self.bucket.clone(),
key,
})
}
#[tracing::instrument(
skip_all,
fields(
klieo.tenants.key = %key,
klieo.tenants.found = tracing::field::Empty,
db.system = "klieo-kv",
db.namespace = %self.bucket,
db.operation = "get",
),
err,
level = "debug",
)]
pub async fn lookup(&self, key: &str) -> Result<Option<String>, BusError> {
let result = match self.kv.get(&self.bucket, key).await? {
Some(entry) => {
let s = String::from_utf8(entry.value.to_vec())
.map_err(|e| BusError::Permanent(format!("non-utf8 owner: {e}")))?;
Some(s)
}
None => None,
};
tracing::Span::current().record("klieo.tenants.found", result.is_some());
Ok(result)
}
}
pub struct OwnershipHandle {
kv: Arc<dyn KvStore>,
bucket: String,
key: String,
}
impl Drop for OwnershipHandle {
fn drop(&mut self) {
let kv = self.kv.clone();
let bucket = std::mem::take(&mut self.bucket);
let key = std::mem::take(&mut self.key);
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
if let Err(e) = kv.delete(&bucket, &key).await {
tracing::warn!(
target: "klieo.tenants",
bucket = %bucket,
key = %key,
error = %e,
"ownership delete failed; entry will linger until bucket TTL",
);
}
});
}
Err(_) => {
tracing::warn!(
target: "klieo.tenants",
bucket = %bucket,
key = %key,
"ownership delete skipped: no tokio runtime active during Drop",
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::fake_kv;
use std::time::Duration;
const BUCKET: &str = "klieo-tenants";
#[tokio::test]
async fn claim_writes_entry_and_lookup_returns_principal() {
let kv = fake_kv();
let reg = OwnershipRegistry::new(kv.clone(), BUCKET.into());
let _handle = reg.claim("a2a.t-1".into(), "alice".into()).await.unwrap();
assert_eq!(
reg.lookup("a2a.t-1").await.unwrap().as_deref(),
Some("alice")
);
}
#[tokio::test]
async fn drop_handle_deletes_entry() {
let kv = fake_kv();
let reg = OwnershipRegistry::new(kv.clone(), BUCKET.into());
{
let _handle = reg.claim("a2a.t-2".into(), "bob".into()).await.unwrap();
assert!(reg.lookup("a2a.t-2").await.unwrap().is_some());
}
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(reg.lookup("a2a.t-2").await.unwrap().is_none());
}
#[tokio::test]
async fn lookup_returns_none_for_unknown_key() {
let kv = fake_kv();
let reg = OwnershipRegistry::new(kv, BUCKET.into());
assert!(reg.lookup("never").await.unwrap().is_none());
}
}