clawft_kernel/cog_ingest/
registry.rs1use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10
11use super::forward::Forwarder;
12use super::types::IngestError;
13use crate::workload_runtime::HostContract;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct InstanceBinding {
18 pub instance_id: String,
20 pub project_id: Option<String>,
23 pub controller_node: String,
25}
26
27impl InstanceBinding {
28 pub fn new(
31 instance_id: impl Into<String>,
32 project_id: Option<String>,
33 controller_node: impl Into<String>,
34 ) -> Self {
35 Self {
36 instance_id: instance_id.into(),
37 project_id,
38 controller_node: controller_node.into(),
39 }
40 }
41
42 pub fn from_record(
44 rec: &crate::workload_ctl::PlacementRecord,
45 project_id: Option<String>,
46 controller_node: &str,
47 ) -> Self {
48 Self::new(rec.instance_id.clone(), project_id, controller_node)
49 }
50}
51
52#[derive(Default)]
54pub struct TokenRegistry {
55 inner: Mutex<RegInner>,
56}
57
58#[derive(Default)]
59struct RegInner {
60 by_hash: HashMap<[u8; 32], InstanceBinding>,
61 by_instance: HashMap<String, [u8; 32]>,
62}
63
64fn hash(token: &str) -> [u8; 32] {
65 *blake3::hash(token.as_bytes()).as_bytes()
66}
67
68impl TokenRegistry {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn register(
76 &self,
77 binding: InstanceBinding,
78 contract: &HostContract,
79 ) -> Result<(), IngestError> {
80 contract
81 .validate()
82 .map_err(|e| IngestError::Malformed(e.to_string()))?;
83 let h = hash(contract.token.expose());
84 let mut g = self.inner.lock().map_err(|_| poisoned())?;
85 if g.by_instance.contains_key(&binding.instance_id) || g.by_hash.contains_key(&h) {
86 return Err(IngestError::Malformed(
87 "instance or token already registered".into(),
88 ));
89 }
90 g.by_instance.insert(binding.instance_id.clone(), h);
91 g.by_hash.insert(h, binding);
92 Ok(())
93 }
94
95 pub fn revoke(&self, instance_id: &str) -> bool {
97 let Ok(mut g) = self.inner.lock() else {
98 return false;
99 };
100 match g.by_instance.remove(instance_id) {
101 Some(h) => g.by_hash.remove(&h).is_some(),
102 None => false,
103 }
104 }
105
106 pub fn lookup(&self, token: &str) -> Option<InstanceBinding> {
108 self.inner.lock().ok()?.by_hash.get(&hash(token)).cloned()
109 }
110
111 pub fn contains(&self, instance_id: &str) -> bool {
113 self.inner
114 .lock()
115 .map(|g| g.by_instance.contains_key(instance_id))
116 .unwrap_or(false)
117 }
118
119 pub fn len(&self) -> usize {
121 self.inner.lock().map(|g| g.by_instance.len()).unwrap_or(0)
122 }
123
124 pub fn is_empty(&self) -> bool {
126 self.len() == 0
127 }
128}
129
130fn poisoned() -> IngestError {
131 IngestError::Unavailable("registry lock poisoned".into())
132}
133
134pub struct RateBudget {
136 window: Duration,
137 max_requests: u32,
138 max_vectors: u32,
139 used: Mutex<HashMap<String, (Instant, u32, u32)>>,
140}
141
142impl RateBudget {
143 pub fn new(window: Duration, max_requests: u32, max_vectors: u32) -> Self {
145 Self {
146 window,
147 max_requests,
148 max_vectors,
149 used: Mutex::new(HashMap::new()),
150 }
151 }
152
153 pub fn charge_request(&self, instance: &str) -> bool {
155 self.charge(instance, 1, 0)
156 }
157
158 pub fn charge_vectors(&self, instance: &str, n: usize) -> bool {
160 self.charge(instance, 0, u32::try_from(n).unwrap_or(u32::MAX))
161 }
162
163 fn charge(&self, instance: &str, reqs: u32, vecs: u32) -> bool {
164 let Ok(mut m) = self.used.lock() else {
165 return false;
166 };
167 let now = Instant::now();
168 let e = m.entry(instance.to_string()).or_insert((now, 0, 0));
169 if now.duration_since(e.0) >= self.window {
170 *e = (now, 0, 0);
171 }
172 if e.1.saturating_add(reqs) > self.max_requests || e.2.saturating_add(vecs) > self.max_vectors
173 {
174 return false;
175 }
176 e.1 += reqs;
177 e.2 += vecs;
178 true
179 }
180
181 pub fn forget(&self, instance: &str) {
183 if let Ok(mut m) = self.used.lock() {
184 m.remove(instance);
185 }
186 }
187}
188
189impl Default for RateBudget {
190 fn default() -> Self {
192 Self::new(Duration::from_secs(1), 20, 2048)
193 }
194}
195
196pub trait StoreRouter: Send + Sync {
198 fn route(&self, b: &InstanceBinding) -> Option<Arc<dyn Forwarder>>;
200}
201
202#[derive(Default)]
207pub struct StaticRouter {
208 projects: Mutex<HashMap<String, Arc<dyn Forwarder>>>,
209 controllers: Mutex<HashMap<String, Arc<dyn Forwarder>>>,
210}
211
212impl StaticRouter {
213 pub fn new() -> Self {
215 Self::default()
216 }
217
218 pub fn with_project_route(self, project_id: &str, f: Arc<dyn Forwarder>) -> Self {
220 if let Ok(mut m) = self.projects.lock() {
221 m.insert(project_id.to_string(), f);
222 }
223 self
224 }
225
226 pub fn with_controller(self, controller_node: &str, f: Arc<dyn Forwarder>) -> Self {
228 if let Ok(mut m) = self.controllers.lock() {
229 m.insert(controller_node.to_string(), f);
230 }
231 self
232 }
233}
234
235impl StoreRouter for StaticRouter {
236 fn route(&self, b: &InstanceBinding) -> Option<Arc<dyn Forwarder>> {
237 match &b.project_id {
238 Some(p) => self.projects.lock().ok()?.get(p).cloned(),
239 None => self.controllers.lock().ok()?.get(&b.controller_node).cloned(),
240 }
241 }
242}