feat(verbo): verbo-daemon — embeddings compartidos entre procesos
Daemon que carga un Provider una vez y lo sirve sobre socket Unix; DaemonClient lo consume desde otro proceso implementando el trait Provider (indistinguible de un backend local). Multi-instancia: un daemon por modelo, cada uno en su socket. Frames postcard con prefijo de largo. 8 tests (wire + integración real sobre socket). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
//! Protocolo de cable del daemon — frames postcard con prefijo de largo.
|
||||
//!
|
||||
//! Cada mensaje va como `u32` little-endian (largo) + bytes postcard.
|
||||
//! Es el mismo encuadre que usa el resto de brahman para sockets.
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{self, ErrorKind};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use verbo_core::{EmbeddingVector, ModelId};
|
||||
|
||||
/// Tope de tamaño de un frame (8 MiB). Un lote grande de embeddings
|
||||
/// cabe holgado; cualquier cosa mayor se trata como frame corrupto.
|
||||
const MAX_FRAME: usize = 8 * 1024 * 1024;
|
||||
|
||||
/// Petición del cliente al daemon.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Request {
|
||||
/// Handshake: pide la identidad del modelo servido.
|
||||
ModelId,
|
||||
/// Embebe un texto.
|
||||
Embed(String),
|
||||
/// Embebe un lote en un solo round-trip.
|
||||
EmbedBatch(Vec<String>),
|
||||
}
|
||||
|
||||
/// Respuesta del daemon al cliente.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Response {
|
||||
ModelId(ModelId),
|
||||
Embed(EmbeddingVector),
|
||||
EmbedBatch(Vec<EmbeddingVector>),
|
||||
/// El backend falló; el texto es el `Display` del `EmbedError`.
|
||||
Error(String),
|
||||
}
|
||||
|
||||
/// Serializa `msg` y lo escribe como frame con prefijo de largo.
|
||||
pub async fn write_frame<W, T>(w: &mut W, msg: &T) -> io::Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
T: Serialize,
|
||||
{
|
||||
let bytes = postcard::to_stdvec(msg)
|
||||
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
|
||||
if bytes.len() > MAX_FRAME {
|
||||
return Err(io::Error::new(ErrorKind::InvalidData, "frame demasiado grande"));
|
||||
}
|
||||
w.write_all(&(bytes.len() as u32).to_le_bytes()).await?;
|
||||
w.write_all(&bytes).await?;
|
||||
w.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lee un frame y lo deserializa. `Ok(None)` si el peer cerró limpio
|
||||
/// antes de empezar un frame nuevo (EOF esperado).
|
||||
pub async fn read_frame<R, T>(r: &mut R) -> io::Result<Option<T>>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let mut len_buf = [0u8; 4];
|
||||
match r.read_exact(&mut len_buf).await {
|
||||
Ok(_) => {}
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(None),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
if len > MAX_FRAME {
|
||||
return Err(io::Error::new(ErrorKind::InvalidData, "frame demasiado grande"));
|
||||
}
|
||||
let mut buf = vec![0u8; len];
|
||||
r.read_exact(&mut buf).await?;
|
||||
let msg = postcard::from_bytes(&buf)
|
||||
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
|
||||
Ok(Some(msg))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn frame_roundtrips_through_a_buffer() {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
write_frame(&mut buf, &Request::Embed("hola".into())).await.unwrap();
|
||||
let mut cursor = std::io::Cursor::new(buf);
|
||||
let got: Request = read_frame(&mut cursor).await.unwrap().unwrap();
|
||||
assert!(matches!(got, Request::Embed(t) if t == "hola"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_stream_reads_as_none() {
|
||||
let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
|
||||
let got: Option<Request> = read_frame(&mut cursor).await.unwrap();
|
||||
assert!(got.is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user