tatara_engine/p2p/
transfer.rs1use 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
9pub 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 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 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 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 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 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 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 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 for hash in &manifest.chunks {
129 self.gossip.advertise_chunk(hash).await;
130 }
131
132 self.cache
134 .retrieve_data(manifest)
135 .await?
136 .context("Failed to reassemble after fetching all chunks")
137 }
138
139 pub async fn serve_chunk(&self, hash: &str) -> Result<Option<Chunk>> {
141 self.cache.get_chunk(hash).await
142 }
143
144 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 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}