Skip to main content

tatara_engine/p2p/
transfer.rs

1use anyhow::{Context, Result};
2use std::sync::Arc;
3use tracing::{debug, info, warn};
4
5use super::cache::DataCache;
6use super::chunk::{Chunk, ChunkManifest};
7use crate::cluster::gossip::GossipCluster;
8
9/// Handles peer-to-peer chunk transfer between nodes.
10///
11/// Protocol:
12/// 1. Node stores data locally → advertises chunk hashes via gossip
13/// 2. Node needing data gets manifest → queries gossip for chunk holders
14/// 3. Fetches missing chunks from any holder (parallel, random order)
15/// 4. Verifies and stores each chunk locally
16/// 5. Once complete, advertises its own possession via gossip
17///
18/// This is BitTorrent-style: content-addressed, swarming, no central tracker.
19pub struct TransferEngine {
20    cache: Arc<DataCache>,
21    gossip: Arc<GossipCluster>,
22    http_client: reqwest::Client,
23}
24
25impl TransferEngine {
26    pub fn new(cache: Arc<DataCache>, gossip: Arc<GossipCluster>) -> Self {
27        Self {
28            cache,
29            gossip,
30            http_client: reqwest::Client::new(),
31        }
32    }
33
34    /// Publish data to the swarm: store locally and advertise via gossip.
35    pub async fn publish(
36        &self,
37        data: &[u8],
38        content_type: &str,
39        label: &str,
40    ) -> Result<ChunkManifest> {
41        let manifest = self.cache.store_data(data, content_type, label).await?;
42
43        // Advertise each chunk via gossip
44        for hash in &manifest.chunks {
45            self.gossip.advertise_chunk(hash).await;
46        }
47
48        info!(
49            root_hash = %manifest.root_hash,
50            chunks = manifest.chunks.len(),
51            "Published data to swarm"
52        );
53
54        Ok(manifest)
55    }
56
57    /// Fetch complete data for a manifest. Downloads missing chunks from peers.
58    pub async fn fetch(&self, manifest: &ChunkManifest) -> Result<Vec<u8>> {
59        let (have, total) = self.cache.manifest_completeness(manifest).await;
60
61        if have == total {
62            // Already have everything
63            return self
64                .cache
65                .retrieve_data(manifest)
66                .await?
67                .context("Failed to reassemble despite having all chunks");
68        }
69
70        info!(
71            root_hash = %manifest.root_hash,
72            have = have,
73            total = total,
74            "Fetching missing chunks from peers"
75        );
76
77        // Find and fetch missing chunks
78        let mut missing: Vec<String> = Vec::new();
79        for hash in &manifest.chunks {
80            if !self.cache.has_chunk(hash).await {
81                missing.push(hash.clone());
82            }
83        }
84
85        // Fetch missing chunks in parallel (up to 8 concurrent)
86        let semaphore = Arc::new(tokio::sync::Semaphore::new(8));
87        let mut handles = Vec::new();
88
89        for hash in missing {
90            let sem = semaphore.clone();
91            let gossip = self.gossip.clone();
92            let cache = self.cache.clone();
93            let client = self.http_client.clone();
94
95            let handle = tokio::spawn(async move {
96                let _permit = sem.acquire().await.unwrap();
97                fetch_chunk_from_peers(&client, &gossip, &cache, &hash).await
98            });
99
100            handles.push(handle);
101        }
102
103        // Wait for all fetches
104        let mut failures = 0;
105        for handle in handles {
106            match handle.await {
107                Ok(Ok(())) => {}
108                Ok(Err(e)) => {
109                    warn!(error = %e, "Chunk fetch failed");
110                    failures += 1;
111                }
112                Err(e) => {
113                    warn!(error = %e, "Chunk fetch task panicked");
114                    failures += 1;
115                }
116            }
117        }
118
119        if failures > 0 {
120            anyhow::bail!(
121                "Failed to fetch {} chunks for manifest {}",
122                failures,
123                manifest.root_hash
124            );
125        }
126
127        // Advertise newly acquired chunks
128        for hash in &manifest.chunks {
129            self.gossip.advertise_chunk(hash).await;
130        }
131
132        // Reassemble
133        self.cache
134            .retrieve_data(manifest)
135            .await?
136            .context("Failed to reassemble after fetching all chunks")
137    }
138
139    /// Serve a chunk to a requesting peer (called by HTTP handler).
140    pub async fn serve_chunk(&self, hash: &str) -> Result<Option<Chunk>> {
141        self.cache.get_chunk(hash).await
142    }
143
144    /// Serve a manifest to a requesting peer.
145    pub async fn serve_manifest(&self, root_hash: &str) -> Option<ChunkManifest> {
146        self.cache.get_manifest(root_hash).await
147    }
148}
149
150async fn fetch_chunk_from_peers(
151    client: &reqwest::Client,
152    gossip: &GossipCluster,
153    cache: &DataCache,
154    hash: &str,
155) -> Result<()> {
156    let holders = gossip.find_chunk_holders(hash);
157
158    if holders.is_empty() {
159        anyhow::bail!("No holders found for chunk {}", hash);
160    }
161
162    // Try each holder (random order would be better, but sequential is fine for now)
163    for holder_addr in &holders {
164        let url = format!("http://{}/p2p/chunks/{}", holder_addr, hash);
165
166        match client.get(&url).send().await {
167            Ok(resp) if resp.status().is_success() => {
168                let data = resp.bytes().await?;
169                let chunk = Chunk {
170                    hash: hash.to_string(),
171                    data: data.to_vec(),
172                    size: data.len(),
173                };
174
175                if chunk.verify() {
176                    cache.put_chunk(&chunk).await?;
177                    debug!(hash = hash, from = %holder_addr, "Fetched chunk from peer");
178                    return Ok(());
179                } else {
180                    warn!(
181                        hash = hash,
182                        from = %holder_addr,
183                        "Chunk from peer failed verification"
184                    );
185                }
186            }
187            Ok(resp) => {
188                debug!(
189                    hash = hash,
190                    from = %holder_addr,
191                    status = %resp.status(),
192                    "Peer returned non-success for chunk"
193                );
194            }
195            Err(e) => {
196                debug!(
197                    hash = hash,
198                    from = %holder_addr,
199                    error = %e,
200                    "Failed to contact peer for chunk"
201                );
202            }
203        }
204    }
205
206    anyhow::bail!(
207        "Failed to fetch chunk {} from any of {} holders",
208        hash,
209        holders.len()
210    )
211}