1use std::{
5 collections::HashMap,
6 fs,
7 path::{Path, PathBuf},
8 sync::{OnceLock, RwLock},
9 time::SystemTime,
10};
11
12use tracing::{debug, instrument, trace};
13
14use crate::{
15 object::ContentHash,
16 store::{
17 Result,
18 pack::{ObjectType, PackObjectId, PackReadTier, PackReader},
19 },
20};
21
22pub struct PackManager {
26 packs_dir: PathBuf,
27 packs: Vec<CachedPack>,
28 object_locations: RwLock<ObjectLocationIndex>,
29 eager_object_locations: bool,
30}
31
32#[derive(Default)]
33struct ObjectLocationIndex {
34 locations: HashMap<PackObjectId, ObjectLocation>,
35 complete: bool,
36}
37
38#[derive(Clone, Copy)]
39struct ObjectLocation {
40 pack_index: usize,
41 tier: PackReadTier,
42}
43
44struct CachedPack {
45 pack_path: PathBuf,
46 index_path: PathBuf,
47 reader: OnceLock<Option<PackReader<'static>>>,
48}
49
50impl CachedPack {
51 fn discovered(pack_path: PathBuf, index_path: PathBuf) -> Self {
52 Self {
53 pack_path,
54 index_path,
55 reader: OnceLock::new(),
56 }
57 }
58
59 fn validated(pack_path: PathBuf, index_path: PathBuf, reader: PackReader<'static>) -> Self {
60 Self {
61 pack_path,
62 index_path,
63 reader: OnceLock::from(Some(reader)),
64 }
65 }
66
67 fn reader(&self) -> Option<&PackReader<'static>> {
68 self.reader
69 .get_or_init(
70 || match PackReader::open_lazy(&self.pack_path, &self.index_path) {
71 Ok(reader) => Some(reader),
72 Err(error) => {
73 debug!(pack = ?self.pack_path, %error, "Failed to open pack");
74 None
75 }
76 },
77 )
78 .as_ref()
79 }
80
81 fn verified_reader(&self) -> Option<&PackReader<'static>> {
82 self.reader
83 .get_or_init(
84 || match PackReader::open(&self.pack_path, &self.index_path) {
85 Ok(reader) => Some(reader),
86 Err(error) => {
87 debug!(pack = ?self.pack_path, %error, "Failed to open pack");
88 None
89 }
90 },
91 )
92 .as_ref()
93 }
94}
95
96impl PackManager {
97 pub fn new(packs_dir: PathBuf) -> Self {
98 Self::new_with_index_mode(packs_dir, force_eager_pack_index())
99 }
100
101 fn new_with_index_mode(packs_dir: PathBuf, eager_object_locations: bool) -> Self {
102 let packs = Self::load_packs(&packs_dir).unwrap_or_default();
103 let object_locations = Self::initial_object_locations(&packs, eager_object_locations);
104 Self {
105 packs_dir,
106 packs,
107 object_locations: RwLock::new(object_locations),
108 eager_object_locations,
109 }
110 }
111
112 fn discover_pack_paths(packs_dir: &Path) -> Result<Vec<(PathBuf, PathBuf)>> {
113 let mut packs = Vec::new();
114
115 if !packs_dir.exists() {
116 return Ok(packs);
117 }
118
119 for entry in fs::read_dir(packs_dir)? {
120 let entry = entry?;
121 let path = entry.path();
122
123 if path.extension().map(|e| e == "pack").unwrap_or(false) {
124 let index_path = path.with_extension("idx");
125 if index_path.exists() {
126 packs.push((path, index_path));
127 }
128 }
129 }
130
131 packs.sort_by(|left, right| {
137 pack_modified(&left.0)
138 .cmp(&pack_modified(&right.0))
139 .then_with(|| left.0.cmp(&right.0))
140 });
141
142 debug!(count = packs.len(), "Discovered pack files");
143 Ok(packs)
144 }
145
146 fn load_packs(packs_dir: &Path) -> Result<Vec<CachedPack>> {
147 Ok(Self::discover_pack_paths(packs_dir)?
148 .into_iter()
149 .map(|(pack_path, index_path)| CachedPack::discovered(pack_path, index_path))
150 .collect())
151 }
152
153 pub fn reload(&mut self) -> Result<()> {
154 self.packs = Self::load_packs(&self.packs_dir)?;
155 self.reset_object_locations();
156 Ok(())
157 }
158
159 fn initial_object_locations(packs: &[CachedPack], eager: bool) -> ObjectLocationIndex {
160 if !eager {
161 return ObjectLocationIndex::default();
162 }
163 let mut locations = HashMap::new();
164 for (pack_index, pack) in packs.iter().enumerate() {
165 let Some(reader) = pack.verified_reader() else {
166 continue;
167 };
168 let Ok(objects) = reader.indexed_read_tiers() else {
169 continue;
170 };
171 for (id, tier) in objects {
172 remember_location(&mut locations, id, pack_index, tier);
173 }
174 }
175 ObjectLocationIndex {
176 locations,
177 complete: true,
178 }
179 }
180
181 fn reset_object_locations(&mut self) {
182 self.object_locations = RwLock::new(Self::initial_object_locations(
183 &self.packs,
184 self.eager_object_locations,
185 ));
186 }
187
188 fn object_location(&self, id: &PackObjectId) -> Result<Option<usize>> {
189 {
190 let index = self
191 .object_locations
192 .read()
193 .unwrap_or_else(std::sync::PoisonError::into_inner);
194 if let Some(location) = index.locations.get(id) {
195 return Ok(Some(location.pack_index));
196 }
197 if index.complete {
198 return Ok(None);
199 }
200 }
201
202 let mut index = self
203 .object_locations
204 .write()
205 .unwrap_or_else(std::sync::PoisonError::into_inner);
206 if !index.complete {
207 for (pack_index, pack) in self.packs.iter().enumerate() {
208 let Some(reader) = pack.reader() else {
209 continue;
210 };
211 let Ok(objects) = reader.indexed_read_tiers() else {
212 continue;
213 };
214 for (object_id, tier) in objects {
215 remember_location(&mut index.locations, object_id, pack_index, tier);
216 }
217 }
218 index.complete = true;
219 }
220 Ok(index.locations.get(id).map(|location| location.pack_index))
221 }
222
223 fn point_object_location(&self, id: &PackObjectId) -> Result<Option<usize>> {
227 {
228 let index = self
229 .object_locations
230 .read()
231 .unwrap_or_else(std::sync::PoisonError::into_inner);
232 if let Some(location) = index.locations.get(id) {
233 return Ok(Some(location.pack_index));
234 }
235 if index.complete {
236 return Ok(None);
237 }
238 }
239 for (pack_index, pack) in self.packs.iter().enumerate().rev() {
240 let Some(reader) = pack.reader() else {
241 continue;
242 };
243 if reader.contains_object(id)? {
244 return Ok(Some(pack_index));
245 }
246 }
247 Ok(None)
248 }
249
250 pub fn add_pack(&mut self, pack_path: PathBuf, index_path: PathBuf) -> Result<()> {
252 if self.packs.iter().any(|pack| pack.pack_path == pack_path) {
253 return Ok(());
254 }
255 let reader = PackReader::open(&pack_path, &index_path)?;
256 let objects = reader.indexed_read_tiers()?;
257 let pack_index = self.packs.len();
258 let cached = CachedPack::validated(pack_path, index_path, reader);
259 self.packs.push(cached);
260 let mut index = self
261 .object_locations
262 .write()
263 .unwrap_or_else(std::sync::PoisonError::into_inner);
264 if index.complete {
265 for (id, tier) in objects {
266 remember_location(&mut index.locations, id, pack_index, tier);
267 }
268 }
269 Ok(())
270 }
271
272 pub fn needs_reload(&self) -> Result<bool> {
279 let discovered = Self::discover_pack_paths(&self.packs_dir)?;
280 Ok(discovered.len() != self.packs.len()
281 || discovered
282 .iter()
283 .zip(&self.packs)
284 .any(|((pack, index), cached)| {
285 *pack != cached.pack_path || *index != cached.index_path
286 }))
287 }
288
289 pub fn reload_if_stale(&mut self) -> Result<bool> {
299 if !self.needs_reload()? {
300 return Ok(false);
301 }
302 debug!("PackManager: pack set changed under us, reloading");
303 self.reload()?;
304 Ok(true)
305 }
306
307 pub fn get_object(&self, id: &PackObjectId) -> Result<Option<(ObjectType, Vec<u8>)>> {
308 let Some(pack_index) = self.point_object_location(id)? else {
309 trace!("Object not found in any pack");
310 return Ok(None);
311 };
312 let Some(reader) = self.packs[pack_index].reader() else {
313 return Ok(None);
314 };
315 let object = reader.get_object(id)?;
316 if object.is_some() {
317 trace!("Found object in pack");
318 }
319 Ok(object)
320 }
321
322 pub fn object_read_tier(&self, id: &PackObjectId) -> Result<Option<PackReadTier>> {
327 let _ = self.object_location(id)?;
328 let index = self
329 .object_locations
330 .read()
331 .unwrap_or_else(std::sync::PoisonError::into_inner);
332 Ok(index.locations.get(id).map(|location| location.tier))
333 }
334
335 pub fn get_object_from_pack(
339 &self,
340 pack_path: &Path,
341 id: &PackObjectId,
342 ) -> Result<Option<(ObjectType, Vec<u8>)>> {
343 let Some(pack) = self.packs.iter().find(|pack| pack.pack_path == pack_path) else {
344 return Ok(None);
345 };
346 let Some(reader) = pack.reader() else {
347 return Ok(None);
348 };
349 reader.get_object(id)
350 }
351
352 pub fn list_ids_from_pack(&self, pack_path: &Path) -> Result<Vec<PackObjectId>> {
355 let Some(pack) = self.packs.iter().find(|pack| pack.pack_path == pack_path) else {
356 return Ok(Vec::new());
357 };
358 let Some(reader) = pack.reader() else {
359 return Ok(Vec::new());
360 };
361 reader.list_ids()
362 }
363
364 #[instrument(skip(self), fields(hash = %hash.short()))]
365 pub fn get_hashed_object(&self, hash: &ContentHash) -> Result<Option<(ObjectType, Vec<u8>)>> {
366 self.get_object(&PackObjectId::Hash(*hash))
367 }
368
369 pub fn get_hashed_object_type(&self, hash: &ContentHash) -> Result<Option<ObjectType>> {
371 let id = PackObjectId::Hash(*hash);
372 let Some(pack_index) = self.point_object_location(&id)? else {
373 return Ok(None);
374 };
375 let Some(reader) = self.packs[pack_index].reader() else {
376 return Ok(None);
377 };
378 reader.get_hashed_object_type(hash)
379 }
380
381 pub fn get_hashed_object_bytes(
386 &self,
387 hash: &ContentHash,
388 ) -> Result<Option<(ObjectType, bytes::Bytes)>> {
389 let id = PackObjectId::Hash(*hash);
390 let Some(pack_index) = self.point_object_location(&id)? else {
391 return Ok(None);
392 };
393 let Some(reader) = self.packs[pack_index].reader() else {
394 return Ok(None);
395 };
396 reader.get_object_bytes(&id)
397 }
398
399 pub fn has_object(&self, hash: &ContentHash) -> bool {
400 self.point_object_location(&PackObjectId::Hash(*hash))
401 .is_ok_and(|location| location.is_some())
402 }
403
404 pub fn get_hashed_object_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
408 let id = PackObjectId::Hash(*hash);
409 let Some(pack_index) = self.point_object_location(&id)? else {
410 return Ok(None);
411 };
412 let Some(reader) = self.packs[pack_index].reader() else {
413 return Ok(None);
414 };
415 reader.get_hashed_object_size(hash)
416 }
417
418 pub fn has_object_id(&self, id: &PackObjectId) -> bool {
419 self.object_location(id)
420 .is_ok_and(|location| location.is_some())
421 }
422
423 pub fn list_all_hashes(&self) -> Result<Vec<ContentHash>> {
425 let mut hashes = Vec::new();
426 for pack in &self.packs {
427 if let Some(reader) = pack.reader() {
428 hashes.extend(reader.list_hashes()?);
429 }
430 }
431 Ok(hashes)
432 }
433
434 pub fn list_all_ids(&self) -> Result<Vec<PackObjectId>> {
435 let mut ids = Vec::new();
436 for pack in &self.packs {
437 if let Some(reader) = pack.reader() {
438 ids.extend(reader.list_ids()?);
439 }
440 }
441 Ok(ids)
442 }
443
444 pub fn pack_file_paths(&self) -> Vec<(&Path, &Path)> {
446 self.packs
447 .iter()
448 .map(|pack| (pack.pack_path.as_path(), pack.index_path.as_path()))
449 .collect()
450 }
451
452 pub fn pack_count(&self) -> usize {
453 self.packs.len()
454 }
455
456 pub fn packs_dir(&self) -> &Path {
457 &self.packs_dir
458 }
459}
460
461fn pack_modified(path: &Path) -> SystemTime {
462 fs::metadata(path)
463 .and_then(|metadata| metadata.modified())
464 .unwrap_or(SystemTime::UNIX_EPOCH)
465}
466
467fn remember_location(
468 locations: &mut HashMap<PackObjectId, ObjectLocation>,
469 id: PackObjectId,
470 pack_index: usize,
471 tier: PackReadTier,
472) {
473 let candidate = ObjectLocation { pack_index, tier };
474 match locations.get_mut(&id) {
475 Some(existing)
476 if existing.tier == PackReadTier::SolidFrame && tier == PackReadTier::Hot =>
477 {
478 *existing = candidate;
479 }
480 Some(_) => {}
481 None => {
482 locations.insert(id, candidate);
483 }
484 }
485}
486
487fn force_eager_pack_index() -> bool {
488 std::env::var("HEDDLE_PERF_FORCE_EAGER_PACK_INDEX")
489 .is_ok_and(|value| matches!(value.as_str(), "1" | "true" | "yes"))
490}
491
492#[cfg(test)]
493#[path = "manager_tests.rs"]
494mod tests;