use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use hmac::{Hmac, Mac};
use tokio::sync::broadcast;
use exocortex_kernel::OntologyFingerprint;
use exocortex_storage::{Invalidation, LeaseKey, OwnerLease, Storage};
use exocortex_wire::cluster::v1::InvalidationEnvelope;
use exocortex_wire::WIRE_VERSION;
use prost::Message;
#[derive(Debug, thiserror::Error)]
pub enum ClusterError {
#[error("wire version mismatch")]
WireMismatch,
#[error("ontology mismatch")]
OntologyMismatch,
#[error("hmac verification failed")]
HmacFailed,
#[error("storage: {0}")]
Storage(String),
}
pub struct ClusterNode<S: Storage> {
pub storage: Arc<S>,
pub node_id: smol_str::SmolStr,
pub fp: OntologyFingerprint,
pub hmac_key: [u8; 32],
pub tx: broadcast::Sender<InvalidationEnvelope>,
replay: Mutex<VecDeque<InvalidationEnvelope>>,
replay_cap: usize,
}
pub const REPLAY_CAPACITY_DEFAULT: usize = 1024;
#[derive(Debug, Clone)]
pub enum Replay {
Fresh(Vec<InvalidationEnvelope>),
TooOld,
}
impl<S: Storage + 'static> ClusterNode<S> {
pub fn new(
storage: Arc<S>,
node_id: smol_str::SmolStr,
fp: OntologyFingerprint,
hmac_key: [u8; 32],
) -> Self {
let (tx, _) = broadcast::channel(4096);
Self {
storage,
node_id,
fp,
hmac_key,
tx,
replay: Mutex::new(VecDeque::with_capacity(REPLAY_CAPACITY_DEFAULT)),
replay_cap: REPLAY_CAPACITY_DEFAULT,
}
}
pub fn with_replay_capacity(mut self, cap: usize) -> Self {
self.replay_cap = cap.max(1);
self
}
pub fn replay_since(&self, since_lsn: u64) -> Replay {
let ring = self.replay.lock().unwrap();
let Some(oldest) = ring.front() else {
return Replay::Fresh(vec![]);
};
let floor = envelope_lsn(oldest);
if since_lsn + 1 < floor {
return Replay::TooOld;
}
Replay::Fresh(
ring.iter()
.filter(|e| envelope_lsn(e) > since_lsn)
.cloned()
.collect(),
)
}
pub fn replay_floor(&self) -> u64 {
self.replay
.lock()
.unwrap()
.front()
.map(envelope_lsn)
.unwrap_or(1)
}
fn record_replay(&self, env: InvalidationEnvelope) {
let mut ring = self.replay.lock().unwrap();
if ring.len() == self.replay_cap {
ring.pop_front();
}
ring.push_back(env);
}
pub fn publish_envelope(&self, env: InvalidationEnvelope) {
self.record_replay(env.clone());
metrics::counter!("exocortex_cluster_invalidations_published_total").increment(1);
let _ = self.tx.send(env);
}
pub async fn run(self: Arc<Self>) -> anyhow::Result<()> {
let region = exocortex_storage::RegionKey {
org: "*".into(),
project: "*".into(),
memory_type: 0,
};
let mut sub = self.storage.subscribe_invalidations(®ion).await?;
use futures::StreamExt;
while let Some(inv) = sub.next().await {
let Ok(inv) = inv else { continue };
let env = self.envelope(inv);
self.publish_envelope(env);
}
Ok(())
}
pub fn envelope(&self, inv: Invalidation) -> InvalidationEnvelope {
let inv_pb = crate::sse::invalidation_to_pb(&inv);
let mut env = InvalidationEnvelope {
wire_version: WIRE_VERSION,
ontology_fingerprint: self.fp.0.to_vec(),
emitter_node_id: self.node_id.to_string(),
inv: Some(inv_pb),
hmac: vec![],
};
let mut mac = <Hmac<Sha256Mac> as Mac>::new_from_slice(&self.hmac_key)
.expect("HMAC accepts any key length");
mac.update(&env.encode_to_vec());
env.hmac = mac.finalize().into_bytes().to_vec();
env
}
pub fn verify_hmac(&self, env: &InvalidationEnvelope) -> Result<(), ClusterError> {
let mut unsigned = env.clone();
unsigned.hmac = vec![];
let mut mac = <Hmac<Sha256Mac> as Mac>::new_from_slice(&self.hmac_key)
.expect("HMAC accepts any key length");
mac.update(&unsigned.encode_to_vec());
let expected = mac.finalize().into_bytes();
if expected.len() != env.hmac.len()
|| !bool::from(subtle::ConstantTimeEq::ct_eq(
expected.as_slice(),
env.hmac.as_slice(),
))
{
return Err(ClusterError::HmacFailed);
}
Ok(())
}
pub fn admit(&self, env: &InvalidationEnvelope) -> Result<(), ClusterError> {
if env.wire_version != WIRE_VERSION {
return Err(ClusterError::WireMismatch);
}
if env.ontology_fingerprint.as_slice() != self.fp.0.as_slice() {
return Err(ClusterError::OntologyMismatch);
}
self.verify_hmac(env)?;
Ok(())
}
pub async fn acquire(
&self,
key: LeaseKey,
ttl: std::time::Duration,
) -> Result<OwnerLease, ClusterError> {
self.storage
.acquire_lease(&key, ttl)
.await
.map_err(|e| ClusterError::Storage(e.to_string()))
}
pub fn subscribe_local(&self) -> broadcast::Receiver<InvalidationEnvelope> {
self.tx.subscribe()
}
}
type Sha256Mac = sha2::Sha256;
fn envelope_lsn(env: &InvalidationEnvelope) -> u64 {
env.inv.as_ref().map(|i| i.backend_lsn).unwrap_or(0)
}