use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use ed25519_dalek::SigningKey;
use serde::{Deserialize, Serialize};
use super::registry::InstanceBinding;
use super::store::{IngestOutcome, Provenance, StoreDirectory, StoreError};
use super::types::{IngestBatch, IngestError, IngestVector};
use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
use crate::mesh::MeshStream;
use crate::mesh_ipc::{MeshIpcEnvelope, MeshRequest};
use crate::node_registry::node_id_from_pubkey;
use crate::workload_ctl::msg::{SignedCtl, fresh_nonce, open, sign};
use crate::workload_ctl::transport::CtlConnector;
pub const STORE_SERVICE: &str = "cog-store";
pub const STORE_INGEST_METHOD: &str = "store.ingest";
pub const FORWARD_VERSION: u32 = 1;
pub const FORWARD_DOMAIN: &[u8] = b"weftos.cog_ingest.forward.v1\0";
pub const FORWARD_RESPONSE_DOMAIN: &[u8] = b"weftos.cog_ingest.forward_response.v1\0";
pub const MEM_SCHEME: &str = "mem://";
pub(super) fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[async_trait]
pub trait Forwarder: Send + Sync {
async fn forward(
&self,
b: &InstanceBinding,
batch: &IngestBatch,
) -> Result<IngestOutcome, IngestError>;
}
fn store_err(e: StoreError) -> IngestError {
IngestError::Unavailable(e.to_string())
}
pub struct LocalForwarder {
node_id: String,
directory: Arc<dyn StoreDirectory>,
}
impl LocalForwarder {
pub fn new(node_id: impl Into<String>, directory: Arc<dyn StoreDirectory>) -> Self {
Self {
node_id: node_id.into(),
directory,
}
}
}
#[async_trait]
impl Forwarder for LocalForwarder {
async fn forward(
&self,
b: &InstanceBinding,
batch: &IngestBatch,
) -> Result<IngestOutcome, IngestError> {
let store = self
.directory
.store_for(b.project_id.as_deref())
.ok_or_else(|| IngestError::Unavailable("no store for this placement".into()))?;
let from = Provenance {
instance_id: b.instance_id.clone(),
source_node: self.node_id.clone(),
};
store
.ingest(&from, &batch.vectors, batch.dedup)
.map_err(store_err)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ForwardRequest {
pub version: u32,
pub requester: String,
pub target: String,
pub nonce: String,
pub issued_at_ms: u64,
pub expires_at_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_id: Option<String>,
pub instance_id: String,
pub controller_node: String,
pub dedup: bool,
pub vectors: Vec<IngestVector>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ForwardRefusal {
Signature,
Expired,
Replay,
NotForMe,
Unauthorized,
Invalid,
NoStore,
StoreFull,
Backend,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum ForwardOutcome {
Ok {
result: IngestOutcome,
},
Refused {
code: ForwardRefusal,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ForwardResponse {
pub version: u32,
pub responder: String,
pub request_nonce: String,
pub outcome: ForwardOutcome,
}
impl ForwardRequest {
pub fn signed(
key: &SigningKey,
target: &str,
b: &InstanceBinding,
batch: &IngestBatch,
ttl_ms: u64,
) -> Result<(Self, SignedCtl), IngestError> {
let now = now_ms();
let req = Self {
version: FORWARD_VERSION,
requester: node_id_from_pubkey(&key.verifying_key().to_bytes()),
target: target.to_string(),
nonce: fresh_nonce(),
issued_at_ms: now,
expires_at_ms: now.saturating_add(ttl_ms),
project_id: b.project_id.clone(),
instance_id: b.instance_id.clone(),
controller_node: b.controller_node.clone(),
dedup: batch.dedup,
vectors: batch.vectors.clone(),
};
let payload = serde_json::to_string(&req)
.map_err(|e| IngestError::Unavailable(format!("encode: {e}")))?;
let signed = sign(FORWARD_DOMAIN, payload, key);
Ok((req, signed))
}
}
pub(super) fn owner_target() -> MessageTarget {
MessageTarget::ServiceMethod {
service: STORE_SERVICE.to_string(),
method: STORE_INGEST_METHOD.to_string(),
}
}
pub struct MeshForwarder {
key: SigningKey,
local_node: String,
owner_node: String,
owner_key: [u8; 32],
addr: String,
connector: Arc<dyn CtlConnector>,
timeout: Duration,
conn: tokio::sync::Mutex<Option<Box<dyn MeshStream>>>,
}
impl MeshForwarder {
pub fn new(
key: SigningKey,
owner_node: &str,
owner_key: [u8; 32],
addr: &str,
connector: Arc<dyn CtlConnector>,
) -> Self {
Self {
local_node: node_id_from_pubkey(&key.verifying_key().to_bytes()),
key,
owner_node: owner_node.to_string(),
owner_key,
addr: addr.to_string(),
connector,
timeout: Duration::from_secs(5),
conn: tokio::sync::Mutex::new(None),
}
}
async fn round_trip(
&self,
guard: &mut Option<Box<dyn MeshStream>>,
req: &MeshRequest,
bytes: &[u8],
) -> Result<MeshIpcEnvelope, String> {
if guard.is_none() {
*guard = Some(
self.connector
.connect(&self.addr)
.await
.map_err(|e| e.to_string())?,
);
}
let s = guard.as_mut().ok_or("no connection")?;
s.send(bytes).await.map_err(|e| e.to_string())?;
let raw = tokio::time::timeout(self.timeout, s.recv())
.await
.map_err(|_| "timed out".to_string())?
.map_err(|e| e.to_string())?;
let env = MeshIpcEnvelope::from_bytes(&raw).map_err(|e| e.to_string())?;
if !req.matches_response(&env) {
return Err("uncorrelated response".into());
}
Ok(env)
}
}
#[async_trait]
impl Forwarder for MeshForwarder {
async fn forward(
&self,
b: &InstanceBinding,
batch: &IngestBatch,
) -> Result<IngestOutcome, IngestError> {
let unavailable = |m: String| IngestError::Unavailable(m);
let (sent, signed) = ForwardRequest::signed(&self.key, &self.owner_node, b, batch, 30_000)?;
let msg = KernelMessage::new(
0,
owner_target(),
MessagePayload::Json(serde_json::to_value(&signed).unwrap_or_default()),
);
let req = MeshRequest::new(
MeshIpcEnvelope::new(self.local_node.clone(), self.owner_node.clone(), msg),
self.timeout,
);
let bytes = req.request.to_bytes().map_err(|e| unavailable(e.to_string()))?;
let mut guard = self.conn.lock().await;
let env = match self.round_trip(&mut guard, &req, &bytes).await {
Ok(e) => e,
Err(first) => {
*guard = None;
self.round_trip(&mut guard, &req, &bytes)
.await
.map_err(|e| unavailable(format!("{first}; retry: {e}")))?
}
};
drop(guard);
let MessagePayload::Json(v) = env.message.payload else {
return Err(unavailable("response payload is not JSON".into()));
};
let signed: SignedCtl = serde_json::from_value(v).map_err(|e| unavailable(e.to_string()))?;
let pk = open(FORWARD_RESPONSE_DOMAIN, &signed).map_err(|r| unavailable(r.reason))?;
if pk != self.owner_key {
return Err(unavailable("response signed by another key".into()));
}
let resp: ForwardResponse =
serde_json::from_str(&signed.payload).map_err(|e| unavailable(e.to_string()))?;
if resp.responder != self.owner_node || resp.request_nonce != sent.nonce {
return Err(unavailable("response answers another request".into()));
}
match resp.outcome {
ForwardOutcome::Ok { result } => Ok(result),
ForwardOutcome::Refused { code, reason } => {
Err(unavailable(format!("owner refused ({code:?}): {reason}")))
}
}
}
}