Skip to main content

hashtree_lmdb/pool/
mod.rs

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    /// Largest map size among members that are available in this process.
702    ///
703    /// The pool catalog is a separate LMDB environment and is intentionally
704    /// not reported as blob capacity.
705    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}