Skip to main content

tatara_engine/p2p/
cache.rs

1use anyhow::Result;
2use std::collections::{HashMap, HashSet};
3use std::path::{Path, PathBuf};
4use tokio::sync::RwLock;
5use tracing::{debug, info, warn};
6
7use super::chunk::{Chunk, ChunkManifest};
8
9/// Local content-addressed cache. Stores chunks on disk, indexed by BLAKE3 hash.
10///
11/// Layout:
12///   <cache_dir>/
13///     manifests/<root_hash>.json     — chunk manifests
14///     chunks/<first2>/<hash>         — raw chunk data (sharded by first 2 hex chars)
15pub struct DataCache {
16    dir: PathBuf,
17    /// In-memory index: hash → true if we have the chunk locally.
18    index: RwLock<HashSet<String>>,
19    /// Manifest index: root_hash → manifest.
20    manifests: RwLock<HashMap<String, ChunkManifest>>,
21}
22
23impl DataCache {
24    pub async fn new(dir: &Path) -> Result<Self> {
25        tokio::fs::create_dir_all(dir.join("manifests")).await?;
26        tokio::fs::create_dir_all(dir.join("chunks")).await?;
27
28        let cache = Self {
29            dir: dir.to_path_buf(),
30            index: RwLock::new(HashSet::new()),
31            manifests: RwLock::new(HashMap::new()),
32        };
33
34        cache.rebuild_index().await?;
35        Ok(cache)
36    }
37
38    /// Store a chunk locally.
39    pub async fn put_chunk(&self, chunk: &Chunk) -> Result<()> {
40        if !chunk.verify() {
41            anyhow::bail!("Chunk verification failed: {}", chunk.hash);
42        }
43
44        let path = self.chunk_path(&chunk.hash);
45        if let Some(parent) = path.parent() {
46            tokio::fs::create_dir_all(parent).await?;
47        }
48
49        tokio::fs::write(&path, &chunk.data).await?;
50        self.index.write().await.insert(chunk.hash.clone());
51
52        debug!(hash = %chunk.hash, size = chunk.size, "Stored chunk");
53        Ok(())
54    }
55
56    /// Retrieve a chunk from local cache.
57    pub async fn get_chunk(&self, hash: &str) -> Result<Option<Chunk>> {
58        if !self.has_chunk(hash).await {
59            return Ok(None);
60        }
61
62        let path = self.chunk_path(hash);
63        let data = tokio::fs::read(&path).await?;
64
65        let chunk = Chunk {
66            hash: hash.to_string(),
67            data,
68            size: 0, // Will be filled
69        };
70
71        // Verify
72        if !chunk.verify() {
73            warn!(hash = hash, "Cached chunk failed verification — removing");
74            let _ = tokio::fs::remove_file(&path).await;
75            self.index.write().await.remove(hash);
76            return Ok(None);
77        }
78
79        Ok(Some(Chunk {
80            size: chunk.data.len(),
81            ..chunk
82        }))
83    }
84
85    /// Check if we have a chunk.
86    pub async fn has_chunk(&self, hash: &str) -> bool {
87        self.index.read().await.contains(hash)
88    }
89
90    /// Store a manifest.
91    pub async fn put_manifest(&self, manifest: &ChunkManifest) -> Result<()> {
92        let path = self
93            .dir
94            .join("manifests")
95            .join(format!("{}.json", manifest.root_hash));
96        let data = serde_json::to_string_pretty(manifest)?;
97        tokio::fs::write(&path, data).await?;
98        self.manifests
99            .write()
100            .await
101            .insert(manifest.root_hash.clone(), manifest.clone());
102
103        debug!(
104            root_hash = %manifest.root_hash,
105            chunks = manifest.chunks.len(),
106            content_type = %manifest.content_type,
107            "Stored manifest"
108        );
109        Ok(())
110    }
111
112    /// Get a manifest.
113    pub async fn get_manifest(&self, root_hash: &str) -> Option<ChunkManifest> {
114        self.manifests.read().await.get(root_hash).cloned()
115    }
116
117    /// List all chunk hashes we have.
118    pub async fn local_chunks(&self) -> Vec<String> {
119        self.index.read().await.iter().cloned().collect()
120    }
121
122    /// List all manifests we have.
123    pub async fn local_manifests(&self) -> Vec<ChunkManifest> {
124        self.manifests.read().await.values().cloned().collect()
125    }
126
127    /// Check how many chunks of a manifest we have locally.
128    pub async fn manifest_completeness(&self, manifest: &ChunkManifest) -> (usize, usize) {
129        let index = self.index.read().await;
130        let have = manifest
131            .chunks
132            .iter()
133            .filter(|h| index.contains(*h))
134            .count();
135        (have, manifest.chunks.len())
136    }
137
138    /// Store complete data: split into chunks, store all, store manifest.
139    pub async fn store_data(
140        &self,
141        data: &[u8],
142        content_type: &str,
143        label: &str,
144    ) -> Result<ChunkManifest> {
145        let (manifest, chunks) = ChunkManifest::from_data(data, content_type, label);
146
147        for chunk in &chunks {
148            self.put_chunk(chunk).await?;
149        }
150        self.put_manifest(&manifest).await?;
151
152        info!(
153            root_hash = %manifest.root_hash,
154            chunks = chunks.len(),
155            total_size = manifest.total_size,
156            "Data stored in p2p cache"
157        );
158
159        Ok(manifest)
160    }
161
162    /// Retrieve complete data from cache, given a manifest.
163    pub async fn retrieve_data(&self, manifest: &ChunkManifest) -> Result<Option<Vec<u8>>> {
164        let mut chunks = Vec::with_capacity(manifest.chunks.len());
165
166        for hash in &manifest.chunks {
167            match self.get_chunk(hash).await? {
168                Some(chunk) => chunks.push(chunk),
169                None => return Ok(None), // Missing chunk
170            }
171        }
172
173        manifest
174            .reassemble(&chunks)
175            .map(Some)
176            .map_err(|e| anyhow::anyhow!("Reassembly failed: {}", e))
177    }
178
179    /// Cache size in bytes (approximate).
180    pub async fn size_bytes(&self) -> u64 {
181        let mut total = 0u64;
182        let chunks_dir = self.dir.join("chunks");
183
184        if let Ok(mut entries) = tokio::fs::read_dir(&chunks_dir).await {
185            while let Ok(Some(shard)) = entries.next_entry().await {
186                if let Ok(mut shard_entries) = tokio::fs::read_dir(shard.path()).await {
187                    while let Ok(Some(entry)) = shard_entries.next_entry().await {
188                        if let Ok(meta) = entry.metadata().await {
189                            total += meta.len();
190                        }
191                    }
192                }
193            }
194        }
195
196        total
197    }
198
199    fn chunk_path(&self, hash: &str) -> PathBuf {
200        let shard = &hash[..2.min(hash.len())];
201        self.dir.join("chunks").join(shard).join(hash)
202    }
203
204    async fn rebuild_index(&self) -> Result<()> {
205        let mut index = self.index.write().await;
206        let mut manifests = self.manifests.write().await;
207
208        // Scan chunk directory
209        let chunks_dir = self.dir.join("chunks");
210        if let Ok(mut entries) = tokio::fs::read_dir(&chunks_dir).await {
211            while let Ok(Some(shard)) = entries.next_entry().await {
212                if let Ok(mut shard_entries) = tokio::fs::read_dir(shard.path()).await {
213                    while let Ok(Some(entry)) = shard_entries.next_entry().await {
214                        if let Some(hash) = entry.file_name().to_str() {
215                            index.insert(hash.to_string());
216                        }
217                    }
218                }
219            }
220        }
221
222        // Scan manifest directory
223        let manifests_dir = self.dir.join("manifests");
224        if let Ok(mut entries) = tokio::fs::read_dir(&manifests_dir).await {
225            while let Ok(Some(entry)) = entries.next_entry().await {
226                if let Some(name) = entry.file_name().to_str() {
227                    if name.ends_with(".json") {
228                        if let Ok(data) = tokio::fs::read_to_string(entry.path()).await {
229                            if let Ok(manifest) = serde_json::from_str::<ChunkManifest>(&data) {
230                                manifests.insert(manifest.root_hash.clone(), manifest);
231                            }
232                        }
233                    }
234                }
235            }
236        }
237
238        info!(
239            chunks = index.len(),
240            manifests = manifests.len(),
241            "P2P cache index rebuilt"
242        );
243
244        Ok(())
245    }
246}