use crate::bitswap::{BitswapConfig, BitswapExchange};
use crate::peer_manager::PeerId;
use crate::schema_registry::{SchemaError, SchemaEvolutionFrame, SchemaRegistry, SchemaVersion};
use crate::want_list::Priority;
use ipfrs_core::{Block, Cid, Result};
use ipfrs_storage::traits::BlockStore;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Instant;
use tokio::sync::mpsc;
use super::streaming::{
BackpressureConfig, BackpressureController, StreamProgress, TensorMetadata, TensorStream,
};
#[derive(Debug, Clone)]
pub struct TensorSwapConfig {
pub bitswap: BitswapConfig,
pub progressive_streaming: bool,
pub chunk_size: usize,
pub deadline_aware: bool,
pub backpressure: BackpressureConfig,
pub max_concurrent_streams: usize,
pub critical_priority_boost: i32,
pub dependency_priority_boost: i32,
}
impl Default for TensorSwapConfig {
fn default() -> Self {
Self {
bitswap: BitswapConfig::default(),
progressive_streaming: true,
chunk_size: 1024 * 1024, deadline_aware: true,
backpressure: BackpressureConfig::default(),
max_concurrent_streams: 16,
critical_priority_boost: 50,
dependency_priority_boost: 10,
}
}
}
pub struct TensorSwap<S: BlockStore> {
bitswap: Arc<BitswapExchange<S>>,
tensor_metadata: Arc<RwLock<HashMap<Cid, TensorMetadata>>>,
active_streams: Arc<RwLock<HashMap<Cid, TensorStream>>>,
backpressure: Arc<RwLock<BackpressureController>>,
config: TensorSwapConfig,
}
impl<S: BlockStore> TensorSwap<S> {
pub fn new(store: Arc<S>, config: TensorSwapConfig) -> Result<Self> {
let bitswap = Arc::new(BitswapExchange::new(store, config.bitswap.clone())?);
let backpressure = BackpressureController::new(config.backpressure.clone());
Ok(Self {
bitswap,
tensor_metadata: Arc::new(RwLock::new(HashMap::new())),
active_streams: Arc::new(RwLock::new(HashMap::new())),
backpressure: Arc::new(RwLock::new(backpressure)),
config,
})
}
pub fn with_defaults(store: Arc<S>) -> Result<Self> {
Self::new(store, TensorSwapConfig::default())
}
pub fn register_tensor(&self, metadata: TensorMetadata) {
if let Ok(mut map) = self.tensor_metadata.write() {
map.insert(metadata.cid, metadata);
}
}
pub fn want_tensor(&self, cid: Cid) -> Result<()> {
let metadata = self
.tensor_metadata
.read()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?;
let priority = if let Some(meta) = metadata.get(&cid) {
self.calculate_priority(meta)
} else {
0 };
self.bitswap.want(cid, priority)?;
if let Some(meta) = metadata.get(&cid) {
for dep_cid in &meta.dependencies {
let dep_priority = priority + self.config.dependency_priority_boost;
self.bitswap.want(*dep_cid, dep_priority)?;
}
}
Ok(())
}
pub fn start_stream(&self, cid: Cid) -> Result<()> {
{
let bp = self
.backpressure
.read()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?;
if !bp.should_accept() {
return Err(ipfrs_core::Error::Internal(
"Backpressure limit reached".to_string(),
));
}
}
{
let active_count = self
.active_streams
.read()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?
.len();
if active_count >= self.config.max_concurrent_streams {
return Err(ipfrs_core::Error::Internal(
"Maximum concurrent streams reached".to_string(),
));
}
}
let metadata = {
let meta_map = self
.tensor_metadata
.read()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?;
meta_map.get(&cid).cloned()
};
let metadata = metadata.unwrap_or_else(|| TensorMetadata::new(cid));
let stream = TensorStream::new(metadata.clone());
let base_priority = self.calculate_priority(&metadata);
for (idx, chunk_info) in stream.chunks.iter().enumerate() {
let chunk_priority = base_priority + (stream.chunks.len() - idx) as i32;
self.bitswap.want(chunk_info.cid, chunk_priority)?;
}
self.active_streams
.write()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?
.insert(cid, stream);
Ok(())
}
pub fn start_stream_with_progress(
&self,
cid: Cid,
progress_tx: mpsc::Sender<StreamProgress>,
) -> Result<()> {
{
let bp = self
.backpressure
.read()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?;
if !bp.should_accept() {
return Err(ipfrs_core::Error::Internal(
"Backpressure limit reached".to_string(),
));
}
}
{
let active_count = self
.active_streams
.read()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?
.len();
if active_count >= self.config.max_concurrent_streams {
return Err(ipfrs_core::Error::Internal(
"Maximum concurrent streams reached".to_string(),
));
}
}
let metadata = {
let meta_map = self
.tensor_metadata
.read()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?;
meta_map.get(&cid).cloned()
};
let metadata = metadata.unwrap_or_else(|| TensorMetadata::new(cid));
let stream = TensorStream::new(metadata.clone()).with_progress_channel(progress_tx);
let base_priority = self.calculate_priority(&metadata);
for (idx, chunk_info) in stream.chunks.iter().enumerate() {
let chunk_priority = base_priority + (stream.chunks.len() - idx) as i32;
self.bitswap.want(chunk_info.cid, chunk_priority)?;
}
self.active_streams
.write()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?
.insert(cid, stream);
Ok(())
}
pub fn stream_progress(&self, cid: &Cid) -> Option<f64> {
self.active_streams
.read()
.ok()?
.get(cid)
.map(|s| s.progress())
}
pub fn is_stream_complete(&self, cid: &Cid) -> bool {
self.active_streams
.read()
.ok()
.and_then(|s| s.get(cid).map(|s| s.is_complete()))
.unwrap_or(false)
}
pub fn cancel_stream(&self, cid: &Cid) -> Result<()> {
let stream = self
.active_streams
.write()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?
.remove(cid);
if let Some(stream) = stream {
for chunk in stream.missing_chunks() {
self.bitswap.cancel_want(&chunk)?;
}
}
Ok(())
}
fn calculate_priority(&self, meta: &TensorMetadata) -> i32 {
let mut priority = meta.priority_hint.unwrap_or(0);
if meta.is_critical {
priority += self.config.critical_priority_boost;
}
if self.config.deadline_aware {
if let Some(deadline) = meta.deadline {
let now = Instant::now();
if deadline > now {
let time_left = deadline.duration_since(now).as_secs();
priority += (100 - time_left.min(100)) as i32;
} else {
priority += Priority::Critical as i32;
}
}
}
priority += meta.dependencies.len() as i32 * self.config.dependency_priority_boost;
priority
}
#[allow(clippy::await_holding_lock)]
pub async fn receive_tensor(&self, peer_id: &PeerId, block: Block) -> Result<()> {
let cid = *block.cid();
let size = block.size();
{
let mut streams = self
.active_streams
.write()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?;
for stream in streams.values_mut() {
stream.mark_received(&cid, size).await;
}
}
if let Ok(mut bp) = self.backpressure.write() {
bp.on_ack(size as usize);
}
self.bitswap.receive_block(peer_id, block).await
}
pub async fn send_tensor(
&self,
peer_id: &PeerId,
cid: &Cid,
) -> Result<Option<crate::messages::Message>> {
let should_send = self
.backpressure
.read()
.map_err(|_| ipfrs_core::Error::Internal("lock poisoned".to_string()))?
.should_accept();
if !should_send {
return Err(ipfrs_core::Error::Internal(
"Backpressure active".to_string(),
));
}
let result = self.bitswap.send_block(peer_id, cid).await?;
if let Some(crate::messages::Message::Block(block_msg)) = &result {
if let Ok(mut bp) = self.backpressure.write() {
bp.on_send(block_msg.data.len());
}
}
Ok(result)
}
pub fn cancel_tensor(&self, cid: &Cid) -> Result<()> {
self.cancel_stream(cid)?;
self.bitswap.cancel_want(cid)
}
pub fn next_tensor(&self) -> Option<Cid> {
self.bitswap.next_want()
}
pub fn cleanup_completed_streams(&self) {
if let Ok(mut streams) = self.active_streams.write() {
streams.retain(|_, stream| !stream.is_complete());
}
}
pub fn stats(&self) -> TensorSwapStats {
let bitswap_stats = self.bitswap.stats();
let streams = self.active_streams.read();
let backpressure = self.backpressure.read();
let (active_streams, backpressure_paused, pending_chunks) =
match (streams.as_ref(), backpressure.as_ref()) {
(Ok(s), Ok(bp)) => (s.len(), bp.is_paused(), bp.pending_count()),
_ => (0, false, 0),
};
TensorSwapStats {
want_list_size: bitswap_stats.want_list_size,
num_tensors_registered: self.tensor_metadata.read().map(|m| m.len()).unwrap_or(0),
active_streams,
total_bytes_sent: bitswap_stats.total_bytes_sent,
total_bytes_recv: bitswap_stats.total_bytes_recv,
backpressure_paused,
pending_chunks,
}
}
pub fn bitswap(&self) -> &Arc<BitswapExchange<S>> {
&self.bitswap
}
pub fn is_backpressure_active(&self) -> bool {
self.backpressure
.read()
.map(|bp| bp.is_paused())
.unwrap_or(false)
}
pub fn ack_data(&self, bytes: usize) {
if let Ok(mut bp) = self.backpressure.write() {
bp.on_ack(bytes);
}
}
pub async fn negotiate_schema(
&mut self,
proposed: &SchemaVersion,
registry: &SchemaRegistry,
) -> std::result::Result<SchemaVersion, SchemaError> {
registry
.get(proposed)
.ok_or_else(|| SchemaError::NotFound(proposed.name.clone()))?;
let latest = registry
.latest_version(&proposed.name)
.ok_or_else(|| SchemaError::NotFound(proposed.name.clone()))?;
if registry.can_read_with(proposed, &latest) {
Ok(proposed.clone())
} else {
Ok(latest)
}
}
pub async fn evolve_schema(
&mut self,
session_id: &str,
old_version: &SchemaVersion,
new_version: &SchemaVersion,
registry: &SchemaRegistry,
) -> std::result::Result<Vec<u8>, SchemaError> {
let _old_schema = registry
.get(old_version)
.ok_or_else(|| SchemaError::NotFound(old_version.name.clone()))?;
let new_schema = registry
.get(new_version)
.ok_or_else(|| SchemaError::NotFound(new_version.name.clone()))?;
let frame = SchemaEvolutionFrame::new(
session_id,
old_version.clone(),
new_version.clone(),
&new_schema,
);
frame.to_bytes()
}
}
#[derive(Debug, Clone)]
pub struct TensorSwapStats {
pub want_list_size: usize,
pub num_tensors_registered: usize,
pub active_streams: usize,
pub total_bytes_sent: u64,
pub total_bytes_recv: u64,
pub backpressure_paused: bool,
pub pending_chunks: usize,
}
impl Default for TensorSwap<ipfrs_storage::SledBlockStore> {
fn default() -> Self {
let config = ipfrs_storage::BlockStoreConfig::default();
let store = Arc::new(
ipfrs_storage::SledBlockStore::new(config)
.expect("SledBlockStore::new with default config"),
);
Self::with_defaults(store).expect("TensorSwap::with_defaults")
}
}