use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteResult {
pub peer_id: String,
pub bindings: HashMap<String, String>,
pub proof_depth: u32,
pub latency_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceRequest {
pub request_id: String,
pub goal: String,
pub max_depth: u32,
pub requester_peer_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceResponse {
pub request_id: String,
pub bindings: Vec<HashMap<String, String>>,
pub proof_found: bool,
pub error: Option<String>,
}
#[derive(Debug)]
pub struct DistributedInferenceSession {
pub session_id: String,
pub goal: String,
pub local_results: Vec<String>,
pub remote_results: Vec<RemoteResult>,
pub pending_peers: HashSet<String>,
pub completed_peers: HashSet<String>,
pub started_at: std::time::Instant,
pub timeout: std::time::Duration,
}
impl DistributedInferenceSession {
pub fn new(goal: &str, timeout: std::time::Duration) -> Self {
Self {
session_id: uuid::Uuid::new_v4().to_string(),
goal: goal.to_string(),
local_results: Vec::new(),
remote_results: Vec::new(),
pending_peers: HashSet::new(),
completed_peers: HashSet::new(),
started_at: std::time::Instant::now(),
timeout,
}
}
pub fn is_complete(&self) -> bool {
let any_peers_registered =
!self.pending_peers.is_empty() || !self.completed_peers.is_empty();
if self.started_at.elapsed() >= self.timeout {
return true; }
if !any_peers_registered {
return false;
}
self.pending_peers.is_empty()
}
pub fn is_expired(&self) -> bool {
self.started_at.elapsed() >= self.timeout
}
}
#[derive(Debug, thiserror::Error)]
pub enum ReasoningError {
#[error("Session not found: {0}")]
SessionNotFound(String),
#[error("Peer already registered: {0}")]
PeerAlreadyRegistered(String),
#[error("Peer not registered in session: {0}")]
PeerNotRegistered(String),
#[error("Session has already expired")]
SessionExpired,
}
#[derive(Debug, Clone)]
pub struct DistributedReasonerConfig {
pub max_depth: usize,
pub timeout: std::time::Duration,
pub max_peers: usize,
pub cache_ttl: std::time::Duration,
pub parallel_queries: usize,
}
impl Default for DistributedReasonerConfig {
fn default() -> Self {
Self {
max_depth: 10,
timeout: std::time::Duration::from_secs(30),
max_peers: 5,
cache_ttl: std::time::Duration::from_secs(300),
parallel_queries: 3,
}
}
}
#[derive(Debug, Default)]
pub struct SessionStats {
pub active_sessions: usize,
pub total_results: usize,
pub avg_latency_ms: f64,
pub cache_hit_rate: f64,
}
pub struct DistributedReasonerV2 {
#[allow(dead_code)]
local_reasoner: crate::reasoning::MemoizedInferenceEngine,
pub(super) sessions: HashMap<String, DistributedInferenceSession>,
cache: HashMap<String, (std::time::Instant, Vec<RemoteResult>)>,
config: DistributedReasonerConfig,
cache_lookups: u64,
cache_hits: u64,
}
impl DistributedReasonerV2 {
pub fn new(config: DistributedReasonerConfig) -> Self {
let cache_mgr = std::sync::Arc::new(crate::cache::CacheManager::new());
Self {
local_reasoner: crate::reasoning::MemoizedInferenceEngine::new(cache_mgr),
sessions: HashMap::new(),
cache: HashMap::new(),
config,
cache_lookups: 0,
cache_hits: 0,
}
}
pub fn start_session(&mut self, goal: &str) -> String {
let session = DistributedInferenceSession::new(goal, self.config.timeout);
let id = session.session_id.clone();
self.sessions.insert(id.clone(), session);
id
}
pub fn start_session_with_id(&mut self, goal: &str, session_id: &str) {
let mut session = DistributedInferenceSession::new(goal, self.config.timeout);
session.session_id = session_id.to_string();
self.sessions.insert(session_id.to_string(), session);
}
pub fn add_session_peer(
&mut self,
session_id: &str,
peer_id: &str,
) -> Result<(), ReasoningError> {
let session = self
.sessions
.get_mut(session_id)
.ok_or_else(|| ReasoningError::SessionNotFound(session_id.to_string()))?;
if session.pending_peers.contains(peer_id) || session.completed_peers.contains(peer_id) {
return Err(ReasoningError::PeerAlreadyRegistered(peer_id.to_string()));
}
session.pending_peers.insert(peer_id.to_string());
Ok(())
}
pub fn record_remote_result(
&mut self,
session_id: &str,
result: RemoteResult,
) -> Result<(), ReasoningError> {
let session = self
.sessions
.get_mut(session_id)
.ok_or_else(|| ReasoningError::SessionNotFound(session_id.to_string()))?;
let peer = result.peer_id.clone();
session.pending_peers.remove(&peer);
session.completed_peers.insert(peer);
session.remote_results.push(result);
Ok(())
}
pub fn mark_peer_responded(
&mut self,
session_id: &str,
peer_id: &str,
) -> Result<(), ReasoningError> {
let session = self
.sessions
.get_mut(session_id)
.ok_or_else(|| ReasoningError::SessionNotFound(session_id.to_string()))?;
if !session.pending_peers.contains(peer_id) && !session.completed_peers.contains(peer_id) {
return Err(ReasoningError::PeerNotRegistered(peer_id.to_string()));
}
session.pending_peers.remove(peer_id);
session.completed_peers.insert(peer_id.to_string());
Ok(())
}
pub fn is_session_complete(&self, session_id: &str) -> bool {
self.sessions
.get(session_id)
.map(|s| s.is_complete())
.unwrap_or(false)
}
pub fn get_session_results(&mut self, session_id: &str) -> Option<Vec<RemoteResult>> {
self.cache_lookups += 1;
let session = self.sessions.get(session_id)?;
let goal = session.goal.clone();
let ttl = self.config.cache_ttl;
if let Some((cached_at, cached)) = self.cache.get(&goal) {
if cached_at.elapsed() < ttl {
self.cache_hits += 1;
return Some(cached.clone());
}
}
let results = session.remote_results.clone();
if !results.is_empty() {
self.cache
.insert(goal, (std::time::Instant::now(), results.clone()));
}
Some(results)
}
pub fn cleanup_expired_sessions(&mut self) -> usize {
let expired: Vec<String> = self
.sessions
.iter()
.filter(|(_, s)| s.is_expired())
.map(|(id, _)| id.clone())
.collect();
let count = expired.len();
for id in expired {
self.sessions.remove(&id);
}
count
}
pub fn evict_stale_cache(&mut self) -> usize {
let ttl = self.config.cache_ttl;
let before = self.cache.len();
self.cache
.retain(|_, (cached_at, _)| cached_at.elapsed() < ttl);
before - self.cache.len()
}
pub fn session_stats(&self) -> SessionStats {
let active_sessions = self.sessions.values().filter(|s| !s.is_complete()).count();
let all_results: Vec<&RemoteResult> = self
.sessions
.values()
.flat_map(|s| s.remote_results.iter())
.collect();
let total_results = all_results.len();
let avg_latency_ms = if total_results == 0 {
0.0
} else {
let sum: u64 = all_results.iter().map(|r| r.latency_ms).sum();
sum as f64 / total_results as f64
};
let cache_hit_rate = if self.cache_lookups == 0 {
0.0
} else {
self.cache_hits as f64 / self.cache_lookups as f64
};
SessionStats {
active_sessions,
total_results,
avg_latency_ms,
cache_hit_rate,
}
}
#[inline]
pub fn session_count(&self) -> usize {
self.sessions.len()
}
pub fn gc_sessions(&mut self, max_age_secs: u64) -> usize {
let max_age = std::time::Duration::from_secs(max_age_secs);
let expired: Vec<String> = self
.sessions
.iter()
.filter(|(_, s)| s.started_at.elapsed() >= max_age)
.map(|(id, _)| id.clone())
.collect();
let count = expired.len();
for id in expired {
self.sessions.remove(&id);
}
count
}
pub fn session_metrics(&self) -> SessionMetrics {
let mut active_sessions: usize = 0;
let mut completed_sessions: usize = 0;
let mut expired_sessions: usize = 0;
let mut total_peers: usize = 0;
let mut latency_sum: u64 = 0;
let mut result_count: usize = 0;
for session in self.sessions.values() {
let is_expired = session.is_expired();
let is_complete = session.is_complete();
if is_expired {
expired_sessions += 1;
}
if is_complete {
completed_sessions += 1;
} else {
active_sessions += 1;
}
total_peers += session.pending_peers.len() + session.completed_peers.len();
for result in &session.remote_results {
latency_sum += result.latency_ms;
result_count += 1;
}
}
let session_count = self.sessions.len();
let avg_peers_per_session = if session_count == 0 {
0.0
} else {
total_peers as f64 / session_count as f64
};
let avg_latency_ms = if result_count == 0 {
0.0
} else {
latency_sum as f64 / result_count as f64
};
SessionMetrics {
active_sessions,
completed_sessions,
expired_sessions,
avg_peers_per_session,
avg_latency_ms,
}
}
}
#[derive(Debug, Clone)]
pub struct SessionMetrics {
pub active_sessions: usize,
pub completed_sessions: usize,
pub expired_sessions: usize,
pub avg_peers_per_session: f64,
pub avg_latency_ms: f64,
}
#[derive(Debug, Clone)]
pub struct PartialResult {
pub peer_id: String,
pub new_bindings: Vec<HashMap<String, String>>,
pub total_so_far: usize,
pub is_final: bool,
}
pub struct InferenceResultStream {
session_id: String,
rx: tokio::sync::mpsc::Receiver<InferenceResponse>,
accumulated: Vec<HashMap<String, String>>,
deadline: tokio::time::Instant,
}
impl InferenceResultStream {
pub fn new(
session_id: String,
rx: tokio::sync::mpsc::Receiver<InferenceResponse>,
deadline: tokio::time::Instant,
) -> Self {
Self {
session_id,
rx,
accumulated: Vec::new(),
deadline,
}
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn result_count(&self) -> usize {
self.accumulated.len()
}
pub async fn next_partial(&mut self) -> Option<PartialResult> {
let remaining = self
.deadline
.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return None;
}
match tokio::time::timeout(remaining, self.rx.recv()).await {
Ok(Some(resp)) => {
let new_bindings: Vec<HashMap<String, String>> = resp.bindings;
self.accumulated.extend(new_bindings.clone());
let total_so_far = self.accumulated.len();
let is_final = self
.deadline
.saturating_duration_since(tokio::time::Instant::now())
.is_zero();
Some(PartialResult {
peer_id: resp.request_id,
new_bindings,
total_so_far,
is_final,
})
}
Ok(None) | Err(_) => None,
}
}
}
#[cfg(test)]
mod distributed_v2_tests {
use super::*;
fn make_reasoner() -> DistributedReasonerV2 {
DistributedReasonerV2::new(DistributedReasonerConfig {
max_depth: 5,
timeout: std::time::Duration::from_secs(10),
max_peers: 3,
cache_ttl: std::time::Duration::from_secs(60),
parallel_queries: 2,
})
}
#[test]
fn test_session_lifecycle() {
let mut reasoner = make_reasoner();
let session_id = reasoner.start_session("parent(alice, bob)");
assert!(!reasoner.is_session_complete(&session_id));
reasoner
.add_session_peer(&session_id, "peer1")
.expect("add peer1");
reasoner
.mark_peer_responded(&session_id, "peer1")
.expect("peer1 responded");
assert!(reasoner.is_session_complete(&session_id));
}
#[test]
fn test_multiple_peers_session() {
let mut reasoner = make_reasoner();
let sid = reasoner.start_session("ancestor(alice, Z)");
reasoner.add_session_peer(&sid, "peer1").expect("add peer1");
reasoner.add_session_peer(&sid, "peer2").expect("add peer2");
reasoner
.mark_peer_responded(&sid, "peer1")
.expect("peer1 responded");
assert!(!reasoner.is_session_complete(&sid));
reasoner
.mark_peer_responded(&sid, "peer2")
.expect("peer2 responded");
assert!(reasoner.is_session_complete(&sid));
}
#[test]
fn test_record_remote_result() {
let mut reasoner = make_reasoner();
let sid = reasoner.start_session("parent(alice, X)");
reasoner
.add_session_peer(&sid, "peer-alpha")
.expect("add peer");
let result = RemoteResult {
peer_id: "peer-alpha".to_string(),
bindings: [("X".to_string(), "bob".to_string())].into_iter().collect(),
proof_depth: 1,
latency_ms: 42,
};
reasoner
.record_remote_result(&sid, result)
.expect("record result");
let session = reasoner.sessions.get(&sid).expect("session exists");
assert!(!session.pending_peers.contains("peer-alpha"));
assert!(session.completed_peers.contains("peer-alpha"));
assert_eq!(session.remote_results.len(), 1);
let results = reasoner.get_session_results(&sid).expect("get results");
assert_eq!(results.len(), 1);
assert_eq!(results[0].latency_ms, 42);
}
#[test]
fn test_record_result_unknown_session() {
let mut reasoner = make_reasoner();
let result = RemoteResult {
peer_id: "ghost".to_string(),
bindings: HashMap::new(),
proof_depth: 0,
latency_ms: 0,
};
let err = reasoner.record_remote_result("no-such-id", result);
assert!(matches!(err, Err(ReasoningError::SessionNotFound(_))));
}
#[test]
fn test_session_timeout_detection() {
let mut reasoner = DistributedReasonerV2::new(DistributedReasonerConfig {
max_depth: 3,
timeout: std::time::Duration::from_nanos(1),
max_peers: 2,
cache_ttl: std::time::Duration::from_secs(60),
parallel_queries: 1,
});
let sid = reasoner.start_session("slow_predicate(X)");
reasoner
.add_session_peer(&sid, "slow-peer")
.expect("add peer");
assert!(reasoner.is_session_complete(&sid));
let session = reasoner.sessions.get(&sid).expect("session present");
assert!(session.is_expired());
}
#[test]
fn test_cleanup_expired_sessions() {
let mut reasoner = DistributedReasonerV2::new(DistributedReasonerConfig {
max_depth: 3,
timeout: std::time::Duration::from_nanos(1), max_peers: 2,
cache_ttl: std::time::Duration::from_secs(60),
parallel_queries: 1,
});
let _s1 = reasoner.start_session("foo(X)");
let _s2 = reasoner.start_session("bar(Y)");
assert_eq!(reasoner.session_count(), 2);
let cleaned = reasoner.cleanup_expired_sessions();
assert_eq!(cleaned, 2);
assert_eq!(reasoner.session_count(), 0);
}
#[test]
fn test_cleanup_keeps_active_sessions() {
let mut reasoner = make_reasoner();
let _sid = reasoner.start_session("live_goal(X)");
assert_eq!(reasoner.session_count(), 1);
let cleaned = reasoner.cleanup_expired_sessions();
assert_eq!(cleaned, 0);
assert_eq!(reasoner.session_count(), 1);
}
#[test]
fn test_session_stats_empty() {
let reasoner = make_reasoner();
let stats = reasoner.session_stats();
assert_eq!(stats.active_sessions, 0);
assert_eq!(stats.total_results, 0);
assert_eq!(stats.avg_latency_ms, 0.0);
assert_eq!(stats.cache_hit_rate, 0.0);
}
#[test]
fn test_session_stats() {
let mut reasoner = make_reasoner();
let sid = reasoner.start_session("grandparent(alice, Z)");
reasoner.add_session_peer(&sid, "p1").expect("add p1");
reasoner.add_session_peer(&sid, "p2").expect("add p2");
reasoner
.record_remote_result(
&sid,
RemoteResult {
peer_id: "p1".to_string(),
bindings: HashMap::new(),
proof_depth: 2,
latency_ms: 100,
},
)
.expect("record p1 result");
reasoner
.record_remote_result(
&sid,
RemoteResult {
peer_id: "p2".to_string(),
bindings: HashMap::new(),
proof_depth: 1,
latency_ms: 200,
},
)
.expect("record p2 result");
let stats = reasoner.session_stats();
assert_eq!(stats.active_sessions, 0);
assert_eq!(stats.total_results, 2);
assert!((stats.avg_latency_ms - 150.0).abs() < f64::EPSILON);
}
#[test]
fn test_session_stats_active_count() {
let mut reasoner = make_reasoner();
let sid = reasoner.start_session("pending_goal(X)");
reasoner
.add_session_peer(&sid, "waiting-peer")
.expect("add peer");
let stats = reasoner.session_stats();
assert_eq!(stats.active_sessions, 1);
}
#[test]
fn test_inference_request_serde() {
let req = InferenceRequest {
request_id: "req-001".to_string(),
goal: "ancestor(alice, X)".to_string(),
max_depth: 7,
requester_peer_id: "12D3KooW...".to_string(),
};
let json = serde_json::to_string(&req).expect("serialize InferenceRequest");
let decoded: InferenceRequest =
serde_json::from_str(&json).expect("deserialize InferenceRequest");
assert_eq!(req.request_id, decoded.request_id);
assert_eq!(req.goal, decoded.goal);
assert_eq!(req.max_depth, decoded.max_depth);
assert_eq!(req.requester_peer_id, decoded.requester_peer_id);
}
#[test]
fn test_inference_response_serde() {
let mut bindings = HashMap::new();
bindings.insert("X".to_string(), "charlie".to_string());
let resp = InferenceResponse {
request_id: "req-001".to_string(),
bindings: vec![bindings],
proof_found: true,
error: None,
};
let json = serde_json::to_string(&resp).expect("serialize InferenceResponse");
let decoded: InferenceResponse =
serde_json::from_str(&json).expect("deserialize InferenceResponse");
assert_eq!(resp.request_id, decoded.request_id);
assert!(decoded.proof_found);
assert!(decoded.error.is_none());
assert_eq!(decoded.bindings.len(), 1);
assert_eq!(
decoded.bindings[0].get("X").map(String::as_str),
Some("charlie")
);
}
#[test]
fn test_inference_response_with_error_serde() {
let resp = InferenceResponse {
request_id: "req-err".to_string(),
bindings: vec![],
proof_found: false,
error: Some("depth limit exceeded".to_string()),
};
let json = serde_json::to_string(&resp).expect("serialize");
let decoded: InferenceResponse = serde_json::from_str(&json).expect("deserialize");
assert_eq!(decoded.error.as_deref(), Some("depth limit exceeded"));
}
#[test]
fn test_remote_result_serde() {
let mut bindings = HashMap::new();
bindings.insert("Y".to_string(), "dave".to_string());
let result = RemoteResult {
peer_id: "QmPeer123".to_string(),
bindings,
proof_depth: 3,
latency_ms: 55,
};
let json = serde_json::to_string(&result).expect("serialize RemoteResult");
let decoded: RemoteResult = serde_json::from_str(&json).expect("deserialize RemoteResult");
assert_eq!(decoded.peer_id, "QmPeer123");
assert_eq!(decoded.proof_depth, 3);
assert_eq!(decoded.latency_ms, 55);
assert_eq!(decoded.bindings.get("Y").map(String::as_str), Some("dave"));
}
#[test]
fn test_add_peer_unknown_session() {
let mut reasoner = make_reasoner();
let err = reasoner.add_session_peer("ghost-session", "peer1");
assert!(matches!(err, Err(ReasoningError::SessionNotFound(_))));
}
#[test]
fn test_add_duplicate_peer() {
let mut reasoner = make_reasoner();
let sid = reasoner.start_session("dup_peer_test(X)");
reasoner
.add_session_peer(&sid, "peer1")
.expect("first add ok");
let err = reasoner.add_session_peer(&sid, "peer1");
assert!(matches!(err, Err(ReasoningError::PeerAlreadyRegistered(_))));
}
#[test]
fn test_mark_unregistered_peer_responded() {
let mut reasoner = make_reasoner();
let sid = reasoner.start_session("unregistered(X)");
let err = reasoner.mark_peer_responded(&sid, "ghost-peer");
assert!(matches!(err, Err(ReasoningError::PeerNotRegistered(_))));
}
#[test]
fn test_cache_hit_rate() {
let mut reasoner = make_reasoner();
let sid = reasoner.start_session("cached_goal(X)");
let result = RemoteResult {
peer_id: "cache-peer".to_string(),
bindings: HashMap::new(),
proof_depth: 1,
latency_ms: 10,
};
reasoner.record_remote_result(&sid, result).expect("record");
let _ = reasoner.get_session_results(&sid);
let sid2 = reasoner.start_session("cached_goal(X)");
let _ = reasoner.get_session_results(&sid2);
let stats = reasoner.session_stats();
assert!((stats.cache_hit_rate - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_session_gc() {
let mut reasoner = DistributedReasonerV2::new(DistributedReasonerConfig {
max_depth: 3,
timeout: std::time::Duration::from_nanos(1),
max_peers: 2,
cache_ttl: std::time::Duration::from_secs(60),
parallel_queries: 1,
});
for i in 0..5 {
reasoner.start_session(&format!("goal_{i}(X)"));
}
assert_eq!(reasoner.session_count(), 5);
let removed = reasoner.gc_sessions(0);
assert_eq!(removed, 5, "all 5 sessions should have been gc'd");
assert_eq!(reasoner.session_count(), 0);
}
#[test]
fn test_session_gc_keeps_recent() {
let mut reasoner = make_reasoner();
for i in 0..5 {
reasoner.start_session(&format!("fresh_{i}(X)"));
}
assert_eq!(reasoner.session_count(), 5);
let removed = reasoner.gc_sessions(3600);
assert_eq!(removed, 0, "no session should have been gc'd");
assert_eq!(reasoner.session_count(), 5);
}
#[test]
fn test_session_metrics() {
let mut reasoner = make_reasoner();
let sid_a = reasoner.start_session("active_goal(X)");
reasoner
.add_session_peer(&sid_a, "peer-a")
.expect("add peer");
let sid_b = reasoner.start_session("done_goal(Y)");
reasoner
.add_session_peer(&sid_b, "peer-b")
.expect("add peer-b");
reasoner
.record_remote_result(
&sid_b,
RemoteResult {
peer_id: "peer-b".to_string(),
bindings: HashMap::new(),
proof_depth: 1,
latency_ms: 80,
},
)
.expect("record peer-b");
let metrics = reasoner.session_metrics();
assert_eq!(metrics.active_sessions, 1, "one session should be active");
assert_eq!(
metrics.completed_sessions, 1,
"one session should be completed"
);
assert!(
(metrics.avg_peers_per_session - 1.0).abs() < f64::EPSILON,
"avg_peers_per_session should be 1.0, got {}",
metrics.avg_peers_per_session
);
assert!(
(metrics.avg_latency_ms - 80.0).abs() < f64::EPSILON,
"avg_latency_ms should be 80.0, got {}",
metrics.avg_latency_ms
);
}
#[tokio::test]
async fn test_inference_result_stream_collects() {
let (tx, rx) = tokio::sync::mpsc::channel::<InferenceResponse>(16);
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10);
let mut stream = InferenceResultStream::new("session-123".to_string(), rx, deadline);
for i in 0u32..3 {
let mut b = HashMap::new();
b.insert("X".to_string(), format!("val{i}"));
tx.send(InferenceResponse {
request_id: format!("peer-{i}"),
bindings: vec![b],
proof_found: true,
error: None,
})
.await
.expect("send");
}
drop(tx);
assert_eq!(stream.session_id(), "session-123");
let mut received_count = 0usize;
while let Some(partial) = stream.next_partial().await {
received_count += 1;
assert_eq!(
partial.new_bindings.len(),
1,
"each peer sends exactly one binding set"
);
assert_eq!(
partial.total_so_far, received_count,
"accumulated count must grow monotonically"
);
}
assert_eq!(
received_count, 3,
"must have received exactly 3 partial results"
);
assert_eq!(stream.result_count(), 3);
}
}