clawft_kernel/cog_ingest/
bridge.rs1use std::net::SocketAddr;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::time::Duration;
17
18use tokio::io::{AsyncReadExt, AsyncWriteExt};
19use tokio::net::{TcpListener, TcpStream};
20use tokio::task::JoinHandle;
21
22use super::registry::{InstanceBinding, RateBudget, StoreRouter, TokenRegistry};
23use super::types::{INGEST_PATH, IngestError, MAX_BODY_BYTES, parse_batch};
24
25pub const MAX_HEAD_BYTES: usize = 8 * 1024;
27pub const MAX_CONNECTIONS: usize = 128;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum BridgeScope {
33 Instance(String),
36 Token([u8; 32]),
40 Any,
42}
43
44#[derive(Debug, Clone)]
46pub struct BridgeConfig {
47 pub read_timeout: Duration,
49 pub preauth_timeout: Duration,
52 pub max_unauthenticated: usize,
60 pub allow_non_loopback: bool,
64}
65
66impl Default for BridgeConfig {
67 fn default() -> Self {
68 Self {
69 read_timeout: Duration::from_secs(5),
70 preauth_timeout: Duration::from_secs(2),
71 max_unauthenticated: 64,
72 allow_non_loopback: false,
73 }
74 }
75}
76
77#[derive(Debug, Default)]
79pub struct BridgeStats {
80 pub accepted: AtomicU64,
82 pub vectors: AtomicU64,
84 pub rejected: AtomicU64,
86 pub failed: AtomicU64,
88}
89
90pub struct IngestBridge {
92 registry: Arc<TokenRegistry>,
93 router: Arc<dyn StoreRouter>,
94 budget: RateBudget,
95 cfg: BridgeConfig,
96 stats: BridgeStats,
97}
98
99pub struct BridgeHandle {
101 addr: SocketAddr,
102 task: JoinHandle<()>,
103}
104
105impl BridgeHandle {
106 pub fn addr(&self) -> SocketAddr {
108 self.addr
109 }
110}
111
112impl Drop for BridgeHandle {
113 fn drop(&mut self) {
114 self.task.abort();
115 }
116}
117
118impl IngestBridge {
119 pub fn new(
121 registry: Arc<TokenRegistry>,
122 router: Arc<dyn StoreRouter>,
123 budget: RateBudget,
124 cfg: BridgeConfig,
125 ) -> Arc<Self> {
126 Arc::new(Self {
127 registry,
128 router,
129 budget,
130 cfg,
131 stats: BridgeStats::default(),
132 })
133 }
134
135 pub fn has_route(&self, b: &InstanceBinding) -> bool {
137 self.router.route(b).is_some()
138 }
139
140 pub fn forget(&self, instance_id: &str) {
142 self.budget.forget(instance_id);
143 }
144
145 pub fn stats(&self) -> &BridgeStats {
147 &self.stats
148 }
149
150 pub async fn bind(
153 self: &Arc<Self>,
154 addr: SocketAddr,
155 scope: BridgeScope,
156 ) -> Result<BridgeHandle, IngestError> {
157 let one_instance = matches!(scope, BridgeScope::Instance(_) | BridgeScope::Token(_));
158 if !(addr.ip().is_loopback() || self.cfg.allow_non_loopback && one_instance) {
159 return Err(IngestError::Malformed(format!(
160 "refusing to bind {addr}: the bridge listens on loopback, or on one \
161 instance-scoped address when allow_non_loopback is set"
162 )));
163 }
164 let listener = TcpListener::bind(addr)
165 .await
166 .map_err(|e| IngestError::Unavailable(format!("bind {addr}: {e}")))?;
167 let bound = listener
168 .local_addr()
169 .map_err(|e| IngestError::Unavailable(e.to_string()))?;
170 let me = self.clone();
171 let slots = Arc::new(tokio::sync::Semaphore::new(MAX_CONNECTIONS));
172 let unauth = Arc::new(tokio::sync::Semaphore::new(self.cfg.max_unauthenticated));
173 let task = tokio::spawn(async move {
174 loop {
175 let Ok((sock, _)) = listener.accept().await else {
176 return;
177 };
178 let Ok(slot) = slots.clone().try_acquire_owned() else {
179 continue; };
181 let (me, scope, unauth) = (me.clone(), scope.clone(), unauth.clone());
182 tokio::spawn(async move {
183 let _slot = slot;
184 let Ok(Ok(pre)) =
190 tokio::time::timeout(me.cfg.preauth_timeout, unauth.acquire_owned()).await
191 else {
192 return;
193 };
194 me.serve(sock, &scope, pre).await;
195 });
196 }
197 });
198 Ok(BridgeHandle { addr: bound, task })
199 }
200
201 async fn serve(
202 &self,
203 mut sock: TcpStream,
204 scope: &BridgeScope,
205 pre: tokio::sync::OwnedSemaphorePermit,
206 ) {
207 let res = tokio::time::timeout(
208 self.cfg.read_timeout,
209 self.handle(&mut sock, scope, pre),
210 )
211 .await;
212 let (status, body, extra) = match res {
213 Ok(Ok(v)) => (200, v, None),
214 Ok(Err(e)) => {
215 match &e {
216 IngestError::Unavailable(_) => self.stats.failed.fetch_add(1, Ordering::Relaxed),
217 _ => self.stats.rejected.fetch_add(1, Ordering::Relaxed),
218 };
219 let extra = matches!(e, IngestError::RateLimited).then_some("Retry-After: 1\r\n");
220 let msg = match &e {
222 IngestError::Unavailable(d) => {
223 tracing::warn!(detail = %d, "cog ingest forward failed");
224 "store unavailable".to_string()
225 }
226 other => other.to_string(),
227 };
228 (e.status(), serde_json::json!({"ok": false, "error": msg}), extra)
229 }
230 Err(_) => {
231 self.stats.rejected.fetch_add(1, Ordering::Relaxed);
232 (408, serde_json::json!({"ok": false, "error": "timeout"}), None)
233 }
234 };
235 let body = body.to_string();
236 let head = format!(
237 "HTTP/1.1 {status} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n{}Connection: close\r\n\r\n",
238 reason(status),
239 body.len(),
240 extra.unwrap_or("")
241 );
242 let _ = sock.write_all(head.as_bytes()).await;
243 let _ = sock.write_all(body.as_bytes()).await;
244 let _ = sock.shutdown().await;
245 }
246
247 async fn handle(
248 &self,
249 sock: &mut TcpStream,
250 scope: &BridgeScope,
251 pre: tokio::sync::OwnedSemaphorePermit,
252 ) -> Result<serde_json::Value, IngestError> {
253 let (req, mut body, binding) = tokio::time::timeout(self.cfg.preauth_timeout, async {
256 let (head, body) = read_head(sock).await?;
257 let req = parse_head(&head)?;
258 if req.path != INGEST_PATH {
259 return Err(IngestError::Malformed(format!("no such path `{}`", req.path)));
260 }
261 if req.method != "POST" {
262 return Err(IngestError::Malformed("POST only".into()));
263 }
264 let binding = self.authenticate(req.token.as_deref(), scope)?;
265 Ok((req, body, binding))
266 })
267 .await
268 .map_err(|_| IngestError::Malformed("request head not received in time".into()))??;
269 drop(pre);
270 if !self.budget.charge_request(&binding.instance_id) {
271 return Err(IngestError::RateLimited);
272 }
273 let len = req
274 .content_length
275 .ok_or_else(|| IngestError::Malformed("Content-Length required".into()))?;
276 if len > MAX_BODY_BYTES {
277 return Err(IngestError::TooLarge(format!(
278 "body over {MAX_BODY_BYTES} bytes"
279 )));
280 }
281 if body.len() > len {
282 body.truncate(len); }
284 while body.len() < len {
285 let mut chunk = vec![0u8; (len - body.len()).min(8192)];
286 let n = sock
287 .read(&mut chunk)
288 .await
289 .map_err(|e| IngestError::Malformed(format!("read: {e}")))?;
290 if n == 0 {
291 return Err(IngestError::Malformed("body shorter than Content-Length".into()));
292 }
293 body.extend_from_slice(&chunk[..n]);
294 }
295 let batch = parse_batch(&body)?;
296 if !self.budget.charge_vectors(&binding.instance_id, batch.vectors.len()) {
297 return Err(IngestError::RateLimited);
298 }
299 let fwd = self
300 .router
301 .route(&binding)
302 .ok_or_else(|| IngestError::Unavailable("no store owner for this placement".into()))?;
303 if !self.registry.contains(&binding.instance_id) {
307 return Err(IngestError::Unauthorized);
308 }
309 let out = fwd.forward(&binding, &batch).await?;
310 self.stats.accepted.fetch_add(1, Ordering::Relaxed);
311 self.stats
312 .vectors
313 .fetch_add(out.accepted as u64, Ordering::Relaxed);
314 Ok(serde_json::json!({
315 "ok": true,
316 "accepted": out.accepted,
317 "deduped": out.deduped,
318 }))
319 }
320
321 fn authenticate(
322 &self,
323 token: Option<&str>,
324 scope: &BridgeScope,
325 ) -> Result<InstanceBinding, IngestError> {
326 let b = token
327 .and_then(|t| self.registry.lookup(t))
328 .ok_or(IngestError::Unauthorized)?;
329 match scope {
330 BridgeScope::Instance(id) if *id != b.instance_id => Err(IngestError::Forbidden),
331 BridgeScope::Token(h)
332 if token.map(|t| blake3::hash(t.as_bytes())) != Some(blake3::Hash::from(*h)) =>
333 {
334 Err(IngestError::Forbidden)
335 }
336 _ => Ok(b),
337 }
338 }
339}
340
341fn reason(status: u16) -> &'static str {
342 match status {
343 200 => "OK",
344 400 => "Bad Request",
345 401 => "Unauthorized",
346 403 => "Forbidden",
347 408 => "Request Timeout",
348 411 => "Length Required",
349 413 => "Payload Too Large",
350 429 => "Too Many Requests",
351 502 => "Bad Gateway",
352 _ => "Error",
353 }
354}
355
356struct Head {
357 method: String,
358 path: String,
359 content_length: Option<usize>,
360 token: Option<String>,
361}
362
363async fn read_head(sock: &mut TcpStream) -> Result<(String, Vec<u8>), IngestError> {
366 let mut buf = Vec::with_capacity(1024);
367 loop {
368 if let Some(i) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
369 let rest = buf.split_off(i + 4);
370 buf.truncate(i);
371 let head = String::from_utf8(buf)
372 .map_err(|_| IngestError::Malformed("head is not UTF-8".into()))?;
373 return Ok((head, rest));
374 }
375 if buf.len() > MAX_HEAD_BYTES {
376 return Err(IngestError::TooLarge("request head".into()));
377 }
378 let mut chunk = [0u8; 1024];
379 let n = sock
380 .read(&mut chunk)
381 .await
382 .map_err(|e| IngestError::Malformed(format!("read: {e}")))?;
383 if n == 0 {
384 return Err(IngestError::Malformed("connection closed in head".into()));
385 }
386 buf.extend_from_slice(&chunk[..n]);
387 }
388}
389
390fn parse_head(head: &str) -> Result<Head, IngestError> {
391 let bad = |m: &str| IngestError::Malformed(m.to_string());
392 let mut lines = head.split("\r\n");
393 let line = lines.next().ok_or_else(|| bad("empty request"))?;
394 let mut parts = line.split(' ');
395 let (method, target, version) = (parts.next(), parts.next(), parts.next());
396 let (Some(method), Some(target), Some(version), None) = (method, target, version, parts.next())
397 else {
398 return Err(bad("bad request line"));
399 };
400 if version != "HTTP/1.1" && version != "HTTP/1.0" {
401 return Err(bad("unsupported HTTP version"));
402 }
403 let path = target.split('?').next().unwrap_or("").to_string();
404 let (mut content_length, mut token) = (None, None);
405 for l in lines {
406 let (k, v) = l.split_once(':').ok_or_else(|| bad("bad header line"))?;
407 let (k, v) = (k.trim().to_ascii_lowercase(), v.trim());
408 match k.as_str() {
409 "content-length" => {
410 if content_length.is_some() {
411 return Err(bad("duplicate Content-Length"));
412 }
413 content_length = Some(v.parse::<usize>().map_err(|_| bad("bad Content-Length"))?);
414 }
415 "transfer-encoding" => return Err(bad("Transfer-Encoding is not supported")),
416 "authorization" => {
417 let t = v.strip_prefix("Bearer ").or_else(|| v.strip_prefix("bearer "));
418 token = token.or(t.map(|t| t.trim().to_string()));
419 }
420 "x-api-key" | "x-cog-token" => token = token.or(Some(v.to_string())),
421 _ => {}
422 }
423 }
424 Ok(Head {
425 method: method.to_string(),
426 path,
427 content_length,
428 token,
429 })
430}