1mod adaptive;
2mod catalog;
3mod gate;
4mod maintenance;
5mod member;
6mod model;
7mod temperature;
8mod temperature_balancer;
9mod temperature_catalog;
10mod temperature_worker;
11#[cfg(test)]
12mod tests;
13
14use self::adaptive::AdaptivePoolState;
15use self::gate::ConcurrencyGate;
16use self::member::{open_member_store, prepare_member_paths, validate_member_config};
17use self::model::{LocationRecord, MemberRecord, PoolManifest, MIN_MEMBER_MAP_SIZE_BYTES};
18pub use self::model::{
19 PoolMaintenanceReport, PoolMemberConfig, PoolMemberId, PoolMemberState, PoolMemberStatus,
20 PoolStoreConfig, PoolTemperatureConfig, PoolTemperatureReport,
21};
22use self::temperature::TemperatureRuntime;
23use self::temperature_worker::TemperatureWorker;
24use crate::{managed_env::ManagedEnv, LmdbBlobStore};
25use async_trait::async_trait;
26use hashtree_core::store::{slice_blob_range, PutManyReport, Store, StoreError, StoreStats};
27use hashtree_core::{sha256, types::Hash};
28use heed::types::{Bytes, Unit};
29use heed::{Database, EnvOpenOptions};
30use std::collections::{HashMap, HashSet};
31use std::fs;
32use std::ops::Deref;
33use std::path::Path;
34use std::sync::atomic::{AtomicU64, Ordering};
35use std::sync::{Arc, Mutex, RwLock};
36use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
37
38const CATALOG_DATABASES: u32 = 6;
39const CATALOG_MAX_READERS: u32 = 1024;
40const MANIFEST_KEY: &[u8] = b"pool-manifest-v1";
41const MEMBER_MARKER_NAME: &str = ".hashtree-pool-member-v1";
42const EXTERNAL_MARKER_NAME: &str = ".hashtree-pool-external-v1";
43
44#[derive(Default)]
45struct RuntimeMembers {
46 generation: Option<u64>,
47 stores: HashMap<PoolMemberId, Arc<LmdbBlobStore>>,
48 read_gates: HashMap<PoolMemberId, Arc<ConcurrencyGate>>,
49 write_gates: HashMap<PoolMemberId, Arc<ConcurrencyGate>>,
50 errors: HashMap<PoolMemberId, String>,
51}
52
53#[derive(Clone)]
54pub struct PoolStore {
55 inner: Arc<PoolStoreInner>,
56}
57
58#[doc(hidden)]
59pub struct PoolStoreInner {
60 env: ManagedEnv,
61 manifest_db: Database<Bytes, Bytes>,
62 locations: Database<Bytes, Bytes>,
63 by_member: Database<Bytes, Unit>,
64 pins: Database<Bytes, Bytes>,
65 last_accessed: Database<Bytes, Bytes>,
66 temperature_state: Database<Bytes, Bytes>,
67 runtime: RwLock<RuntimeMembers>,
68 adaptive: Mutex<AdaptivePoolState>,
69 temperature_config: PoolTemperatureConfig,
70 temperature: Mutex<TemperatureRuntime>,
71 temperature_access_counter: AtomicU64,
72 temperature_owner: PoolMemberId,
73 temperature_cycle: Mutex<()>,
74 temperature_worker: TemperatureWorker,
75}
76
77impl Deref for PoolStore {
78 type Target = PoolStoreInner;
79
80 fn deref(&self) -> &Self::Target {
81 &self.inner
82 }
83}
84
85fn validate_new_member_paths(
86 manifest: &PoolManifest,
87 config: &PoolMemberConfig,
88) -> Result<(), StoreError> {
89 if manifest
90 .members
91 .iter()
92 .any(|member| member.config.path == config.path)
93 {
94 return Err(StoreError::Other(format!(
95 "pool member path is already configured: {}",
96 config.path.display()
97 )));
98 }
99 if let Some(external) = config.external_blob_dir.as_ref() {
100 if manifest
101 .members
102 .iter()
103 .any(|member| member.config.external_blob_dir.as_ref() == Some(external))
104 {
105 return Err(StoreError::Other(format!(
106 "pool external blob path is already configured: {}",
107 external.display()
108 )));
109 }
110 }
111 Ok(())
112}
113
114impl PoolStore {
115 pub fn open<P: AsRef<Path>>(path: P, config: PoolStoreConfig) -> Result<Self, StoreError> {
116 config.temperature.validate()?;
117 let path = path.as_ref();
118 fs::create_dir_all(path).map_err(StoreError::Io)?;
119 let existing_size = fs::metadata(path.join("data.mdb"))
120 .map(|metadata| metadata.len())
121 .unwrap_or(0);
122 let requested = config
123 .catalog_map_size_bytes
124 .max(existing_size.saturating_add(existing_size / 10))
125 .max(MIN_MEMBER_MAP_SIZE_BYTES);
126 let map_size = usize::try_from(requested)
127 .map_err(|_| StoreError::Other("pool catalog map size exceeds usize".into()))?;
128
129 let mut options = EnvOpenOptions::new();
130 options
131 .map_size(map_size)
132 .max_dbs(CATALOG_DATABASES)
133 .max_readers(CATALOG_MAX_READERS);
134 unsafe {
135 options.flags(super::env_flags_from_env());
136 }
137 let env = unsafe { ManagedEnv::open(&options, path) }.map_err(|error| {
138 StoreError::Other(format!("open pool catalog {}: {error}", path.display()))
139 })?;
140 let _ = env.clear_stale_readers();
141 if env.info().map_size < map_size {
142 unsafe { env.resize(map_size) }.map_err(map_heed)?;
143 }
144
145 let mut wtxn = env.write_txn().map_err(map_heed)?;
146 let manifest_db = env
147 .create_database(&mut wtxn, Some("manifest"))
148 .map_err(map_heed)?;
149 let locations = env
150 .create_database(&mut wtxn, Some("locations"))
151 .map_err(map_heed)?;
152 let by_member = env
153 .create_database(&mut wtxn, Some("by_member"))
154 .map_err(map_heed)?;
155 let pins = env
156 .create_database(&mut wtxn, Some("pins"))
157 .map_err(map_heed)?;
158 let last_accessed = env
159 .create_database(&mut wtxn, Some("last_accessed"))
160 .map_err(map_heed)?;
161 let temperature_state = env
162 .create_database(&mut wtxn, Some("temperature_state"))
163 .map_err(map_heed)?;
164 if manifest_db
165 .get(&wtxn, MANIFEST_KEY)
166 .map_err(map_heed)?
167 .is_none()
168 {
169 let bytes = encode_manifest(&PoolManifest::default())?;
170 manifest_db
171 .put(&mut wtxn, MANIFEST_KEY, bytes.as_slice())
172 .map_err(map_heed)?;
173 }
174 wtxn.commit().map_err(map_heed)?;
175
176 let temperature_config = config.temperature.clone();
177 let store = Self {
178 inner: Arc::new(PoolStoreInner {
179 env,
180 manifest_db,
181 locations,
182 by_member,
183 pins,
184 last_accessed,
185 temperature_state,
186 runtime: RwLock::new(RuntimeMembers::default()),
187 adaptive: Mutex::new(AdaptivePoolState::new(config.member_failure_cooldown)),
188 temperature: Mutex::new(TemperatureRuntime::new(
189 temperature_config.candidate_capacity,
190 )),
191 temperature_config,
192 temperature_access_counter: AtomicU64::new(0),
193 temperature_owner: PoolMemberId::new(),
194 temperature_cycle: Mutex::new(()),
195 temperature_worker: TemperatureWorker::default(),
196 }),
197 };
198 store.refresh_members()?;
199 store.start_temperature_worker()?;
200 Ok(store)
201 }
202
203 fn start_temperature_worker(&self) -> Result<(), StoreError> {
204 if !self.temperature_config.enabled {
205 return Ok(());
206 }
207 self.temperature_worker.start(
208 Arc::downgrade(&self.inner),
209 self.temperature_config.interval,
210 )
211 }
212
213 pub fn add_member(&self, config: PoolMemberConfig) -> Result<PoolMemberId, StoreError> {
214 self.add_member_inner(config, false)?
215 .ok_or_else(|| StoreError::Other("pool member was not added".into()))
216 }
217
218 pub(crate) fn ensure_initial_member(
219 &self,
220 config: PoolMemberConfig,
221 ) -> Result<Option<PoolMemberId>, StoreError> {
222 if !self.read_manifest()?.members.is_empty() {
223 self.refresh_members()?;
224 return Ok(None);
225 }
226 self.add_member_inner(config, true)
227 }
228
229 fn add_member_inner(
230 &self,
231 config: PoolMemberConfig,
232 only_if_empty: bool,
233 ) -> Result<Option<PoolMemberId>, StoreError> {
234 validate_member_config(&config)?;
235 let wtxn = self.env.write_txn().map_err(map_heed)?;
236 let manifest = self.manifest_from_txn(&wtxn)?;
237 if only_if_empty && !manifest.members.is_empty() {
238 drop(wtxn);
239 self.refresh_members()?;
240 return Ok(None);
241 }
242 validate_new_member_paths(&manifest, &config)?;
243 let id = prepare_member_paths(&config, PoolMemberId::new())?;
244 drop(wtxn);
245 let store = Arc::new(open_member_store(id, &config)?);
246
247 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
248 let mut manifest = self.manifest_from_txn(&wtxn)?;
249 if only_if_empty && !manifest.members.is_empty() {
250 drop(wtxn);
251 self.refresh_members()?;
252 return Ok(None);
253 }
254 validate_new_member_paths(&manifest, &config)?;
255 if manifest.members.iter().any(|member| member.id == id) {
256 return Err(StoreError::Other(format!(
257 "pool member identity is already configured: {id}"
258 )));
259 }
260 manifest.members.push(MemberRecord {
261 id,
262 state: PoolMemberState::Active,
263 config,
264 });
265 manifest.generation = manifest.generation.saturating_add(1);
266 self.put_manifest_txn(&mut wtxn, &manifest)?;
267 wtxn.commit().map_err(map_heed)?;
268
269 let mut runtime = self
270 .runtime
271 .write()
272 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
273 runtime.generation = Some(manifest.generation);
274 runtime.stores.insert(id, store);
275 let member = manifest
276 .members
277 .iter()
278 .find(|member| member.id == id)
279 .expect("new pool member is in committed manifest");
280 runtime.read_gates.insert(
281 id,
282 Arc::new(ConcurrencyGate::new(member.config.max_read_concurrency)),
283 );
284 runtime.write_gates.insert(
285 id,
286 Arc::new(ConcurrencyGate::new(member.config.max_write_concurrency)),
287 );
288 runtime.errors.remove(&id);
289 Ok(Some(id))
290 }
291
292 pub fn begin_drain(&self, id: PoolMemberId) -> Result<(), StoreError> {
293 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
294 let mut manifest = self.manifest_from_txn(&wtxn)?;
295 let has_other_active = manifest
296 .members
297 .iter()
298 .any(|member| member.id != id && member.state == PoolMemberState::Active);
299 let member = manifest
300 .members
301 .iter_mut()
302 .find(|member| member.id == id)
303 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
304 if member.state == PoolMemberState::Draining {
305 return Ok(());
306 }
307 if !has_other_active && self.count_member_locations_txn(&wtxn, id)? > 0 {
308 return Err(StoreError::Other(
309 "cannot drain the final member while it still owns blobs".into(),
310 ));
311 }
312 member.state = PoolMemberState::Draining;
313 manifest.generation = manifest.generation.saturating_add(1);
314 self.put_manifest_txn(&mut wtxn, &manifest)?;
315 wtxn.commit().map_err(map_heed)?;
316 self.refresh_members()?;
317 Ok(())
318 }
319
320 pub fn update_member_limits(
321 &self,
322 id: PoolMemberId,
323 capacity_bytes: u64,
324 max_read_concurrency: u32,
325 max_write_concurrency: u32,
326 ) -> Result<(), StoreError> {
327 if capacity_bytes == 0 || max_read_concurrency == 0 || max_write_concurrency == 0 {
328 return Err(StoreError::Other(
329 "pool member capacity and concurrency limits must be non-zero".into(),
330 ));
331 }
332 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
333 let mut manifest = self.manifest_from_txn(&wtxn)?;
334 let member = manifest
335 .members
336 .iter_mut()
337 .find(|member| member.id == id)
338 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
339 member.config.capacity_bytes = capacity_bytes;
340 member.config.max_read_concurrency = max_read_concurrency;
341 member.config.max_write_concurrency = max_write_concurrency;
342 manifest.generation = manifest.generation.saturating_add(1);
343 self.put_manifest_txn(&mut wtxn, &manifest)?;
344 wtxn.commit().map_err(map_heed)?;
345 self.refresh_members()
346 }
347
348 pub fn update_member_temperature_watermarks(
349 &self,
350 id: PoolMemberId,
351 low_percent: u8,
352 high_percent: u8,
353 ) -> Result<(), StoreError> {
354 if low_percent >= high_percent || high_percent > 100 {
355 return Err(StoreError::Other(
356 "pool temperature watermarks must satisfy 0 <= low < high <= 100".into(),
357 ));
358 }
359 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
360 let mut manifest = self.manifest_from_txn(&wtxn)?;
361 let member = manifest
362 .members
363 .iter_mut()
364 .find(|member| member.id == id)
365 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
366 member.config.temperature_low_watermark_percent = low_percent;
367 member.config.temperature_high_watermark_percent = high_percent;
368 manifest.generation = manifest.generation.saturating_add(1);
369 self.put_manifest_txn(&mut wtxn, &manifest)?;
370 wtxn.commit().map_err(map_heed)?;
371 self.refresh_members()
372 }
373
374 pub fn remove_member(&self, id: PoolMemberId) -> Result<(), StoreError> {
375 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
376 let mut manifest = self.manifest_from_txn(&wtxn)?;
377 let index = manifest
378 .members
379 .iter()
380 .position(|member| member.id == id)
381 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
382 if manifest.members[index].state != PoolMemberState::Draining {
383 return Err(StoreError::Other(
384 "pool member must be draining before removal".into(),
385 ));
386 }
387 let located = self.count_member_locations_txn(&wtxn, id)?;
388 if located != 0 {
389 return Err(StoreError::Other(format!(
390 "pool member {id} still owns {located} blob(s)"
391 )));
392 }
393 manifest.members.remove(index);
394 manifest.generation = manifest.generation.saturating_add(1);
395 self.put_manifest_txn(&mut wtxn, &manifest)?;
396 wtxn.commit().map_err(map_heed)?;
397 self.refresh_members()?;
398 Ok(())
399 }
400
401 pub fn member(&self, id: PoolMemberId) -> Result<PoolMemberStatus, StoreError> {
402 self.refresh_members()?;
403 let manifest = self.read_manifest()?;
404 let member = manifest
405 .members
406 .iter()
407 .find(|member| member.id == id)
408 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
409 let located_blobs = self.count_member_locations(id)?;
410 let runtime = self
411 .runtime
412 .read()
413 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
414 let (logical_bytes, available, last_error) = match runtime.stores.get(&id) {
415 Some(store) => match store.stats() {
416 Ok(stats) => (stats.total_bytes, true, None),
417 Err(error) => (0, false, Some(error.to_string())),
418 },
419 None => (0, false, runtime.errors.get(&id).cloned()),
420 };
421 Ok(PoolMemberStatus {
422 id,
423 state: member.state,
424 path: member.config.path.clone(),
425 capacity_bytes: member.config.capacity_bytes,
426 map_size_bytes: member.config.map_size_bytes,
427 external_blob_dir: member.config.external_blob_dir.clone(),
428 external_blob_min_bytes: member.config.external_blob_min_bytes,
429 external_blob_sync: member.config.external_blob_sync,
430 external_pack_target_bytes: member.config.external_pack_target_bytes,
431 max_read_concurrency: member.config.max_read_concurrency,
432 max_write_concurrency: member.config.max_write_concurrency,
433 temperature_low_watermark_percent: member.config.temperature_low_watermark_percent,
434 temperature_high_watermark_percent: member.config.temperature_high_watermark_percent,
435 logical_bytes,
436 located_blobs,
437 available,
438 last_error,
439 })
440 }
441
442 pub fn members(&self) -> Result<Vec<PoolMemberStatus>, StoreError> {
443 let manifest = self.read_manifest()?;
444 manifest
445 .members
446 .iter()
447 .map(|member| self.member(member.id))
448 .collect()
449 }
450
451 pub fn blob_location(&self, hash: &Hash) -> Result<Option<PoolMemberId>, StoreError> {
452 Ok(self
453 .read_location(hash)?
454 .map(LocationRecord::preferred_member))
455 }
456
457 pub fn put_sync(&self, hash: Hash, data: &[u8]) -> Result<bool, StoreError> {
458 if sha256(data) != hash {
459 return Err(StoreError::Other(
460 "pool rejected bytes that do not match their hash".into(),
461 ));
462 }
463
464 if let Some(location) = self.read_location(&hash)? {
465 match self.read_verified_location(&hash, location) {
466 Ok(Some(found)) => {
467 if matches!(location, LocationRecord::Pending { .. }) {
468 self.finalize_pending(hash, location)?;
469 }
470 debug_assert_eq!(sha256(&found), hash);
471 return Ok(false);
472 }
473 Ok(None) | Err(_) => return self.repair_location(hash, data, location),
474 }
475 }
476
477 let target = self.choose_write_member(data.len() as u64, None)?;
478 let pending = LocationRecord::Pending {
479 member: target,
480 size: data.len() as u64,
481 };
482 let location = self.reserve_if_absent(hash, pending)?;
483 if location != pending {
484 match self.read_verified_location(&hash, location) {
485 Ok(Some(found)) => {
486 if matches!(location, LocationRecord::Pending { .. }) {
487 self.finalize_pending(hash, location)?;
488 }
489 debug_assert_eq!(sha256(&found), hash);
490 return Ok(false);
491 }
492 Ok(None) | Err(_) => return self.repair_location(hash, data, location),
493 }
494 }
495
496 let target = location.preferred_member();
497 let store = self.get_member(target)?;
498 match self.write_verified_member(target, &store, hash, data) {
499 Ok(inserted) => {
500 self.finalize_pending(hash, location)?;
501 Ok(inserted)
502 }
503 Err(_) => {
504 let mut excluded = HashSet::new();
505 excluded.insert(target);
506 self.repair_location_excluding(hash, data, location, excluded)
507 }
508 }
509 }
510
511 pub fn put_many_report_sync(
512 &self,
513 items: &[(Hash, Vec<u8>)],
514 ) -> Result<PutManyReport, StoreError> {
515 let mut seen = HashSet::new();
516 let mut unique = Vec::with_capacity(items.len());
517 let mut ordered = Vec::with_capacity(items.len());
518 for (hash, data) in items {
519 if sha256(data) != *hash {
520 return Err(StoreError::Other(
521 "pool rejected batch bytes that do not match their hash".into(),
522 ));
523 }
524 if seen.insert(*hash) {
525 unique.push((*hash, data));
526 ordered.push((*hash, data.len() as u64));
527 }
528 }
529
530 let mut inserted = HashSet::new();
531 let mut missing = Vec::new();
532 for (hash, data) in unique {
533 if self.read_location(&hash)?.is_some() {
534 if self.put_sync(hash, data)? {
535 inserted.insert(hash);
536 }
537 } else {
538 missing.push((hash, data));
539 }
540 }
541 if missing.is_empty() {
542 return Ok(put_many_report(items.len(), &ordered, &inserted));
543 }
544
545 let mut reserved_bytes = HashMap::new();
546 let mut assignments = Vec::with_capacity(missing.len());
547 for (hash, data) in missing {
548 let target =
549 self.choose_write_member_with_reserved(data.len() as u64, None, &reserved_bytes)?;
550 let reserved = reserved_bytes.entry(target).or_insert(0u64);
551 *reserved = reserved.saturating_add(data.len() as u64);
552 assignments.push((hash, data, target));
553 }
554
555 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
556 let mut plans = Vec::with_capacity(assignments.len());
557 let mut raced = Vec::new();
558 for (hash, data, target) in assignments {
559 if self
560 .locations
561 .get(&wtxn, &hash)
562 .map_err(map_heed)?
563 .is_some()
564 {
565 raced.push((hash, data));
566 continue;
567 }
568 let pending = LocationRecord::Pending {
569 member: target,
570 size: data.len() as u64,
571 };
572 self.set_location_txn(&mut wtxn, hash, Some(pending))?;
573 plans.push((hash, data, target, pending));
574 }
575 wtxn.commit().map_err(map_heed)?;
576
577 for (hash, data) in raced {
578 if self.put_sync(hash, data)? {
579 inserted.insert(hash);
580 }
581 }
582
583 let mut by_target: HashMap<PoolMemberId, Vec<(Hash, &[u8])>> = HashMap::new();
584 for (hash, data, target, _) in &plans {
585 by_target
586 .entry(*target)
587 .or_default()
588 .push((*hash, data.as_slice()));
589 }
590 for (target, batch) in by_target {
591 let store = self.get_member(target)?;
592 let gate = self.member_gate(target, true)?;
593 let permit = gate.acquire()?;
594 for (hash, _) in &batch {
595 if store
596 .get_sync(hash)?
597 .is_some_and(|existing| sha256(&existing) != *hash)
598 {
599 store.delete_sync(hash)?;
600 }
601 }
602 let started = Instant::now();
603 let result = store.put_many_refs_report_sync(&batch);
604 let success = result.is_ok();
605 let bytes = batch.iter().map(|(_, data)| data.len()).sum::<usize>();
606 self.record_write(target, started.elapsed(), bytes, success);
607 let report = match result {
608 Ok(report) => report,
609 Err(_) => {
610 drop(permit);
611 for (hash, data) in batch {
612 if self.put_sync(hash, data)? {
613 inserted.insert(hash);
614 }
615 }
616 continue;
617 }
618 };
619 inserted.extend(report.inserted_hashes);
620 for (hash, _) in &batch {
621 self.read_verified_member(target, &store, hash)?
622 .ok_or_else(|| {
623 StoreError::Other(format!(
624 "pool member {target} lost a committed batch write"
625 ))
626 })?;
627 }
628 }
629
630 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
631 for (hash, _, target, pending) in plans {
632 let Some(current) = self.locations.get(&wtxn, &hash).map_err(map_heed)? else {
633 continue;
634 };
635 if LocationRecord::decode(current)? == pending {
636 self.set_location_txn(
637 &mut wtxn,
638 hash,
639 Some(LocationRecord::Stored {
640 member: target,
641 size: pending.size(),
642 }),
643 )?;
644 }
645 }
646 wtxn.commit().map_err(map_heed)?;
647 Ok(put_many_report(items.len(), &ordered, &inserted))
648 }
649
650 pub fn put_many_sync(&self, items: &[(Hash, Vec<u8>)]) -> Result<usize, StoreError> {
651 self.put_many_report_sync(items)
652 .map(|report| report.inserted)
653 }
654
655 pub fn get_sync(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
656 let Some(location) = self.read_location(hash)? else {
657 return Ok(None);
658 };
659 let data = self.read_verified_location(hash, location)?;
660 if data.is_some() && matches!(location, LocationRecord::Pending { .. }) {
661 self.finalize_pending(*hash, location)?;
662 }
663 if data.is_some() {
664 self.sample_temperature_access(*hash, location);
665 }
666 Ok(data)
667 }
668
669 pub fn get_range_sync(
670 &self,
671 hash: &Hash,
672 start: u64,
673 end_inclusive: u64,
674 ) -> Result<Option<Vec<u8>>, StoreError> {
675 let Some(data) = self.get_sync(hash)? else {
676 return Ok(None);
677 };
678 slice_blob_range(&data, start, end_inclusive).map(Some)
679 }
680
681 pub fn blob_size_sync(&self, hash: &Hash) -> Result<Option<u64>, StoreError> {
682 Ok(self.read_location(hash)?.map(LocationRecord::size))
683 }
684
685 pub fn exists(&self, hash: &Hash) -> Result<bool, StoreError> {
686 Ok(self.get_sync(hash)?.is_some())
687 }
688
689 pub fn existing_hashes_in_sorted_candidates(
690 &self,
691 sorted_hashes: &[Hash],
692 ) -> Result<Vec<bool>, StoreError> {
693 let rtxn = self.env.read_txn().map_err(map_heed)?;
694 sorted_hashes
695 .iter()
696 .map(|hash| self.locations.get(&rtxn, hash).map(|value| value.is_some()))
697 .collect::<Result<Vec<_>, _>>()
698 .map_err(map_heed)
699 }
700
701 pub fn largest_member_map_size_bytes(&self) -> Result<Option<usize>, StoreError> {
706 self.refresh_members()?;
707 let runtime = self
708 .runtime
709 .read()
710 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
711 Ok(runtime
712 .stores
713 .values()
714 .map(|store| store.map_size_bytes())
715 .max())
716 }
717
718 pub fn delete_sync(&self, hash: &Hash) -> Result<bool, StoreError> {
719 let Some(location) = self.read_location(hash)? else {
720 return Ok(false);
721 };
722 let (members, len) = location.members();
723 let mut deleted = false;
724 for member in members.into_iter().take(len) {
725 if let Ok(store) = self.get_member(member) {
726 let gate = self.member_gate(member, true)?;
727 let _permit = gate.acquire()?;
728 deleted |= store.delete_sync(hash)?;
729 }
730 }
731 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
732 self.set_location_txn(&mut wtxn, *hash, None)?;
733 self.pins.delete(&mut wtxn, hash).map_err(map_heed)?;
734 wtxn.commit().map_err(map_heed)?;
735 Ok(deleted)
736 }
737
738 pub fn pin_sync(&self, hash: &Hash) -> Result<(), StoreError> {
739 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
740 let previous = self
741 .pins
742 .get(&wtxn, hash)
743 .map_err(map_heed)?
744 .map(decode_pin_count)
745 .transpose()?
746 .unwrap_or(0);
747 self.pins
748 .put(&mut wtxn, hash, &previous.saturating_add(1).to_be_bytes())
749 .map_err(map_heed)?;
750 wtxn.commit().map_err(map_heed)
751 }
752
753 pub fn unpin_sync(&self, hash: &Hash) -> Result<(), StoreError> {
754 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
755 let count = self
756 .pins
757 .get(&wtxn, hash)
758 .map_err(map_heed)?
759 .map(decode_pin_count)
760 .transpose()?
761 .unwrap_or(0);
762 if count <= 1 {
763 self.pins.delete(&mut wtxn, hash).map_err(map_heed)?;
764 } else {
765 self.pins
766 .put(&mut wtxn, hash, &(count - 1).to_be_bytes())
767 .map_err(map_heed)?;
768 }
769 wtxn.commit().map_err(map_heed)
770 }
771
772 pub fn pin_count_sync(&self, hash: &Hash) -> Result<u32, StoreError> {
773 let rtxn = self.env.read_txn().map_err(map_heed)?;
774 self.pins
775 .get(&rtxn, hash)
776 .map_err(map_heed)?
777 .map(decode_pin_count)
778 .transpose()
779 .map(|count| count.unwrap_or(0))
780 }
781
782 pub fn touch_accessed_sync(&self, hash: &Hash, now: u64) -> Result<bool, StoreError> {
783 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
784 if self.locations.get(&wtxn, hash).map_err(map_heed)?.is_none() {
785 return Ok(false);
786 }
787 let previous = self
788 .last_accessed
789 .get(&wtxn, hash)
790 .map_err(map_heed)?
791 .and_then(self::temperature::AccessRecord::decode)
792 .unwrap_or_else(|| self::temperature::AccessRecord::new(now));
793 let access = self::temperature::AccessRecord {
794 last_accessed_at: now,
795 ..previous
796 }
797 .encode();
798 self.last_accessed
799 .put(&mut wtxn, hash, &access)
800 .map_err(map_heed)?;
801 wtxn.commit().map_err(map_heed)?;
802 Ok(true)
803 }
804
805 pub fn touch_many_accessed_sync(&self, hashes: &[Hash], now: u64) -> Result<usize, StoreError> {
806 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
807 let mut updated = 0usize;
808 let mut seen = HashSet::new();
809 for hash in hashes {
810 if !seen.insert(*hash) || self.locations.get(&wtxn, hash).map_err(map_heed)?.is_none() {
811 continue;
812 }
813 let previous = self
814 .last_accessed
815 .get(&wtxn, hash)
816 .map_err(map_heed)?
817 .and_then(self::temperature::AccessRecord::decode)
818 .unwrap_or_else(|| self::temperature::AccessRecord::new(now));
819 let access = self::temperature::AccessRecord {
820 last_accessed_at: now,
821 ..previous
822 }
823 .encode();
824 self.last_accessed
825 .put(&mut wtxn, hash, &access)
826 .map_err(map_heed)?;
827 updated += 1;
828 }
829 wtxn.commit().map_err(map_heed)?;
830 Ok(updated)
831 }
832
833 pub fn last_accessed_at_sync(&self, hash: &Hash) -> Result<Option<u64>, StoreError> {
834 let rtxn = self.env.read_txn().map_err(map_heed)?;
835 self.last_accessed
836 .get(&rtxn, hash)
837 .map_err(map_heed)?
838 .map(|bytes| {
839 self::temperature::AccessRecord::decode(bytes)
840 .map(|access| access.last_accessed_at)
841 .ok_or_else(|| StoreError::Other("invalid pool access record".into()))
842 })
843 .transpose()
844 }
845
846 pub fn many_last_accessed_at_sync(
847 &self,
848 hashes: &[Hash],
849 ) -> Result<Vec<(Hash, u64)>, StoreError> {
850 let rtxn = self.env.read_txn().map_err(map_heed)?;
851 let mut values = Vec::new();
852 for hash in hashes {
853 if let Some(value) = self.last_accessed.get(&rtxn, hash).map_err(map_heed)? {
854 let access = self::temperature::AccessRecord::decode(value)
855 .ok_or_else(|| StoreError::Other("invalid pool access record".into()))?;
856 values.push((*hash, access.last_accessed_at));
857 }
858 }
859 Ok(values)
860 }
861
862 pub fn list(&self) -> Result<Vec<Hash>, StoreError> {
863 let rtxn = self.env.read_txn().map_err(map_heed)?;
864 let mut hashes = Vec::new();
865 for item in self.locations.iter(&rtxn).map_err(map_heed)? {
866 let (hash, _) = item.map_err(map_heed)?;
867 let hash: Hash = hash
868 .try_into()
869 .map_err(|_| StoreError::Other("invalid pool hash key".into()))?;
870 hashes.push(hash);
871 }
872 Ok(hashes)
873 }
874
875 pub fn stats(&self) -> Result<StoreStats, StoreError> {
876 let rtxn = self.env.read_txn().map_err(map_heed)?;
877 let mut stats = StoreStats::default();
878 for item in self.locations.iter(&rtxn).map_err(map_heed)? {
879 let (_, location) = item.map_err(map_heed)?;
880 let location = LocationRecord::decode(location)?;
881 stats.count = stats.count.saturating_add(1);
882 stats.bytes = stats.bytes.saturating_add(location.size());
883 }
884 for item in self.pins.iter(&rtxn).map_err(map_heed)? {
885 let (hash, count) = item.map_err(map_heed)?;
886 if decode_pin_count(count)? == 0 {
887 continue;
888 }
889 let Some(location) = self.locations.get(&rtxn, hash).map_err(map_heed)? else {
890 continue;
891 };
892 stats.pinned_count = stats.pinned_count.saturating_add(1);
893 stats.pinned_bytes = stats
894 .pinned_bytes
895 .saturating_add(LocationRecord::decode(location)?.size());
896 }
897 Ok(stats)
898 }
899
900 pub fn force_sync(&self) -> Result<(), StoreError> {
901 self.env.force_sync().map_err(map_heed)?;
902 self.refresh_members()?;
903 let runtime = self
904 .runtime
905 .read()
906 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
907 for store in runtime.stores.values() {
908 store.force_sync()?;
909 }
910 Ok(())
911 }
912
913 fn refresh_members(&self) -> Result<(), StoreError> {
914 let manifest = self.read_manifest()?;
915 let mut runtime = self
916 .runtime
917 .write()
918 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
919 if runtime.generation == Some(manifest.generation) {
920 return Ok(());
921 }
922
923 let configured = manifest
924 .members
925 .iter()
926 .map(|member| member.id)
927 .collect::<HashSet<_>>();
928 runtime.stores.retain(|id, _| configured.contains(id));
929 runtime.read_gates.retain(|id, _| configured.contains(id));
930 runtime.write_gates.retain(|id, _| configured.contains(id));
931 runtime.errors.retain(|id, _| configured.contains(id));
932 for member in &manifest.members {
933 let read_gate = runtime
934 .read_gates
935 .entry(member.id)
936 .or_insert_with(|| {
937 Arc::new(ConcurrencyGate::new(member.config.max_read_concurrency))
938 })
939 .clone();
940 read_gate.set_limit(member.config.max_read_concurrency)?;
941 let write_gate = runtime
942 .write_gates
943 .entry(member.id)
944 .or_insert_with(|| {
945 Arc::new(ConcurrencyGate::new(member.config.max_write_concurrency))
946 })
947 .clone();
948 write_gate.set_limit(member.config.max_write_concurrency)?;
949 if runtime.stores.contains_key(&member.id) {
950 continue;
951 }
952 match open_member_store(member.id, &member.config) {
953 Ok(store) => {
954 runtime.stores.insert(member.id, Arc::new(store));
955 runtime.errors.remove(&member.id);
956 }
957 Err(error) => {
958 runtime.errors.insert(member.id, error.to_string());
959 }
960 }
961 }
962 runtime.generation = Some(manifest.generation);
963 drop(runtime);
964 self.adaptive
965 .lock()
966 .map_err(|_| StoreError::Other("pool adaptive lock poisoned".into()))?
967 .retain(&configured);
968 Ok(())
969 }
970
971 fn get_member(&self, id: PoolMemberId) -> Result<Arc<LmdbBlobStore>, StoreError> {
972 self.refresh_members()?;
973 if let Some(store) = self
974 .runtime
975 .read()
976 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?
977 .stores
978 .get(&id)
979 .cloned()
980 {
981 return Ok(store);
982 }
983
984 let member = self
985 .read_manifest()?
986 .members
987 .into_iter()
988 .find(|member| member.id == id)
989 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
990 match open_member_store(id, &member.config) {
991 Ok(store) => {
992 let store = Arc::new(store);
993 let mut runtime = self
994 .runtime
995 .write()
996 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
997 runtime.stores.insert(id, Arc::clone(&store));
998 runtime.errors.remove(&id);
999 Ok(store)
1000 }
1001 Err(error) => {
1002 self.record_member_failure(id, false);
1003 let mut runtime = self
1004 .runtime
1005 .write()
1006 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1007 runtime.errors.insert(id, error.to_string());
1008 Err(error)
1009 }
1010 }
1011 }
1012
1013 fn member_state(&self, id: PoolMemberId) -> Result<Option<PoolMemberState>, StoreError> {
1014 Ok(self
1015 .read_manifest()?
1016 .members
1017 .into_iter()
1018 .find(|member| member.id == id)
1019 .map(|member| member.state))
1020 }
1021
1022 fn member_gate(
1023 &self,
1024 id: PoolMemberId,
1025 write: bool,
1026 ) -> Result<Arc<ConcurrencyGate>, StoreError> {
1027 self.refresh_members()?;
1028 let runtime = self
1029 .runtime
1030 .read()
1031 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1032 let gates = if write {
1033 &runtime.write_gates
1034 } else {
1035 &runtime.read_gates
1036 };
1037 gates
1038 .get(&id)
1039 .cloned()
1040 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))
1041 }
1042
1043 fn choose_write_member(
1044 &self,
1045 incoming_bytes: u64,
1046 exclude: Option<PoolMemberId>,
1047 ) -> Result<PoolMemberId, StoreError> {
1048 self.choose_write_member_with_reserved(incoming_bytes, exclude, &HashMap::new())
1049 }
1050
1051 fn choose_write_member_with_reserved(
1052 &self,
1053 incoming_bytes: u64,
1054 exclude: Option<PoolMemberId>,
1055 reserved_bytes: &HashMap<PoolMemberId, u64>,
1056 ) -> Result<PoolMemberId, StoreError> {
1057 let excluded = exclude.into_iter().collect::<HashSet<_>>();
1058 self.choose_write_member_excluding(incoming_bytes, &excluded, reserved_bytes)
1059 }
1060
1061 fn choose_write_member_excluding(
1062 &self,
1063 incoming_bytes: u64,
1064 excluded: &HashSet<PoolMemberId>,
1065 reserved_bytes: &HashMap<PoolMemberId, u64>,
1066 ) -> Result<PoolMemberId, StoreError> {
1067 self.refresh_members()?;
1068 let manifest = self.read_manifest()?;
1069 let runtime = self
1070 .runtime
1071 .read()
1072 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1073 let mut candidates = Vec::new();
1074 let mut below_high_watermark = Vec::new();
1075 for member in manifest.members.iter().filter(|member| {
1076 member.state == PoolMemberState::Active && !excluded.contains(&member.id)
1077 }) {
1078 let Some(store) = runtime.stores.get(&member.id) else {
1079 continue;
1080 };
1081 let stats = match store.stats() {
1082 Ok(stats) => stats,
1083 Err(_) => continue,
1084 };
1085 let effective_bytes = stats
1086 .total_bytes
1087 .saturating_add(reserved_bytes.get(&member.id).copied().unwrap_or(0));
1088 if member.config.capacity_bytes > 0
1089 && effective_bytes.saturating_add(incoming_bytes) > member.config.capacity_bytes
1090 {
1091 continue;
1092 }
1093 candidates.push((member.id, effective_bytes, member.config.capacity_bytes));
1094 let projected_fill = effective_bytes
1095 .saturating_add(incoming_bytes)
1096 .saturating_mul(100)
1097 .saturating_div(member.config.capacity_bytes)
1098 .min(100);
1099 if projected_fill <= u64::from(member.config.temperature_high_watermark_percent) {
1100 below_high_watermark.push((
1101 member.id,
1102 effective_bytes,
1103 member.config.capacity_bytes,
1104 ));
1105 }
1106 }
1107 drop(runtime);
1108 if !below_high_watermark.is_empty() {
1109 candidates = below_high_watermark;
1110 }
1111 self.adaptive
1112 .lock()
1113 .map_err(|_| StoreError::Other("pool adaptive lock poisoned".into()))?
1114 .choose_write(&candidates)
1115 .ok_or_else(|| StoreError::Other("no writable pool member has capacity".into()))
1116 }
1117
1118 fn repair_location(
1119 &self,
1120 hash: Hash,
1121 data: &[u8],
1122 expected: LocationRecord,
1123 ) -> Result<bool, StoreError> {
1124 self.repair_location_excluding(hash, data, expected, HashSet::new())
1125 }
1126
1127 fn repair_location_excluding(
1128 &self,
1129 hash: Hash,
1130 data: &[u8],
1131 expected: LocationRecord,
1132 mut excluded: HashSet<PoolMemberId>,
1133 ) -> Result<bool, StoreError> {
1134 let preferred = expected.preferred_member();
1135 let mut next = (!excluded.contains(&preferred)
1136 && self.member_state(preferred)? == Some(PoolMemberState::Active))
1137 .then_some(preferred);
1138 let mut last_error = None;
1139 let (target, inserted) = loop {
1140 let target = match next.take() {
1141 Some(target) => target,
1142 None => match self.choose_write_member_excluding(
1143 data.len() as u64,
1144 &excluded,
1145 &HashMap::new(),
1146 ) {
1147 Ok(target) => target,
1148 Err(error) => {
1149 return Err(last_error.unwrap_or(error));
1150 }
1151 },
1152 };
1153 let result = self
1154 .get_member(target)
1155 .and_then(|store| self.write_verified_member(target, &store, hash, data));
1156 match result {
1157 Ok(inserted) => break (target, inserted),
1158 Err(error) => {
1159 excluded.insert(target);
1160 last_error = Some(error);
1161 }
1162 }
1163 };
1164 let stored = LocationRecord::Stored {
1165 member: target,
1166 size: data.len() as u64,
1167 };
1168
1169 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
1170 let current = self
1171 .locations
1172 .get(&wtxn, &hash)
1173 .map_err(map_heed)?
1174 .map(LocationRecord::decode)
1175 .transpose()?;
1176 if current == Some(expected) {
1177 self.set_location_txn(&mut wtxn, hash, Some(stored))?;
1178 wtxn.commit().map_err(map_heed)?;
1179 return Ok(inserted);
1180 }
1181 drop(wtxn);
1182
1183 if let Some(current) = current {
1184 if self.read_verified_location(&hash, current)?.is_some() {
1185 return Ok(false);
1186 }
1187 }
1188 Err(StoreError::Other(format!(
1189 "pool location changed while repairing {hash:?}"
1190 )))
1191 }
1192
1193 fn read_verified_location(
1194 &self,
1195 hash: &Hash,
1196 location: LocationRecord,
1197 ) -> Result<Option<Vec<u8>>, StoreError> {
1198 let mut ids = match location {
1199 LocationRecord::Pending { member, .. } | LocationRecord::Stored { member, .. } => {
1200 vec![member]
1201 }
1202 LocationRecord::Moving { source, target, .. } => vec![target, source],
1203 };
1204 self.adaptive
1205 .lock()
1206 .map_err(|_| StoreError::Other("pool adaptive lock poisoned".into()))?
1207 .order_reads(&mut ids);
1208
1209 let mut first_error = None;
1210 for id in ids {
1211 let store = match self.get_member(id) {
1212 Ok(store) => store,
1213 Err(error) => {
1214 first_error.get_or_insert(error);
1215 continue;
1216 }
1217 };
1218 match self.read_verified_member(id, &store, hash) {
1219 Ok(Some(data)) => return Ok(Some(data)),
1220 Ok(None) => {}
1221 Err(error) => {
1222 first_error.get_or_insert(error);
1223 }
1224 }
1225 }
1226 match first_error {
1227 Some(error) => Err(error),
1228 None => Ok(None),
1229 }
1230 }
1231
1232 fn read_verified_member(
1233 &self,
1234 id: PoolMemberId,
1235 store: &LmdbBlobStore,
1236 hash: &Hash,
1237 ) -> Result<Option<Vec<u8>>, StoreError> {
1238 let gate = self.member_gate(id, false)?;
1239 let _permit = gate.acquire()?;
1240 let started = Instant::now();
1241 let result = store.get_sync(hash);
1242 match result {
1243 Ok(Some(data)) if sha256(&data) == *hash => {
1244 self.record_read(id, started.elapsed(), true);
1245 Ok(Some(data))
1246 }
1247 Ok(Some(_)) => {
1248 self.record_read(id, started.elapsed(), false);
1249 Err(StoreError::Other(format!(
1250 "pool member {id} returned corrupt bytes"
1251 )))
1252 }
1253 Ok(None) => {
1254 self.record_read(id, started.elapsed(), true);
1255 Ok(None)
1256 }
1257 Err(error) => {
1258 self.record_read(id, started.elapsed(), false);
1259 Err(error)
1260 }
1261 }
1262 }
1263
1264 fn write_verified_member(
1265 &self,
1266 id: PoolMemberId,
1267 store: &LmdbBlobStore,
1268 hash: Hash,
1269 data: &[u8],
1270 ) -> Result<bool, StoreError> {
1271 let gate = self.member_gate(id, true)?;
1272 let _permit = gate.acquire()?;
1273 if let Some(existing) = store.get_sync(&hash)? {
1274 if sha256(&existing) == hash {
1275 return Ok(false);
1276 }
1277 store.delete_sync(&hash)?;
1278 }
1279 let started = Instant::now();
1280 let result = store.put_sync(hash, data);
1281 let success = result.is_ok();
1282 self.record_write(id, started.elapsed(), data.len(), success);
1283 let inserted = result?;
1284 let written = store
1285 .get_sync(&hash)?
1286 .ok_or_else(|| StoreError::Other(format!("pool member {id} lost a committed write")))?;
1287 if sha256(&written) != hash {
1288 self.record_member_failure(id, true);
1289 return Err(StoreError::Other(format!(
1290 "pool member {id} committed corrupt bytes"
1291 )));
1292 }
1293 Ok(inserted)
1294 }
1295
1296 fn delete_member_blob(
1297 &self,
1298 id: PoolMemberId,
1299 store: &LmdbBlobStore,
1300 hash: &Hash,
1301 ) -> Result<bool, StoreError> {
1302 let gate = self.member_gate(id, true)?;
1303 let _permit = gate.acquire()?;
1304 store.delete_sync(hash)
1305 }
1306
1307 fn record_read(&self, id: PoolMemberId, elapsed: Duration, success: bool) {
1308 if let Ok(mut adaptive) = self.adaptive.lock() {
1309 adaptive.record_read(id, elapsed, success);
1310 }
1311 }
1312
1313 fn record_write(&self, id: PoolMemberId, elapsed: Duration, bytes: usize, success: bool) {
1314 if let Ok(mut adaptive) = self.adaptive.lock() {
1315 adaptive.record_write(id, elapsed, bytes, success);
1316 }
1317 }
1318
1319 fn record_member_failure(&self, id: PoolMemberId, write: bool) {
1320 if write {
1321 self.record_write(id, Duration::ZERO, 0, false);
1322 } else {
1323 self.record_read(id, Duration::ZERO, false);
1324 }
1325 }
1326
1327 fn sample_temperature_access(&self, hash: Hash, location: LocationRecord) {
1328 if !self.temperature_config.enabled {
1329 return;
1330 }
1331 let sample_rate = u64::from(self.temperature_config.read_sample_rate.max(1));
1332 let access = self
1333 .temperature_access_counter
1334 .fetch_add(1, Ordering::Relaxed)
1335 .saturating_add(1);
1336 if !access.is_multiple_of(sample_rate) {
1337 return;
1338 }
1339 if let Ok(mut temperature) = self.temperature.lock() {
1340 temperature
1341 .samples
1342 .observe(hash, location, unix_timestamp_now());
1343 }
1344 }
1345}
1346
1347#[async_trait]
1348impl Store for PoolStore {
1349 async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError> {
1350 self.put_sync(hash, &data)
1351 }
1352
1353 async fn put_many(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
1354 self.put_many_sync(&items)
1355 }
1356
1357 async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
1358 self.get_sync(hash)
1359 }
1360
1361 async fn get_range(
1362 &self,
1363 hash: &Hash,
1364 start: u64,
1365 end_inclusive: u64,
1366 ) -> Result<Option<Vec<u8>>, StoreError> {
1367 self.get_range_sync(hash, start, end_inclusive)
1368 }
1369
1370 async fn blob_size(&self, hash: &Hash) -> Result<Option<u64>, StoreError> {
1371 self.blob_size_sync(hash)
1372 }
1373
1374 async fn has(&self, hash: &Hash) -> Result<bool, StoreError> {
1375 self.exists(hash)
1376 }
1377
1378 async fn delete(&self, hash: &Hash) -> Result<bool, StoreError> {
1379 self.delete_sync(hash)
1380 }
1381
1382 async fn stats(&self) -> StoreStats {
1383 PoolStore::stats(self).unwrap_or_default()
1384 }
1385
1386 async fn pin(&self, hash: &Hash) -> Result<(), StoreError> {
1387 self.pin_sync(hash)
1388 }
1389
1390 async fn unpin(&self, hash: &Hash) -> Result<(), StoreError> {
1391 self.unpin_sync(hash)
1392 }
1393
1394 fn pin_count(&self, hash: &Hash) -> u32 {
1395 self.pin_count_sync(hash).unwrap_or(0)
1396 }
1397}
1398
1399fn encode_manifest(manifest: &PoolManifest) -> Result<Vec<u8>, StoreError> {
1400 rmp_serde::to_vec_named(manifest)
1401 .map_err(|error| StoreError::Other(format!("encode pool manifest: {error}")))
1402}
1403
1404fn decode_manifest(bytes: &[u8]) -> Result<PoolManifest, StoreError> {
1405 let manifest: PoolManifest = rmp_serde::from_slice(bytes)
1406 .map_err(|error| StoreError::Other(format!("decode pool manifest: {error}")))?;
1407 if manifest.version != 1 {
1408 return Err(StoreError::Other(format!(
1409 "unsupported pool manifest version {}",
1410 manifest.version
1411 )));
1412 }
1413 Ok(manifest)
1414}
1415
1416fn member_hash_key(member: PoolMemberId, hash: Hash) -> [u8; 48] {
1417 let mut key = [0u8; 48];
1418 key[..16].copy_from_slice(member.as_bytes());
1419 key[16..].copy_from_slice(&hash);
1420 key
1421}
1422
1423fn decode_pin_count(bytes: &[u8]) -> Result<u32, StoreError> {
1424 Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
1425 StoreError::Other("invalid pool pin count".into())
1426 })?))
1427}
1428
1429fn unix_timestamp_now() -> u64 {
1430 SystemTime::now()
1431 .duration_since(UNIX_EPOCH)
1432 .unwrap_or_default()
1433 .as_secs()
1434}
1435
1436fn put_many_report(
1437 total: usize,
1438 ordered: &[(Hash, u64)],
1439 inserted: &HashSet<Hash>,
1440) -> PutManyReport {
1441 let mut report = PutManyReport {
1442 total,
1443 ..PutManyReport::default()
1444 };
1445 for (hash, bytes) in ordered {
1446 if inserted.contains(hash) {
1447 report.inserted = report.inserted.saturating_add(1);
1448 report.inserted_bytes = report.inserted_bytes.saturating_add(*bytes);
1449 report.inserted_hashes.push(*hash);
1450 }
1451 }
1452 report
1453}
1454
1455fn map_heed(error: heed::Error) -> StoreError {
1456 StoreError::Other(error.to_string())
1457}