use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use super::forward::Forwarder;
use super::types::IngestError;
use crate::workload_runtime::HostContract;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstanceBinding {
pub instance_id: String,
pub project_id: Option<String>,
pub controller_node: String,
}
impl InstanceBinding {
pub fn new(
instance_id: impl Into<String>,
project_id: Option<String>,
controller_node: impl Into<String>,
) -> Self {
Self {
instance_id: instance_id.into(),
project_id,
controller_node: controller_node.into(),
}
}
pub fn from_record(
rec: &crate::workload_ctl::PlacementRecord,
project_id: Option<String>,
controller_node: &str,
) -> Self {
Self::new(rec.instance_id.clone(), project_id, controller_node)
}
}
#[derive(Default)]
pub struct TokenRegistry {
inner: Mutex<RegInner>,
}
#[derive(Default)]
struct RegInner {
by_hash: HashMap<[u8; 32], InstanceBinding>,
by_instance: HashMap<String, [u8; 32]>,
}
fn hash(token: &str) -> [u8; 32] {
*blake3::hash(token.as_bytes()).as_bytes()
}
impl TokenRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(
&self,
binding: InstanceBinding,
contract: &HostContract,
) -> Result<(), IngestError> {
contract
.validate()
.map_err(|e| IngestError::Malformed(e.to_string()))?;
let h = hash(contract.token.expose());
let mut g = self.inner.lock().map_err(|_| poisoned())?;
if g.by_instance.contains_key(&binding.instance_id) || g.by_hash.contains_key(&h) {
return Err(IngestError::Malformed(
"instance or token already registered".into(),
));
}
g.by_instance.insert(binding.instance_id.clone(), h);
g.by_hash.insert(h, binding);
Ok(())
}
pub fn revoke(&self, instance_id: &str) -> bool {
let Ok(mut g) = self.inner.lock() else {
return false;
};
match g.by_instance.remove(instance_id) {
Some(h) => g.by_hash.remove(&h).is_some(),
None => false,
}
}
pub fn lookup(&self, token: &str) -> Option<InstanceBinding> {
self.inner.lock().ok()?.by_hash.get(&hash(token)).cloned()
}
pub fn contains(&self, instance_id: &str) -> bool {
self.inner
.lock()
.map(|g| g.by_instance.contains_key(instance_id))
.unwrap_or(false)
}
pub fn len(&self) -> usize {
self.inner.lock().map(|g| g.by_instance.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
fn poisoned() -> IngestError {
IngestError::Unavailable("registry lock poisoned".into())
}
pub struct RateBudget {
window: Duration,
max_requests: u32,
max_vectors: u32,
used: Mutex<HashMap<String, (Instant, u32, u32)>>,
}
impl RateBudget {
pub fn new(window: Duration, max_requests: u32, max_vectors: u32) -> Self {
Self {
window,
max_requests,
max_vectors,
used: Mutex::new(HashMap::new()),
}
}
pub fn charge_request(&self, instance: &str) -> bool {
self.charge(instance, 1, 0)
}
pub fn charge_vectors(&self, instance: &str, n: usize) -> bool {
self.charge(instance, 0, u32::try_from(n).unwrap_or(u32::MAX))
}
fn charge(&self, instance: &str, reqs: u32, vecs: u32) -> bool {
let Ok(mut m) = self.used.lock() else {
return false;
};
let now = Instant::now();
let e = m.entry(instance.to_string()).or_insert((now, 0, 0));
if now.duration_since(e.0) >= self.window {
*e = (now, 0, 0);
}
if e.1.saturating_add(reqs) > self.max_requests || e.2.saturating_add(vecs) > self.max_vectors
{
return false;
}
e.1 += reqs;
e.2 += vecs;
true
}
pub fn forget(&self, instance: &str) {
if let Ok(mut m) = self.used.lock() {
m.remove(instance);
}
}
}
impl Default for RateBudget {
fn default() -> Self {
Self::new(Duration::from_secs(1), 20, 2048)
}
}
pub trait StoreRouter: Send + Sync {
fn route(&self, b: &InstanceBinding) -> Option<Arc<dyn Forwarder>>;
}
#[derive(Default)]
pub struct StaticRouter {
projects: Mutex<HashMap<String, Arc<dyn Forwarder>>>,
controllers: Mutex<HashMap<String, Arc<dyn Forwarder>>>,
}
impl StaticRouter {
pub fn new() -> Self {
Self::default()
}
pub fn with_project_route(self, project_id: &str, f: Arc<dyn Forwarder>) -> Self {
if let Ok(mut m) = self.projects.lock() {
m.insert(project_id.to_string(), f);
}
self
}
pub fn with_controller(self, controller_node: &str, f: Arc<dyn Forwarder>) -> Self {
if let Ok(mut m) = self.controllers.lock() {
m.insert(controller_node.to_string(), f);
}
self
}
}
impl StoreRouter for StaticRouter {
fn route(&self, b: &InstanceBinding) -> Option<Arc<dyn Forwarder>> {
match &b.project_id {
Some(p) => self.projects.lock().ok()?.get(p).cloned(),
None => self.controllers.lock().ok()?.get(&b.controller_node).cloned(),
}
}
}