1mod adaptive;
2mod catalog;
3mod delete_protection;
4mod gate;
5mod maintenance;
6mod maintenance_batch;
7mod maintenance_move;
8mod map_size;
9#[cfg(test)]
10mod maintenance_tests;
11mod member;
12mod model;
13mod move_catalog;
14mod read_only;
15mod reader;
16mod temperature;
17mod temperature_balancer;
18mod temperature_catalog;
19mod temperature_worker;
20#[cfg(test)]
21mod tests;
22
23use self::adaptive::AdaptivePoolState;
24pub use self::delete_protection::{PoolDeleteProtectionGuard, POOL_DELETE_PROTECTED};
25use self::gate::ConcurrencyGate;
26use self::member::{open_member_store, prepare_member_paths, validate_member_config};
27use self::model::{LocationRecord, MemberRecord, PoolManifest, MIN_MEMBER_MAP_SIZE_BYTES};
28pub use self::model::{
29 PoolDeleteProtectionChange, PoolDeleteProtectionStatus, PoolMaintenanceReport,
30 PoolMemberConfig, PoolMemberId, PoolMemberRuntimePaths, PoolMemberState, PoolMemberStatus,
31 PoolStalePending, PoolStalePendingCleanupReport, PoolStoreConfig, PoolTemperatureConfig,
32 PoolTemperatureReport,
33};
34use self::move_catalog::{
35 move_cleanup_state_key, move_state_key, rebuild_move_cleanup_member_index_txn,
36 validate_move_cleanup_member_index,
37};
38pub use self::read_only::{
39 ReadOnlyPoolCatalogAudit, ReadOnlyPoolManifestMember, ReadOnlyPoolManifestSnapshot,
40 ReadOnlyPoolStore,
41};
42pub use self::reader::{
43 PoolCatalogLocation, PoolManifestIdentity, PoolPhysicalAudit, PoolReadBatchItem,
44 PoolStoreReader, PoolTerminalAudit,
45};
46use self::temperature::TemperatureRuntime;
47use self::temperature_worker::TemperatureWorker;
48use crate::{managed_env::ManagedEnv, pinned_lmdb_data_len, LmdbBlobStore};
49use async_trait::async_trait;
50use hashtree_core::store::{slice_blob_range, PutManyReport, Store, StoreError, StoreStats};
51use hashtree_core::{sha256, to_hex, types::Hash};
52use heed::types::{Bytes, Unit};
53use heed::{Database, EnvOpenOptions};
54use std::collections::{HashMap, HashSet};
55use std::fs;
56use std::ops::Deref;
57use std::path::{Path, PathBuf};
58use std::sync::atomic::{AtomicU64, Ordering};
59use std::sync::{Arc, Mutex, RwLock};
60use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
61
62const CATALOG_DATABASES: u32 = 6;
63const CATALOG_MAX_READERS: u32 = 1024;
64const MANIFEST_KEY: &[u8] = b"pool-manifest-v1";
65const MEMBER_MARKER_NAME: &str = ".hashtree-pool-member-v1";
66const EXTERNAL_MARKER_NAME: &str = ".hashtree-pool-external-v1";
67const MAX_STALE_PENDING_CLEANUP_ITEMS: usize = 4_096;
68
69fn align_catalog_map_size_bytes(bytes: u64) -> Result<u64, StoreError> {
70 let page_size = (page_size::get() as u64).max(4_096);
71 let remainder = bytes % page_size;
72 if remainder == 0 {
73 Ok(bytes)
74 } else {
75 bytes
76 .checked_add(page_size - remainder)
77 .ok_or_else(|| StoreError::Other("pool catalog map size overflows alignment".into()))
78 }
79}
80
81#[derive(Default)]
82struct RuntimeMembers {
83 generation: Option<u64>,
84 stores: HashMap<PoolMemberId, Arc<LmdbBlobStore>>,
85 read_gates: HashMap<PoolMemberId, Arc<ConcurrencyGate>>,
86 write_gates: HashMap<PoolMemberId, Arc<ConcurrencyGate>>,
87 errors: HashMap<PoolMemberId, String>,
88}
89
90#[derive(Clone)]
91pub struct PoolStore {
92 inner: Arc<PoolStoreInner>,
93}
94
95#[doc(hidden)]
96pub struct PoolStoreInner {
97 env: ManagedEnv,
98 catalog_path: PathBuf,
99 manifest_db: Database<Bytes, Bytes>,
100 locations: Database<Bytes, Bytes>,
101 by_member: Database<Bytes, Unit>,
102 pins: Database<Bytes, Bytes>,
103 last_accessed: Database<Bytes, Bytes>,
104 temperature_state: Database<Bytes, Bytes>,
105 runtime: RwLock<RuntimeMembers>,
106 adaptive: Mutex<AdaptivePoolState>,
107 temperature_config: PoolTemperatureConfig,
108 temperature: Mutex<TemperatureRuntime>,
109 temperature_access_counter: AtomicU64,
110 temperature_owner: PoolMemberId,
111 temperature_cycle: Mutex<()>,
112 temperature_worker: TemperatureWorker,
113 member_runtime_paths: HashMap<PoolMemberId, PoolMemberRuntimePaths>,
114 expected_manifest_sha256: Option<Hash>,
115 physical_space: Option<crate::PhysicalSpaceGuard>,
116}
117
118impl Deref for PoolStore {
119 type Target = PoolStoreInner;
120
121 fn deref(&self) -> &Self::Target {
122 &self.inner
123 }
124}
125
126fn validate_new_member_paths(
127 manifest: &PoolManifest,
128 config: &PoolMemberConfig,
129) -> Result<(), StoreError> {
130 if manifest
131 .members
132 .iter()
133 .any(|member| member.config.path == config.path)
134 {
135 return Err(StoreError::Other(format!(
136 "pool member path is already configured: {}",
137 config.path.display()
138 )));
139 }
140 if let Some(external) = config.external_blob_dir.as_ref() {
141 if manifest
142 .members
143 .iter()
144 .any(|member| member.config.external_blob_dir.as_ref() == Some(external))
145 {
146 return Err(StoreError::Other(format!(
147 "pool external blob path is already configured: {}",
148 external.display()
149 )));
150 }
151 }
152 Ok(())
153}
154
155fn open_required_catalog_database<K: 'static, D: 'static>(
156 env: &ManagedEnv,
157 txn: &heed::RoTxn<'_>,
158 name: &str,
159) -> Result<Database<K, D>, StoreError> {
160 env.open_database(txn, Some(name))
161 .map_err(map_heed)?
162 .ok_or_else(|| StoreError::Other(format!("pool {name} database is missing")))
163}
164
165fn validate_controlled_manifest(
166 bytes: &[u8],
167 expected_sha256: Hash,
168 bindings: &HashMap<PoolMemberId, PoolMemberRuntimePaths>,
169) -> Result<PoolManifest, StoreError> {
170 let actual_sha256 = sha256(bytes);
171 if actual_sha256 != expected_sha256 {
172 return Err(StoreError::Other(format!(
173 "live pool manifest SHA-256 differs from controlled authority: expected {}, found {}",
174 to_hex(&expected_sha256),
175 to_hex(&actual_sha256)
176 )));
177 }
178 let manifest = decode_manifest(bytes)?;
179 validate_controlled_manifest_members(&manifest, bindings)?;
180 Ok(manifest)
181}
182
183fn configured_paths_match(pinned: &Path, live: &Path) -> bool {
184 pinned == live
185 || fs::canonicalize(pinned)
186 .ok()
187 .zip(fs::canonicalize(live).ok())
188 .is_some_and(|(pinned, live)| pinned == live)
189}
190
191fn configured_optional_paths_match(pinned: Option<&Path>, live: Option<&Path>) -> bool {
192 match (pinned, live) {
193 (Some(pinned), Some(live)) => configured_paths_match(pinned, live),
194 (None, None) => true,
195 _ => false,
196 }
197}
198
199fn validate_controlled_manifest_members(
200 manifest: &PoolManifest,
201 bindings: &HashMap<PoolMemberId, PoolMemberRuntimePaths>,
202) -> Result<(), StoreError> {
203 if bindings.len() != manifest.members.len() {
204 return Err(StoreError::Other(format!(
205 "pinned pool topology has {} members, live manifest has {}",
206 bindings.len(),
207 manifest.members.len()
208 )));
209 }
210 for member in &manifest.members {
211 let binding = bindings.get(&member.id).ok_or_else(|| {
212 StoreError::Other(format!(
213 "live pool member {} is absent from pinned topology",
214 member.id
215 ))
216 })?;
217 if !configured_paths_match(&binding.configured_path, &member.config.path)
218 || !configured_optional_paths_match(
219 binding.configured_external_path.as_deref(),
220 member.config.external_blob_dir.as_deref(),
221 )
222 {
223 return Err(StoreError::Other(format!(
224 "live pool member {} paths differ from pinned topology",
225 member.id
226 )));
227 }
228 if !member.config.external_blob_sync {
229 return Err(StoreError::Other(format!(
230 "controlled migration requires external_blob_sync=true for pool member {}",
231 member.id
232 )));
233 }
234 }
235 Ok(())
236}
237
238impl PoolStore {
239 pub fn open<P: AsRef<Path>>(path: P, config: PoolStoreConfig) -> Result<Self, StoreError> {
240 config.temperature.validate()?;
241 let mut member_runtime_paths = HashMap::new();
242 for binding in &config.member_runtime_paths {
243 if member_runtime_paths
244 .insert(binding.id, binding.clone())
245 .is_some()
246 {
247 return Err(StoreError::Other(format!(
248 "duplicate runtime path binding for pool member {}",
249 binding.id
250 )));
251 }
252 if binding.configured_external_path.is_some() != binding.runtime_external_path.is_some()
253 {
254 return Err(StoreError::Other(format!(
255 "incomplete external runtime path binding for pool member {}",
256 binding.id
257 )));
258 }
259 }
260 let controlled = !member_runtime_paths.is_empty()
261 || config.catalog_lmdb_identity.is_some()
262 || config.expected_manifest_sha256.is_some();
263 if controlled
264 && (member_runtime_paths.is_empty()
265 || config.catalog_lmdb_identity.is_none()
266 || config.expected_manifest_sha256.is_none())
267 {
268 return Err(StoreError::Other(
269 "controlled Pool open requires catalog identity, exact manifest SHA-256, and every member runtime binding".into(),
270 ));
271 }
272 let path = path.as_ref();
273 if !controlled {
274 if let Some(guard) = &config.physical_space {
275 guard.create_dir_all(path)?;
276 } else {
277 fs::create_dir_all(path).map_err(StoreError::Io)?;
278 }
279 }
280 let existing_size = match config.catalog_lmdb_identity {
281 Some(identity) => pinned_lmdb_data_len(path, identity)?,
282 None => fs::metadata(path.join("data.mdb"))
283 .map(|metadata| metadata.len())
284 .unwrap_or(0),
285 };
286 let requested = if controlled {
287 config.catalog_map_size_bytes.max(MIN_MEMBER_MAP_SIZE_BYTES)
288 } else {
289 config
290 .catalog_map_size_bytes
291 .max(existing_size.saturating_add(existing_size / 10))
292 .max(MIN_MEMBER_MAP_SIZE_BYTES)
293 };
294 let requested = align_catalog_map_size_bytes(requested)?;
295 let map_size = usize::try_from(requested)
296 .map_err(|_| StoreError::Other("pool catalog map size exceeds usize".into()))?;
297
298 let mut options = EnvOpenOptions::new();
299 if let Some(guard) = &config.physical_space {
300 guard.install(&mut options)?;
301 }
302 if !controlled {
303 options.map_size(map_size);
304 }
305 options
306 .max_dbs(CATALOG_DATABASES)
307 .max_readers(CATALOG_MAX_READERS);
308 unsafe {
309 options.flags(super::env_flags_from_env());
310 }
311 let env = unsafe {
312 match config.catalog_lmdb_identity {
313 Some(identity) => ManagedEnv::open_pinned(&options, path, identity),
314 None => ManagedEnv::open(&options, path),
315 }
316 }
317 .map_err(|error| {
318 StoreError::Other(format!("open pool catalog {}: {error}", path.display()))
319 })?;
320 if controlled && env.info().map_size < map_size {
321 return Err(StoreError::Other(format!(
322 "controlled Pool catalog map is {} bytes, below its manifest-owned {} bytes; pre-size it before exact migration",
323 env.info().map_size,
324 map_size
325 )));
326 }
327 if !controlled {
328 let _ = env.clear_stale_readers();
329 }
330 let (manifest_db, locations, by_member, pins, last_accessed, temperature_state) =
331 if controlled {
332 let rtxn = env.read_txn().map_err(map_heed)?;
333 let manifest_db: Database<Bytes, Bytes> =
334 open_required_catalog_database(&env, &rtxn, "manifest")?;
335 let locations: Database<Bytes, Bytes> =
336 open_required_catalog_database(&env, &rtxn, "locations")?;
337 let by_member: Database<Bytes, Unit> =
338 open_required_catalog_database(&env, &rtxn, "by_member")?;
339 let pins: Database<Bytes, Bytes> =
340 open_required_catalog_database(&env, &rtxn, "pins")?;
341 let last_accessed: Database<Bytes, Bytes> =
342 open_required_catalog_database(&env, &rtxn, "last_accessed")?;
343 let temperature_state: Database<Bytes, Bytes> =
344 open_required_catalog_database(&env, &rtxn, "temperature_state")?;
345 let manifest_bytes = manifest_db
346 .get(&rtxn, MANIFEST_KEY)
347 .map_err(map_heed)?
348 .ok_or_else(|| StoreError::Other("pool manifest is missing".into()))?;
349 validate_controlled_manifest(
350 manifest_bytes,
351 config
352 .expected_manifest_sha256
353 .expect("controlled manifest identity checked above"),
354 &member_runtime_paths,
355 )?;
356 rtxn.commit().map_err(map_heed)?;
357 (
358 manifest_db,
359 locations,
360 by_member,
361 pins,
362 last_accessed,
363 temperature_state,
364 )
365 } else {
366 let mut wtxn = env.write_txn().map_err(map_heed)?;
367 let manifest_db = env
368 .create_database(&mut wtxn, Some("manifest"))
369 .map_err(map_heed)?;
370 let locations = env
371 .create_database(&mut wtxn, Some("locations"))
372 .map_err(map_heed)?;
373 let by_member = env
374 .create_database(&mut wtxn, Some("by_member"))
375 .map_err(map_heed)?;
376 let pins = env
377 .create_database(&mut wtxn, Some("pins"))
378 .map_err(map_heed)?;
379 let last_accessed = env
380 .create_database(&mut wtxn, Some("last_accessed"))
381 .map_err(map_heed)?;
382 let temperature_state = env
383 .create_database(&mut wtxn, Some("temperature_state"))
384 .map_err(map_heed)?;
385 if manifest_db
386 .get(&wtxn, MANIFEST_KEY)
387 .map_err(map_heed)?
388 .is_none()
389 {
390 let bytes = encode_manifest(&PoolManifest::default())?;
391 manifest_db
392 .put(&mut wtxn, MANIFEST_KEY, bytes.as_slice())
393 .map_err(map_heed)?;
394 }
395 wtxn.commit().map_err(map_heed)?;
396 (
397 manifest_db,
398 locations,
399 by_member,
400 pins,
401 last_accessed,
402 temperature_state,
403 )
404 };
405 if !controlled && env.info().map_size < map_size {
406 unsafe { env.resize(map_size) }.map_err(map_heed)?;
407 }
408
409 if controlled {
410 validate_move_cleanup_member_index(&temperature_state, &by_member, &env)?;
411 } else {
412 let mut wtxn = env.write_txn().map_err(map_heed)?;
413 let cleanup_rebuild_started = Instant::now();
414 let cleanup_rebuild_count =
415 rebuild_move_cleanup_member_index_txn(&temperature_state, &by_member, &mut wtxn)?;
416 let cleanup_rebuild_elapsed = cleanup_rebuild_started.elapsed();
417 wtxn.commit().map_err(map_heed)?;
418 if cleanup_rebuild_count > 0 || cleanup_rebuild_elapsed >= Duration::from_millis(10) {
419 eprintln!(
420 "Pool cleanup ownership index rebuild: entries {cleanup_rebuild_count}, elapsed {} us",
421 cleanup_rebuild_elapsed.as_micros()
422 );
423 }
424 }
425
426 let mut temperature_config = config.temperature.clone();
427 if config.physical_space.is_some() {
430 temperature_config.enabled = false;
431 }
432 let store = Self {
433 inner: Arc::new(PoolStoreInner {
434 env,
435 catalog_path: path.to_path_buf(),
436 manifest_db,
437 locations,
438 by_member,
439 pins,
440 last_accessed,
441 temperature_state,
442 runtime: RwLock::new(RuntimeMembers::default()),
443 adaptive: Mutex::new(AdaptivePoolState::new(config.member_failure_cooldown)),
444 temperature: Mutex::new(TemperatureRuntime::new(
445 temperature_config.candidate_capacity,
446 )),
447 temperature_config,
448 temperature_access_counter: AtomicU64::new(0),
449 temperature_owner: PoolMemberId::new(),
450 temperature_cycle: Mutex::new(()),
451 temperature_worker: TemperatureWorker::default(),
452 member_runtime_paths,
453 expected_manifest_sha256: config.expected_manifest_sha256,
454 physical_space: config.physical_space,
455 }),
456 };
457 store.refresh_members()?;
458 store.start_temperature_worker()?;
459 Ok(store)
460 }
461
462 fn start_temperature_worker(&self) -> Result<(), StoreError> {
463 if !self.temperature_config.enabled {
464 return Ok(());
465 }
466 self.temperature_worker.start(
467 Arc::downgrade(&self.inner),
468 self.temperature_config.interval,
469 )
470 }
471
472 pub fn stop_temperature_worker(&self) -> Result<(), StoreError> {
478 self.temperature_worker.stop()
479 }
480
481 pub fn add_member(&self, config: PoolMemberConfig) -> Result<PoolMemberId, StoreError> {
482 self.add_member_inner(config, false)?
483 .ok_or_else(|| StoreError::Other("pool member was not added".into()))
484 }
485
486 pub(crate) fn ensure_initial_member(
487 &self,
488 config: PoolMemberConfig,
489 ) -> Result<Option<PoolMemberId>, StoreError> {
490 if !self.read_manifest()?.members.is_empty() {
491 self.refresh_members()?;
492 return Ok(None);
493 }
494 self.add_member_inner(config, true)
495 }
496
497 fn add_member_inner(
498 &self,
499 config: PoolMemberConfig,
500 only_if_empty: bool,
501 ) -> Result<Option<PoolMemberId>, StoreError> {
502 if !self.member_runtime_paths.is_empty() {
503 return Err(StoreError::Other(
504 "cannot change pool membership while exact runtime paths are pinned".into(),
505 ));
506 }
507 validate_member_config(&config)?;
508 let wtxn = self.env.write_txn().map_err(map_heed)?;
509 let manifest = self.manifest_from_txn(&wtxn)?;
510 if only_if_empty && !manifest.members.is_empty() {
511 drop(wtxn);
512 self.refresh_members()?;
513 return Ok(None);
514 }
515 validate_new_member_paths(&manifest, &config)?;
516 let id = prepare_member_paths(&config, PoolMemberId::new(), self.physical_space.as_ref())?;
517 drop(wtxn);
518 let store = Arc::new(open_member_store(
519 id,
520 &config,
521 None,
522 self.physical_space.as_ref(),
523 )?);
524
525 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
526 let mut manifest = self.manifest_from_txn(&wtxn)?;
527 if only_if_empty && !manifest.members.is_empty() {
528 drop(wtxn);
529 self.refresh_members()?;
530 return Ok(None);
531 }
532 validate_new_member_paths(&manifest, &config)?;
533 if manifest.members.iter().any(|member| member.id == id) {
534 return Err(StoreError::Other(format!(
535 "pool member identity is already configured: {id}"
536 )));
537 }
538 manifest.members.push(MemberRecord {
539 id,
540 state: PoolMemberState::Active,
541 config,
542 });
543 manifest.generation = manifest.generation.saturating_add(1);
544 self.put_manifest_txn(&mut wtxn, &manifest)?;
545 wtxn.commit().map_err(map_heed)?;
546
547 let mut runtime = self
548 .runtime
549 .write()
550 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
551 runtime.generation = Some(manifest.generation);
552 runtime.stores.insert(id, store);
553 let member = manifest
554 .members
555 .iter()
556 .find(|member| member.id == id)
557 .expect("new pool member is in committed manifest");
558 runtime.read_gates.insert(
559 id,
560 Arc::new(ConcurrencyGate::new(member.config.max_read_concurrency)),
561 );
562 runtime.write_gates.insert(
563 id,
564 Arc::new(ConcurrencyGate::new(member.config.max_write_concurrency)),
565 );
566 runtime.errors.remove(&id);
567 Ok(Some(id))
568 }
569
570 pub fn begin_drain(&self, id: PoolMemberId) -> Result<(), StoreError> {
571 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
572 let mut manifest = self.manifest_from_txn(&wtxn)?;
573 let has_other_active = manifest
574 .members
575 .iter()
576 .any(|member| member.id != id && member.state == PoolMemberState::Active);
577 let member = manifest
578 .members
579 .iter_mut()
580 .find(|member| member.id == id)
581 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
582 if member.state == PoolMemberState::Draining {
583 return Ok(());
584 }
585 if !has_other_active && self.member_has_locations_txn(&wtxn, id)? {
586 return Err(StoreError::Other(
587 "cannot drain the final member while it still owns blobs".into(),
588 ));
589 }
590 member.state = PoolMemberState::Draining;
591 manifest.generation = manifest.generation.saturating_add(1);
592 self.put_manifest_txn(&mut wtxn, &manifest)?;
593 wtxn.commit().map_err(map_heed)?;
594 self.refresh_members()?;
595 Ok(())
596 }
597
598 pub fn update_member_limits(
599 &self,
600 id: PoolMemberId,
601 capacity_bytes: u64,
602 max_read_concurrency: u32,
603 max_write_concurrency: u32,
604 ) -> Result<(), StoreError> {
605 if capacity_bytes == 0 || max_read_concurrency == 0 || max_write_concurrency == 0 {
606 return Err(StoreError::Other(
607 "pool member capacity and concurrency limits must be non-zero".into(),
608 ));
609 }
610 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
611 let mut manifest = self.manifest_from_txn(&wtxn)?;
612 let member = manifest
613 .members
614 .iter_mut()
615 .find(|member| member.id == id)
616 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
617 member.config.capacity_bytes = capacity_bytes;
618 member.config.max_read_concurrency = max_read_concurrency;
619 member.config.max_write_concurrency = max_write_concurrency;
620 manifest.generation = manifest.generation.saturating_add(1);
621 self.put_manifest_txn(&mut wtxn, &manifest)?;
622 wtxn.commit().map_err(map_heed)?;
623 self.refresh_members()
624 }
625
626 pub fn update_member_temperature_watermarks(
627 &self,
628 id: PoolMemberId,
629 low_percent: u8,
630 high_percent: u8,
631 ) -> Result<(), StoreError> {
632 if low_percent >= high_percent || high_percent > 100 {
633 return Err(StoreError::Other(
634 "pool temperature watermarks must satisfy 0 <= low < high <= 100".into(),
635 ));
636 }
637 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
638 let mut manifest = self.manifest_from_txn(&wtxn)?;
639 let member = manifest
640 .members
641 .iter_mut()
642 .find(|member| member.id == id)
643 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
644 member.config.temperature_low_watermark_percent = low_percent;
645 member.config.temperature_high_watermark_percent = high_percent;
646 manifest.generation = manifest.generation.saturating_add(1);
647 self.put_manifest_txn(&mut wtxn, &manifest)?;
648 wtxn.commit().map_err(map_heed)?;
649 self.refresh_members()
650 }
651
652 pub fn remove_member(&self, id: PoolMemberId) -> Result<(), StoreError> {
653 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
654 let mut manifest = self.manifest_from_txn(&wtxn)?;
655 let index = manifest
656 .members
657 .iter()
658 .position(|member| member.id == id)
659 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
660 if manifest.members[index].state != PoolMemberState::Draining {
661 return Err(StoreError::Other(
662 "pool member must be draining before removal".into(),
663 ));
664 }
665 if self.member_has_locations_txn(&wtxn, id)? {
666 return Err(StoreError::Other(format!(
667 "pool member {id} still owns blob(s)"
668 )));
669 }
670 manifest.members.remove(index);
671 manifest.generation = manifest.generation.saturating_add(1);
672 self.put_manifest_txn(&mut wtxn, &manifest)?;
673 wtxn.commit().map_err(map_heed)?;
674 self.refresh_members()?;
675 Ok(())
676 }
677
678 pub fn member(&self, id: PoolMemberId) -> Result<PoolMemberStatus, StoreError> {
679 self.refresh_members()?;
680 let manifest = self.read_manifest()?;
681 let member = manifest
682 .members
683 .iter()
684 .find(|member| member.id == id)
685 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
686 let located_blobs = self.count_member_locations(id)?;
687 let runtime = self
688 .runtime
689 .read()
690 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
691 let (logical_bytes, available, last_error) = match runtime.stores.get(&id) {
692 Some(store) => match store.stats() {
693 Ok(stats) => (stats.total_bytes, true, None),
694 Err(error) => (0, false, Some(error.to_string())),
695 },
696 None => (0, false, runtime.errors.get(&id).cloned()),
697 };
698 Ok(PoolMemberStatus {
699 id,
700 state: member.state,
701 path: member.config.path.clone(),
702 capacity_bytes: member.config.capacity_bytes,
703 map_size_bytes: member.config.map_size_bytes,
704 external_blob_dir: member.config.external_blob_dir.clone(),
705 external_blob_min_bytes: member.config.external_blob_min_bytes,
706 external_blob_sync: member.config.external_blob_sync,
707 external_pack_target_bytes: member.config.external_pack_target_bytes,
708 max_read_concurrency: member.config.max_read_concurrency,
709 max_write_concurrency: member.config.max_write_concurrency,
710 temperature_low_watermark_percent: member.config.temperature_low_watermark_percent,
711 temperature_high_watermark_percent: member.config.temperature_high_watermark_percent,
712 logical_bytes,
713 located_blobs,
714 available,
715 last_error,
716 })
717 }
718
719 pub fn members(&self) -> Result<Vec<PoolMemberStatus>, StoreError> {
720 let manifest = self.read_manifest()?;
721 manifest
722 .members
723 .iter()
724 .map(|member| self.member(member.id))
725 .collect()
726 }
727
728 pub fn blob_location(&self, hash: &Hash) -> Result<Option<PoolMemberId>, StoreError> {
729 Ok(self
730 .read_location(hash)?
731 .map(LocationRecord::preferred_member))
732 }
733
734 pub fn put_sync(&self, hash: Hash, data: &[u8]) -> Result<bool, StoreError> {
735 if sha256(data) != hash {
736 return Err(StoreError::Other(
737 "pool rejected bytes that do not match their hash".into(),
738 ));
739 }
740
741 if let Some(location) = self.read_location(&hash)? {
742 match self.read_verified_location(&hash, location) {
743 Ok(Some(found)) => {
744 if matches!(location, LocationRecord::Pending { .. }) {
745 self.finalize_pending(hash, location)?;
746 }
747 debug_assert_eq!(sha256(&found), hash);
748 return Ok(false);
749 }
750 Ok(None) | Err(_) => return self.repair_location(hash, data, location),
751 }
752 }
753
754 let target = self.choose_write_member(data.len() as u64, None)?;
755 let pending = LocationRecord::Pending {
756 member: target,
757 size: data.len() as u64,
758 };
759 let location = self.reserve_if_absent(hash, pending)?;
760 if location != pending {
761 match self.read_verified_location(&hash, location) {
762 Ok(Some(found)) => {
763 if matches!(location, LocationRecord::Pending { .. }) {
764 self.finalize_pending(hash, location)?;
765 }
766 debug_assert_eq!(sha256(&found), hash);
767 return Ok(false);
768 }
769 Ok(None) | Err(_) => return self.repair_location(hash, data, location),
770 }
771 }
772
773 let target = location.preferred_member();
774 let store = self.get_member(target)?;
775 match self.write_verified_member(target, &store, hash, data) {
776 Ok(inserted) => {
777 self.finalize_pending(hash, location)?;
778 Ok(inserted)
779 }
780 Err(error)
781 if self
782 .physical_space
783 .as_ref()
784 .is_some_and(|guard| guard.stop_after_write_error(&error)) =>
785 {
786 return Err(self.physical_space.as_ref().unwrap().refusal_error().into())
787 }
788 Err(_) => {
789 let mut excluded = HashSet::new();
790 excluded.insert(target);
791 self.repair_location_excluding(hash, data, location, excluded)
792 }
793 }
794 }
795
796 pub fn put_many_report_sync(
797 &self,
798 items: &[(Hash, Vec<u8>)],
799 ) -> Result<PutManyReport, StoreError> {
800 self.put_many_report_sync_with_existing_verification(items, true)
801 }
802
803 pub fn put_many_optimistic_report_sync(
807 &self,
808 items: &[(Hash, Vec<u8>)],
809 ) -> Result<PutManyReport, StoreError> {
810 self.put_many_report_sync_with_existing_verification(items, false)
811 }
812
813 fn put_many_report_sync_with_existing_verification(
814 &self,
815 items: &[(Hash, Vec<u8>)],
816 verify_existing: bool,
817 ) -> Result<PutManyReport, StoreError> {
818 let mut seen = HashSet::new();
819 let mut unique = Vec::with_capacity(items.len());
820 let mut ordered = Vec::with_capacity(items.len());
821 for (hash, data) in items {
822 if sha256(data) != *hash {
823 return Err(StoreError::Other(
824 "pool rejected batch bytes that do not match their hash".into(),
825 ));
826 }
827 if seen.insert(*hash) {
828 unique.push((*hash, data));
829 ordered.push((*hash, data.len() as u64));
830 }
831 }
832
833 let mut inserted = HashSet::new();
834 let mut missing = Vec::new();
835 for (hash, data) in unique {
836 if let Some(location) = self.read_location(&hash)? {
837 if (verify_existing || matches!(location, LocationRecord::Pending { .. }))
838 && self.put_sync(hash, data)?
839 {
840 inserted.insert(hash);
841 }
842 } else {
843 missing.push((hash, data));
844 }
845 }
846 if missing.is_empty() {
847 return Ok(put_many_report(items.len(), &ordered, &inserted));
848 }
849
850 let mut reserved_bytes = HashMap::new();
851 let mut assignments = Vec::with_capacity(missing.len());
852 for (hash, data) in missing {
853 let target =
854 self.choose_write_member_with_reserved(data.len() as u64, None, &reserved_bytes)?;
855 let reserved = reserved_bytes.entry(target).or_insert(0u64);
856 *reserved = reserved.saturating_add(data.len() as u64);
857 assignments.push((hash, data, target));
858 }
859
860 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
861 let mut plans = Vec::with_capacity(assignments.len());
862 let mut raced = Vec::new();
863 for (hash, data, target) in assignments {
864 if self
865 .locations
866 .get(&wtxn, &hash)
867 .map_err(map_heed)?
868 .is_some()
869 {
870 raced.push((hash, data));
871 continue;
872 }
873 let pending = LocationRecord::Pending {
874 member: target,
875 size: data.len() as u64,
876 };
877 self.set_location_txn(&mut wtxn, hash, Some(pending))?;
878 plans.push((hash, data, target, pending));
879 }
880 wtxn.commit().map_err(map_heed)?;
881
882 for (hash, data) in raced {
883 if self.put_sync(hash, data)? {
884 inserted.insert(hash);
885 }
886 }
887
888 let mut by_target: HashMap<PoolMemberId, Vec<(Hash, &[u8])>> = HashMap::new();
889 for (hash, data, target, _) in &plans {
890 by_target
891 .entry(*target)
892 .or_default()
893 .push((*hash, data.as_slice()));
894 }
895 for (target, batch) in by_target {
896 let store = self.get_member(target)?;
897 let gate = self.member_gate(target, true)?;
898 let permit = gate.acquire()?;
899 for (hash, _) in &batch {
900 if store
901 .get_sync(hash)?
902 .is_some_and(|existing| sha256(&existing) != *hash)
903 {
904 store.delete_sync(hash)?;
905 }
906 }
907 let started = Instant::now();
908 let result = store.put_many_refs_report_sync(&batch);
909 let success = result.is_ok();
910 let bytes = batch.iter().map(|(_, data)| data.len()).sum::<usize>();
911 self.record_write(target, started.elapsed(), bytes, success);
912 let report = match result {
913 Ok(report) => report,
914 Err(error)
915 if self
916 .physical_space
917 .as_ref()
918 .is_some_and(|guard| guard.stop_after_write_error(&error)) =>
919 {
920 return Err(self.physical_space.as_ref().unwrap().refusal_error().into())
921 }
922 Err(_) => {
923 drop(permit);
924 for (hash, data) in batch {
925 if self.put_sync(hash, data)? {
926 inserted.insert(hash);
927 }
928 }
929 continue;
930 }
931 };
932 inserted.extend(report.inserted_hashes);
933 for (hash, _) in &batch {
934 self.read_verified_member(target, &store, hash)?
935 .ok_or_else(|| {
936 StoreError::Other(format!(
937 "pool member {target} lost a committed batch write"
938 ))
939 })?;
940 }
941 }
942
943 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
944 for (hash, _, target, pending) in plans {
945 let Some(current) = self.locations.get(&wtxn, &hash).map_err(map_heed)? else {
946 continue;
947 };
948 if LocationRecord::decode(current)? == pending {
949 self.set_location_txn(
950 &mut wtxn,
951 hash,
952 Some(LocationRecord::Stored {
953 member: target,
954 size: pending.size(),
955 }),
956 )?;
957 }
958 }
959 wtxn.commit().map_err(map_heed)?;
960 Ok(put_many_report(items.len(), &ordered, &inserted))
961 }
962
963 pub fn put_many_sync(&self, items: &[(Hash, Vec<u8>)]) -> Result<usize, StoreError> {
964 self.put_many_report_sync(items)
965 .map(|report| report.inserted)
966 }
967
968 pub fn put_many_optimistic_sync(&self, items: &[(Hash, Vec<u8>)]) -> Result<usize, StoreError> {
969 self.put_many_optimistic_report_sync(items)
970 .map(|report| report.inserted)
971 }
972
973 pub fn get_sync(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
974 let Some(location) = self.read_location(hash)? else {
975 return Ok(None);
976 };
977 let data = self.read_verified_location(hash, location)?;
978 if data.is_some() && matches!(location, LocationRecord::Pending { .. }) {
979 self.finalize_pending(*hash, location)?;
980 }
981 if data.is_some() {
982 self.sample_temperature_access(*hash, location);
983 }
984 Ok(data)
985 }
986
987 pub fn get_range_sync(
988 &self,
989 hash: &Hash,
990 start: u64,
991 end_inclusive: u64,
992 ) -> Result<Option<Vec<u8>>, StoreError> {
993 let Some(data) = self.get_sync(hash)? else {
994 return Ok(None);
995 };
996 slice_blob_range(&data, start, end_inclusive).map(Some)
997 }
998
999 pub fn blob_size_sync(&self, hash: &Hash) -> Result<Option<u64>, StoreError> {
1000 Ok(self.read_location(hash)?.map(LocationRecord::size))
1001 }
1002
1003 pub fn exists(&self, hash: &Hash) -> Result<bool, StoreError> {
1004 Ok(self.get_sync(hash)?.is_some())
1005 }
1006
1007 pub fn existing_hashes_in_sorted_candidates(
1008 &self,
1009 sorted_hashes: &[Hash],
1010 ) -> Result<Vec<bool>, StoreError> {
1011 let rtxn = self.env.read_txn().map_err(map_heed)?;
1012 sorted_hashes
1013 .iter()
1014 .map(|hash| self.locations.get(&rtxn, hash).map(|value| value.is_some()))
1015 .collect::<Result<Vec<_>, _>>()
1016 .map_err(map_heed)
1017 }
1018
1019 pub fn committed_hashes_in_sorted_candidates(
1026 &self,
1027 sorted_hashes: &[Hash],
1028 ) -> Result<Vec<bool>, StoreError> {
1029 let rtxn = self.env.read_txn().map_err(map_heed)?;
1030 sorted_hashes
1031 .iter()
1032 .map(|hash| {
1033 self.locations
1034 .get(&rtxn, hash)
1035 .map_err(map_heed)?
1036 .map(LocationRecord::decode)
1037 .transpose()
1038 .map(|location| {
1039 location.is_some_and(|location| {
1040 !matches!(location, LocationRecord::Pending { .. })
1041 })
1042 })
1043 })
1044 .collect()
1045 }
1046
1047 pub fn catalog_locations_in_sorted_candidates(
1054 &self,
1055 sorted_hashes: &[Hash],
1056 ) -> Result<Vec<PoolCatalogLocation>, StoreError> {
1057 let rtxn = self.env.read_txn().map_err(map_heed)?;
1058 sorted_hashes
1059 .iter()
1060 .map(|hash| {
1061 let record = self
1062 .locations
1063 .get(&rtxn, hash)
1064 .map_err(map_heed)?
1065 .map(LocationRecord::decode)
1066 .transpose()?;
1067 Ok(PoolCatalogLocation::from_record(record))
1068 })
1069 .collect()
1070 }
1071
1072 pub fn scan_catalog_locations_after(
1080 &self,
1081 after: Option<Hash>,
1082 limit: usize,
1083 ) -> Result<Vec<(Hash, PoolCatalogLocation)>, StoreError> {
1084 if limit == 0 {
1085 return Ok(Vec::new());
1086 }
1087 let rtxn = self.env.read_txn().map_err(map_heed)?;
1088 let mut entries = Vec::with_capacity(limit);
1089 let decode =
1090 |hash: &[u8], encoded: &[u8]| -> Result<(Hash, PoolCatalogLocation), StoreError> {
1091 let hash: Hash = hash
1092 .try_into()
1093 .map_err(|_| StoreError::Other("invalid pool catalog hash length".into()))?;
1094 let record = LocationRecord::decode(encoded)?;
1095 Ok((hash, PoolCatalogLocation::from_record(Some(record))))
1096 };
1097 match after {
1098 Some(after) => {
1099 use std::ops::Bound;
1100 let range = (Bound::Excluded(after.as_slice()), Bound::<&[u8]>::Unbounded);
1101 for item in self.locations.range(&rtxn, &range).map_err(map_heed)? {
1102 let (hash, encoded) = item.map_err(map_heed)?;
1103 entries.push(decode(hash, encoded)?);
1104 if entries.len() >= limit {
1105 break;
1106 }
1107 }
1108 }
1109 None => {
1110 for item in self.locations.iter(&rtxn).map_err(map_heed)? {
1111 let (hash, encoded) = item.map_err(map_heed)?;
1112 entries.push(decode(hash, encoded)?);
1113 if entries.len() >= limit {
1114 break;
1115 }
1116 }
1117 }
1118 }
1119 Ok(entries)
1120 }
1121
1122 pub fn largest_member_map_size_bytes(&self) -> Result<Option<usize>, StoreError> {
1127 self.refresh_members()?;
1128 let runtime = self
1129 .runtime
1130 .read()
1131 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1132 Ok(runtime
1133 .stores
1134 .values()
1135 .map(|store| store.map_size_bytes())
1136 .max())
1137 }
1138
1139 pub fn delete_sync(&self, hash: &Hash) -> Result<bool, StoreError> {
1140 let _coordination = self.acquire_delete_coordination_lock(false)?;
1141 self.require_deletes_unprotected()?;
1142 let Some(location) = self.read_location(hash)? else {
1143 return Ok(false);
1144 };
1145 let (members, len) = location.members();
1146 let mut deleted = false;
1147 for member in members.into_iter().take(len) {
1148 if let Ok(store) = self.get_member(member) {
1149 let gate = self.member_gate(member, true)?;
1150 let _permit = gate.acquire()?;
1151 deleted |= store.delete_sync(hash)?;
1152 }
1153 }
1154 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
1155 self.set_location_txn(&mut wtxn, *hash, None)?;
1156 self.pins.delete(&mut wtxn, hash).map_err(map_heed)?;
1157 wtxn.commit().map_err(map_heed)?;
1158 Ok(deleted)
1159 }
1160
1161 pub fn delete_many_sync(&self, hashes: &[Hash]) -> Result<usize, StoreError> {
1164 if hashes.is_empty() {
1165 return Ok(0);
1166 }
1167 let _coordination = self.acquire_delete_coordination_lock(false)?;
1168 self.require_deletes_unprotected()?;
1169 let rtxn = self.env.read_txn().map_err(map_heed)?;
1170 let mut seen = HashSet::with_capacity(hashes.len());
1171 let mut located = Vec::new();
1172 let mut by_member = HashMap::<PoolMemberId, Vec<Hash>>::new();
1173 for hash in hashes {
1174 if !seen.insert(*hash) {
1175 continue;
1176 }
1177 let Some(encoded) = self.locations.get(&rtxn, hash).map_err(map_heed)? else {
1178 continue;
1179 };
1180 let location = LocationRecord::decode(encoded)?;
1181 let (members, len) = location.members();
1182 for member in members.into_iter().take(len) {
1183 by_member.entry(member).or_default().push(*hash);
1184 }
1185 located.push(*hash);
1186 }
1187 drop(rtxn);
1188
1189 for (member, member_hashes) in by_member {
1190 if let Ok(store) = self.get_member(member) {
1191 let gate = self.member_gate(member, true)?;
1192 let _permit = gate.acquire()?;
1193 store.delete_many_sync(&member_hashes)?;
1194 }
1195 }
1196
1197 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
1198 for hash in &located {
1199 self.set_location_txn(&mut wtxn, *hash, None)?;
1200 self.pins.delete(&mut wtxn, hash).map_err(map_heed)?;
1201 }
1202 wtxn.commit().map_err(map_heed)?;
1203 Ok(located.len())
1204 }
1205
1206 pub fn cleanup_stale_pending_exact_offline_sync(
1222 &self,
1223 expected: &[PoolStalePending],
1224 ) -> Result<PoolStalePendingCleanupReport, StoreError> {
1225 if expected.is_empty() || expected.len() > MAX_STALE_PENDING_CLEANUP_ITEMS {
1226 return Err(StoreError::Other(format!(
1227 "stale Pending cleanup requires 1..={MAX_STALE_PENDING_CLEANUP_ITEMS} exact records"
1228 )));
1229 }
1230 if self.expected_manifest_sha256.is_none() || self.member_runtime_paths.is_empty() {
1231 return Err(StoreError::Other(
1232 "stale Pending cleanup requires a controlled Pool open".into(),
1233 ));
1234 }
1235 if self.temperature_config.enabled {
1236 return Err(StoreError::Other(
1237 "stale Pending cleanup requires temperature tracking to be disabled".into(),
1238 ));
1239 }
1240 if expected.windows(2).any(|pair| pair[0].hash >= pair[1].hash) {
1241 return Err(StoreError::Other(
1242 "stale Pending cleanup records must be strictly hash-sorted and unique".into(),
1243 ));
1244 }
1245 let declared_bytes = expected.iter().try_fold(0u64, |total, item| {
1246 total.checked_add(item.size).ok_or_else(|| {
1247 StoreError::Other("stale Pending declared byte total overflow".into())
1248 })
1249 })?;
1250
1251 self.validate_controlled_authority_and_sync()?;
1252
1253 let mut member_ids = expected.iter().map(|item| item.member).collect::<Vec<_>>();
1254 member_ids.sort_unstable();
1255 member_ids.dedup();
1256 let member_resources = member_ids
1257 .iter()
1258 .map(|member| {
1259 Ok((
1260 *member,
1261 self.get_member(*member)?,
1262 self.member_gate(*member, true)?,
1263 ))
1264 })
1265 .collect::<Result<Vec<_>, StoreError>>()?;
1266 let _exclusive_permits = member_resources
1267 .iter()
1268 .map(|(_, _, gate)| gate.acquire_exclusive())
1269 .collect::<Result<Vec<_>, StoreError>>()?;
1270
1271 let ensure_physically_absent = || -> Result<(), StoreError> {
1272 for (member, store, _) in &member_resources {
1273 let hashes = expected
1274 .iter()
1275 .filter(|item| item.member == *member)
1276 .map(|item| item.hash)
1277 .collect::<Vec<_>>();
1278 let present = store.existing_hashes_in_sorted_candidates(&hashes)?;
1279 if let Some(hash) = hashes
1280 .iter()
1281 .zip(present)
1282 .find_map(|(hash, present)| present.then_some(hash))
1283 {
1284 return Err(StoreError::Other(format!(
1285 "stale Pending cleanup rejected physically present member record {} on {member}",
1286 to_hex(hash)
1287 )));
1288 }
1289 }
1290 Ok(())
1291 };
1292 ensure_physically_absent()?;
1293
1294 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
1295 let mut exact_pending = 0usize;
1296 let mut already_missing = 0usize;
1297 for item in expected {
1298 let current = self
1299 .locations
1300 .get(&wtxn, &item.hash)
1301 .map_err(map_heed)?
1302 .map(LocationRecord::decode)
1303 .transpose()?;
1304 let authorized = LocationRecord::Pending {
1305 member: item.member,
1306 size: item.size,
1307 };
1308 match current {
1309 Some(current) if current == authorized => {
1310 exact_pending = exact_pending.saturating_add(1);
1311 if self
1312 .by_member
1313 .get(&wtxn, &member_hash_key(item.member, item.hash))
1314 .map_err(map_heed)?
1315 .is_none()
1316 {
1317 return Err(StoreError::Other(format!(
1318 "stale Pending cleanup found a missing member index for {}",
1319 to_hex(&item.hash)
1320 )));
1321 }
1322 }
1323 None => {
1324 already_missing = already_missing.saturating_add(1);
1325 if self
1326 .by_member
1327 .get(&wtxn, &member_hash_key(item.member, item.hash))
1328 .map_err(map_heed)?
1329 .is_some()
1330 || self
1331 .last_accessed
1332 .get(&wtxn, &item.hash)
1333 .map_err(map_heed)?
1334 .is_some()
1335 {
1336 return Err(StoreError::Other(format!(
1337 "already-cleared stale Pending record {} retains catalog indexes",
1338 to_hex(&item.hash)
1339 )));
1340 }
1341 }
1342 current => {
1343 return Err(StoreError::Other(format!(
1344 "stale Pending authority no longer matches {}: expected {authorized:?}, found {current:?}",
1345 to_hex(&item.hash)
1346 )));
1347 }
1348 }
1349 let pin_count = self
1350 .pins
1351 .get(&wtxn, &item.hash)
1352 .map_err(map_heed)?
1353 .map(decode_pin_count)
1354 .transpose()?
1355 .unwrap_or(0);
1356 if pin_count != 0 {
1357 return Err(StoreError::Other(format!(
1358 "stale Pending cleanup rejected pinned hash {}",
1359 to_hex(&item.hash)
1360 )));
1361 }
1362 if self
1363 .temperature_state
1364 .get(&wtxn, &move_state_key(item.hash))
1365 .map_err(map_heed)?
1366 .is_some()
1367 || self
1368 .temperature_state
1369 .get(&wtxn, &move_cleanup_state_key(item.hash))
1370 .map_err(map_heed)?
1371 .is_some()
1372 {
1373 return Err(StoreError::Other(format!(
1374 "stale Pending cleanup rejected move-owned hash {}",
1375 to_hex(&item.hash)
1376 )));
1377 }
1378 }
1379 if exact_pending != 0 && already_missing != 0 {
1380 return Err(StoreError::Other(
1381 "stale Pending cleanup refuses a partially changed authority set".into(),
1382 ));
1383 }
1384
1385 ensure_physically_absent()?;
1388 if exact_pending != 0 {
1389 for item in expected {
1390 self.set_location_txn(&mut wtxn, item.hash, None)?;
1391 self.pins.delete(&mut wtxn, &item.hash).map_err(map_heed)?;
1392 }
1393 }
1394 wtxn.commit().map_err(map_heed)?;
1395 self.validate_controlled_authority_and_sync()?;
1396
1397 let rtxn = self.env.read_txn().map_err(map_heed)?;
1398 for item in expected {
1399 if self
1400 .locations
1401 .get(&rtxn, &item.hash)
1402 .map_err(map_heed)?
1403 .is_some()
1404 || self
1405 .by_member
1406 .get(&rtxn, &member_hash_key(item.member, item.hash))
1407 .map_err(map_heed)?
1408 .is_some()
1409 || self
1410 .pins
1411 .get(&rtxn, &item.hash)
1412 .map_err(map_heed)?
1413 .is_some()
1414 || self
1415 .last_accessed
1416 .get(&rtxn, &item.hash)
1417 .map_err(map_heed)?
1418 .is_some()
1419 {
1420 return Err(StoreError::Other(format!(
1421 "stale Pending cleanup did not durably clear {}",
1422 to_hex(&item.hash)
1423 )));
1424 }
1425 }
1426 rtxn.commit().map_err(map_heed)?;
1427 Ok(PoolStalePendingCleanupReport {
1428 requested: expected.len(),
1429 declared_bytes,
1430 already_cleaned: exact_pending == 0,
1431 })
1432 }
1433
1434 pub fn pin_sync(&self, hash: &Hash) -> Result<(), StoreError> {
1435 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
1436 let previous = self
1437 .pins
1438 .get(&wtxn, hash)
1439 .map_err(map_heed)?
1440 .map(decode_pin_count)
1441 .transpose()?
1442 .unwrap_or(0);
1443 self.pins
1444 .put(&mut wtxn, hash, &previous.saturating_add(1).to_be_bytes())
1445 .map_err(map_heed)?;
1446 wtxn.commit().map_err(map_heed)
1447 }
1448
1449 pub fn unpin_sync(&self, hash: &Hash) -> Result<(), StoreError> {
1450 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
1451 let count = self
1452 .pins
1453 .get(&wtxn, hash)
1454 .map_err(map_heed)?
1455 .map(decode_pin_count)
1456 .transpose()?
1457 .unwrap_or(0);
1458 if count <= 1 {
1459 self.pins.delete(&mut wtxn, hash).map_err(map_heed)?;
1460 } else {
1461 self.pins
1462 .put(&mut wtxn, hash, &(count - 1).to_be_bytes())
1463 .map_err(map_heed)?;
1464 }
1465 wtxn.commit().map_err(map_heed)
1466 }
1467
1468 pub fn pin_count_sync(&self, hash: &Hash) -> Result<u32, StoreError> {
1469 let rtxn = self.env.read_txn().map_err(map_heed)?;
1470 self.pins
1471 .get(&rtxn, hash)
1472 .map_err(map_heed)?
1473 .map(decode_pin_count)
1474 .transpose()
1475 .map(|count| count.unwrap_or(0))
1476 }
1477
1478 pub fn touch_accessed_sync(&self, hash: &Hash, now: u64) -> Result<bool, StoreError> {
1479 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
1480 if self.locations.get(&wtxn, hash).map_err(map_heed)?.is_none() {
1481 return Ok(false);
1482 }
1483 let previous = self
1484 .last_accessed
1485 .get(&wtxn, hash)
1486 .map_err(map_heed)?
1487 .and_then(self::temperature::AccessRecord::decode)
1488 .unwrap_or_else(|| self::temperature::AccessRecord::new(now));
1489 let access = self::temperature::AccessRecord {
1490 last_accessed_at: now,
1491 ..previous
1492 }
1493 .encode();
1494 self.last_accessed
1495 .put(&mut wtxn, hash, &access)
1496 .map_err(map_heed)?;
1497 wtxn.commit().map_err(map_heed)?;
1498 Ok(true)
1499 }
1500
1501 pub fn touch_many_accessed_sync(&self, hashes: &[Hash], now: u64) -> Result<usize, StoreError> {
1502 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
1503 let mut updated = 0usize;
1504 let mut seen = HashSet::new();
1505 for hash in hashes {
1506 if !seen.insert(*hash) || self.locations.get(&wtxn, hash).map_err(map_heed)?.is_none() {
1507 continue;
1508 }
1509 let previous = self
1510 .last_accessed
1511 .get(&wtxn, hash)
1512 .map_err(map_heed)?
1513 .and_then(self::temperature::AccessRecord::decode)
1514 .unwrap_or_else(|| self::temperature::AccessRecord::new(now));
1515 let access = self::temperature::AccessRecord {
1516 last_accessed_at: now,
1517 ..previous
1518 }
1519 .encode();
1520 self.last_accessed
1521 .put(&mut wtxn, hash, &access)
1522 .map_err(map_heed)?;
1523 updated += 1;
1524 }
1525 wtxn.commit().map_err(map_heed)?;
1526 Ok(updated)
1527 }
1528
1529 pub fn last_accessed_at_sync(&self, hash: &Hash) -> Result<Option<u64>, StoreError> {
1530 let rtxn = self.env.read_txn().map_err(map_heed)?;
1531 self.last_accessed
1532 .get(&rtxn, hash)
1533 .map_err(map_heed)?
1534 .map(|bytes| {
1535 self::temperature::AccessRecord::decode(bytes)
1536 .map(|access| access.last_accessed_at)
1537 .ok_or_else(|| StoreError::Other("invalid pool access record".into()))
1538 })
1539 .transpose()
1540 }
1541
1542 pub fn many_last_accessed_at_sync(
1543 &self,
1544 hashes: &[Hash],
1545 ) -> Result<Vec<(Hash, u64)>, StoreError> {
1546 let rtxn = self.env.read_txn().map_err(map_heed)?;
1547 let mut values = Vec::new();
1548 for hash in hashes {
1549 if let Some(value) = self.last_accessed.get(&rtxn, hash).map_err(map_heed)? {
1550 let access = self::temperature::AccessRecord::decode(value)
1551 .ok_or_else(|| StoreError::Other("invalid pool access record".into()))?;
1552 values.push((*hash, access.last_accessed_at));
1553 }
1554 }
1555 Ok(values)
1556 }
1557
1558 pub fn list(&self) -> Result<Vec<Hash>, StoreError> {
1559 let rtxn = self.env.read_txn().map_err(map_heed)?;
1560 let mut hashes = Vec::new();
1561 for item in self.locations.iter(&rtxn).map_err(map_heed)? {
1562 let (hash, _) = item.map_err(map_heed)?;
1563 let hash: Hash = hash
1564 .try_into()
1565 .map_err(|_| StoreError::Other("invalid pool hash key".into()))?;
1566 hashes.push(hash);
1567 }
1568 Ok(hashes)
1569 }
1570
1571 pub fn stats(&self) -> Result<StoreStats, StoreError> {
1572 let rtxn = self.env.read_txn().map_err(map_heed)?;
1573 let mut stats = StoreStats::default();
1574 for item in self.locations.iter(&rtxn).map_err(map_heed)? {
1575 let (_, location) = item.map_err(map_heed)?;
1576 let location = LocationRecord::decode(location)?;
1577 stats.count = stats.count.saturating_add(1);
1578 stats.bytes = stats.bytes.saturating_add(location.size());
1579 }
1580 for item in self.pins.iter(&rtxn).map_err(map_heed)? {
1581 let (hash, count) = item.map_err(map_heed)?;
1582 if decode_pin_count(count)? == 0 {
1583 continue;
1584 }
1585 let Some(location) = self.locations.get(&rtxn, hash).map_err(map_heed)? else {
1586 continue;
1587 };
1588 stats.pinned_count = stats.pinned_count.saturating_add(1);
1589 stats.pinned_bytes = stats
1590 .pinned_bytes
1591 .saturating_add(LocationRecord::decode(location)?.size());
1592 }
1593 Ok(stats)
1594 }
1595
1596 pub fn writable_physical_stats(&self) -> Result<StoreStats, StoreError> {
1606 self.refresh_members()?;
1607 let manifest = self.read_manifest()?;
1608 let runtime = self
1609 .runtime
1610 .read()
1611 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1612 let mut total = StoreStats::default();
1613 for member in manifest.members {
1614 let store = runtime.stores.get(&member.id).ok_or_else(|| {
1615 let detail = runtime
1616 .errors
1617 .get(&member.id)
1618 .map(String::as_str)
1619 .unwrap_or("member store is not open");
1620 StoreError::Other(format!(
1621 "pool member {} unavailable for physical quota accounting: {detail}",
1622 member.id
1623 ))
1624 })?;
1625 let stats = store.stats().map_err(|error| {
1626 StoreError::Other(format!(
1627 "pool member {} unavailable for physical quota accounting: {error}",
1628 member.id
1629 ))
1630 })?;
1631 let count = u64::try_from(stats.count)
1632 .map_err(|_| StoreError::Other("pool physical blob count exceeds u64".into()))?;
1633 let pinned_count = u64::try_from(stats.pinned_count)
1634 .map_err(|_| StoreError::Other("pool physical pin count exceeds u64".into()))?;
1635 total.count = total
1636 .count
1637 .checked_add(count)
1638 .ok_or_else(|| StoreError::Other("pool physical blob count overflow".into()))?;
1639 total.bytes = total
1640 .bytes
1641 .checked_add(stats.total_bytes)
1642 .ok_or_else(|| StoreError::Other("pool physical byte count overflow".into()))?;
1643 total.pinned_count = total
1644 .pinned_count
1645 .checked_add(pinned_count)
1646 .ok_or_else(|| StoreError::Other("pool physical pin count overflow".into()))?;
1647 total.pinned_bytes = total
1648 .pinned_bytes
1649 .checked_add(stats.pinned_bytes)
1650 .ok_or_else(|| StoreError::Other("pool physical pinned bytes overflow".into()))?;
1651 }
1652 Ok(total)
1653 }
1654
1655 pub fn force_sync(&self) -> Result<(), StoreError> {
1656 self.env.force_sync().map_err(map_heed)?;
1657 self.refresh_members()?;
1658 let runtime = self
1659 .runtime
1660 .read()
1661 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1662 for store in runtime.stores.values() {
1663 store.force_sync()?;
1664 }
1665 Ok(())
1666 }
1667
1668 pub fn catalog_map_size_bytes(&self) -> u64 {
1670 u64::try_from(self.env.info().map_size).unwrap_or(u64::MAX)
1671 }
1672
1673 pub fn validate_controlled_member_state_exact(&self) -> Result<(), StoreError> {
1681 if self.expected_manifest_sha256.is_none() {
1682 return Err(StoreError::Other(
1683 "exact controlled member validation requires an exact manifest SHA-256".into(),
1684 ));
1685 }
1686 self.refresh_members()?;
1687 let manifest = self.read_manifest()?;
1688 let runtime = self
1689 .runtime
1690 .read()
1691 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1692 if runtime.stores.len() != manifest.members.len() {
1693 return Err(StoreError::Other(
1694 "not every authority-pinned Pool member is open for exact validation".into(),
1695 ));
1696 }
1697 for member in manifest.members {
1698 let store = runtime.stores.get(&member.id).ok_or_else(|| {
1699 StoreError::Other(format!(
1700 "controlled Pool member {} is unavailable for exact validation",
1701 member.id
1702 ))
1703 })?;
1704 store.validate_exact_member_state().map_err(|error| {
1705 StoreError::Other(format!(
1706 "controlled Pool member {} failed exact state validation: {error}",
1707 member.id
1708 ))
1709 })?;
1710 }
1711 Ok(())
1712 }
1713
1714 pub fn validate_controlled_authority(&self) -> Result<(), StoreError> {
1722 if self.expected_manifest_sha256.is_none() {
1723 return Err(StoreError::Other(
1724 "controlled authority validation requires an exact manifest SHA-256".into(),
1725 ));
1726 }
1727 self.refresh_members()?;
1728 {
1729 let runtime = self
1730 .runtime
1731 .read()
1732 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1733 if runtime.stores.len() != self.member_runtime_paths.len() {
1734 return Err(StoreError::Other(
1735 "not every authority-pinned Pool member is open".into(),
1736 ));
1737 }
1738 }
1739 self.refresh_members()
1740 }
1741
1742 pub fn validate_controlled_authority_and_sync(&self) -> Result<(), StoreError> {
1745 self.validate_controlled_authority()?;
1746 self.env.force_sync().map_err(map_heed)?;
1747 {
1748 let runtime = self
1749 .runtime
1750 .read()
1751 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1752 for store in runtime.stores.values() {
1753 store.force_sync()?;
1754 }
1755 }
1756 self.validate_controlled_authority()
1757 }
1758
1759 fn refresh_members(&self) -> Result<(), StoreError> {
1760 let (manifest, manifest_sha256) = self.read_manifest_with_identity()?;
1761 if let Some(expected) = self.expected_manifest_sha256 {
1762 if manifest_sha256 != expected {
1763 return Err(StoreError::Other(format!(
1764 "live pool manifest SHA-256 differs from controlled authority: expected {}, found {}",
1765 to_hex(&expected),
1766 to_hex(&manifest_sha256)
1767 )));
1768 }
1769 validate_controlled_manifest_members(&manifest, &self.member_runtime_paths)?;
1770 }
1771 let mut runtime = self
1772 .runtime
1773 .write()
1774 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1775 if runtime.generation == Some(manifest.generation) {
1776 return Ok(());
1777 }
1778
1779 let configured = manifest
1780 .members
1781 .iter()
1782 .map(|member| member.id)
1783 .collect::<HashSet<_>>();
1784 runtime.stores.retain(|id, _| configured.contains(id));
1785 runtime.read_gates.retain(|id, _| configured.contains(id));
1786 runtime.write_gates.retain(|id, _| configured.contains(id));
1787 runtime.errors.retain(|id, _| configured.contains(id));
1788 for member in &manifest.members {
1789 let read_gate = runtime
1790 .read_gates
1791 .entry(member.id)
1792 .or_insert_with(|| {
1793 Arc::new(ConcurrencyGate::new(member.config.max_read_concurrency))
1794 })
1795 .clone();
1796 read_gate.set_limit(member.config.max_read_concurrency)?;
1797 let write_gate = runtime
1798 .write_gates
1799 .entry(member.id)
1800 .or_insert_with(|| {
1801 Arc::new(ConcurrencyGate::new(member.config.max_write_concurrency))
1802 })
1803 .clone();
1804 write_gate.set_limit(member.config.max_write_concurrency)?;
1805 if runtime.stores.contains_key(&member.id) {
1806 continue;
1807 }
1808 let config = self.runtime_member_config(member)?;
1809 let pinned_identity = self
1810 .member_runtime_paths
1811 .get(&member.id)
1812 .map(|binding| binding.lmdb_identity);
1813 match open_member_store(
1814 member.id,
1815 &config,
1816 pinned_identity,
1817 self.physical_space.as_ref(),
1818 ) {
1819 Ok(store) => {
1820 runtime.stores.insert(member.id, Arc::new(store));
1821 runtime.errors.remove(&member.id);
1822 }
1823 Err(error) => {
1824 if self.expected_manifest_sha256.is_some() {
1825 return Err(StoreError::Other(format!(
1826 "controlled Pool member {} failed its exact open: {error}",
1827 member.id
1828 )));
1829 }
1830 runtime.errors.insert(member.id, error.to_string());
1831 }
1832 }
1833 }
1834 runtime.generation = Some(manifest.generation);
1835 drop(runtime);
1836 self.adaptive
1837 .lock()
1838 .map_err(|_| StoreError::Other("pool adaptive lock poisoned".into()))?
1839 .retain(&configured);
1840 Ok(())
1841 }
1842
1843 fn runtime_member_config(&self, member: &MemberRecord) -> Result<PoolMemberConfig, StoreError> {
1844 let Some(binding) = self.member_runtime_paths.get(&member.id) else {
1845 if self.member_runtime_paths.is_empty() {
1846 return Ok(member.config.clone());
1847 }
1848 return Err(StoreError::Other(format!(
1849 "pool member {} has no pinned runtime path",
1850 member.id
1851 )));
1852 };
1853 if !configured_paths_match(&binding.configured_path, &member.config.path)
1854 || !configured_optional_paths_match(
1855 binding.configured_external_path.as_deref(),
1856 member.config.external_blob_dir.as_deref(),
1857 )
1858 {
1859 return Err(StoreError::Other(format!(
1860 "pool member {} paths differ from pinned runtime authority",
1861 member.id
1862 )));
1863 }
1864 let mut config = member.config.clone();
1865 config.path = binding.runtime_path.clone();
1866 config.external_blob_dir = binding.runtime_external_path.clone();
1867 Ok(config)
1868 }
1869
1870 fn get_member(&self, id: PoolMemberId) -> Result<Arc<LmdbBlobStore>, StoreError> {
1871 self.refresh_members()?;
1872 if let Some(store) = self
1873 .runtime
1874 .read()
1875 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?
1876 .stores
1877 .get(&id)
1878 .cloned()
1879 {
1880 return Ok(store);
1881 }
1882
1883 let member = self
1884 .read_manifest()?
1885 .members
1886 .into_iter()
1887 .find(|member| member.id == id)
1888 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))?;
1889 let config = self.runtime_member_config(&member)?;
1890 let pinned_identity = self
1891 .member_runtime_paths
1892 .get(&id)
1893 .map(|binding| binding.lmdb_identity);
1894 match open_member_store(id, &config, pinned_identity, self.physical_space.as_ref()) {
1895 Ok(store) => {
1896 let store = Arc::new(store);
1897 let mut runtime = self
1898 .runtime
1899 .write()
1900 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1901 runtime.stores.insert(id, Arc::clone(&store));
1902 runtime.errors.remove(&id);
1903 Ok(store)
1904 }
1905 Err(error) => {
1906 self.record_member_failure(id, false);
1907 let mut runtime = self
1908 .runtime
1909 .write()
1910 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1911 runtime.errors.insert(id, error.to_string());
1912 Err(error)
1913 }
1914 }
1915 }
1916
1917 fn member_state(&self, id: PoolMemberId) -> Result<Option<PoolMemberState>, StoreError> {
1918 Ok(self
1919 .read_manifest()?
1920 .members
1921 .into_iter()
1922 .find(|member| member.id == id)
1923 .map(|member| member.state))
1924 }
1925
1926 fn member_gate(
1927 &self,
1928 id: PoolMemberId,
1929 write: bool,
1930 ) -> Result<Arc<ConcurrencyGate>, StoreError> {
1931 self.refresh_members()?;
1932 let runtime = self
1933 .runtime
1934 .read()
1935 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1936 let gates = if write {
1937 &runtime.write_gates
1938 } else {
1939 &runtime.read_gates
1940 };
1941 gates
1942 .get(&id)
1943 .cloned()
1944 .ok_or_else(|| StoreError::Other(format!("unknown pool member {id}")))
1945 }
1946
1947 fn choose_write_member(
1948 &self,
1949 incoming_bytes: u64,
1950 exclude: Option<PoolMemberId>,
1951 ) -> Result<PoolMemberId, StoreError> {
1952 self.choose_write_member_with_reserved(incoming_bytes, exclude, &HashMap::new())
1953 }
1954
1955 fn choose_write_member_with_reserved(
1956 &self,
1957 incoming_bytes: u64,
1958 exclude: Option<PoolMemberId>,
1959 reserved_bytes: &HashMap<PoolMemberId, u64>,
1960 ) -> Result<PoolMemberId, StoreError> {
1961 let excluded = exclude.into_iter().collect::<HashSet<_>>();
1962 self.choose_write_member_excluding(incoming_bytes, &excluded, reserved_bytes)
1963 }
1964
1965 fn choose_write_member_excluding(
1966 &self,
1967 incoming_bytes: u64,
1968 excluded: &HashSet<PoolMemberId>,
1969 reserved_bytes: &HashMap<PoolMemberId, u64>,
1970 ) -> Result<PoolMemberId, StoreError> {
1971 self.refresh_members()?;
1972 let manifest = self.read_manifest()?;
1973 let runtime = self
1974 .runtime
1975 .read()
1976 .map_err(|_| StoreError::Other("pool runtime lock poisoned".into()))?;
1977 let mut candidates = Vec::new();
1978 let mut below_high_watermark = Vec::new();
1979 for member in manifest.members.iter().filter(|member| {
1980 member.state == PoolMemberState::Active && !excluded.contains(&member.id)
1981 }) {
1982 let Some(store) = runtime.stores.get(&member.id) else {
1983 continue;
1984 };
1985 let stats = match store.stats() {
1986 Ok(stats) => stats,
1987 Err(_) => continue,
1988 };
1989 let effective_bytes = stats
1990 .total_bytes
1991 .saturating_add(reserved_bytes.get(&member.id).copied().unwrap_or(0));
1992 if member.config.capacity_bytes > 0
1993 && effective_bytes.saturating_add(incoming_bytes) > member.config.capacity_bytes
1994 {
1995 continue;
1996 }
1997 candidates.push((member.id, effective_bytes, member.config.capacity_bytes));
1998 let projected_fill = effective_bytes
1999 .saturating_add(incoming_bytes)
2000 .saturating_mul(100)
2001 .saturating_div(member.config.capacity_bytes)
2002 .min(100);
2003 if projected_fill <= u64::from(member.config.temperature_high_watermark_percent) {
2004 below_high_watermark.push((
2005 member.id,
2006 effective_bytes,
2007 member.config.capacity_bytes,
2008 ));
2009 }
2010 }
2011 drop(runtime);
2012 if !below_high_watermark.is_empty() {
2013 candidates = below_high_watermark;
2014 }
2015 self.adaptive
2016 .lock()
2017 .map_err(|_| StoreError::Other("pool adaptive lock poisoned".into()))?
2018 .choose_write(&candidates)
2019 .ok_or_else(|| StoreError::Other("no writable pool member has capacity".into()))
2020 }
2021
2022 fn repair_location(
2023 &self,
2024 hash: Hash,
2025 data: &[u8],
2026 expected: LocationRecord,
2027 ) -> Result<bool, StoreError> {
2028 self.repair_location_excluding(hash, data, expected, HashSet::new())
2029 }
2030
2031 fn repair_location_excluding(
2032 &self,
2033 hash: Hash,
2034 data: &[u8],
2035 expected: LocationRecord,
2036 mut excluded: HashSet<PoolMemberId>,
2037 ) -> Result<bool, StoreError> {
2038 let preferred = expected.preferred_member();
2039 let mut next = (!excluded.contains(&preferred)
2040 && self.member_state(preferred)? == Some(PoolMemberState::Active))
2041 .then_some(preferred);
2042 let mut last_error = None;
2043 let (target, inserted) = loop {
2044 let target = match next.take() {
2045 Some(target) => target,
2046 None => match self.choose_write_member_excluding(
2047 data.len() as u64,
2048 &excluded,
2049 &HashMap::new(),
2050 ) {
2051 Ok(target) => target,
2052 Err(error) => {
2053 return Err(last_error.unwrap_or(error));
2054 }
2055 },
2056 };
2057 let result = self
2058 .get_member(target)
2059 .and_then(|store| self.write_verified_member(target, &store, hash, data));
2060 match result {
2061 Ok(inserted) => break (target, inserted),
2062 Err(error)
2063 if self
2064 .physical_space
2065 .as_ref()
2066 .is_some_and(|guard| guard.stop_after_write_error(&error)) =>
2067 {
2068 return Err(self.physical_space.as_ref().unwrap().refusal_error().into())
2069 }
2070 Err(error) => {
2071 excluded.insert(target);
2072 last_error = Some(error);
2073 }
2074 }
2075 };
2076 let stored = LocationRecord::Stored {
2077 member: target,
2078 size: data.len() as u64,
2079 };
2080
2081 let mut wtxn = self.env.write_txn().map_err(map_heed)?;
2082 let current = self
2083 .locations
2084 .get(&wtxn, &hash)
2085 .map_err(map_heed)?
2086 .map(LocationRecord::decode)
2087 .transpose()?;
2088 if current == Some(expected) {
2089 self.set_location_txn(&mut wtxn, hash, Some(stored))?;
2090 wtxn.commit().map_err(map_heed)?;
2091 return Ok(inserted);
2092 }
2093 drop(wtxn);
2094
2095 if let Some(current) = current {
2096 if self.read_verified_location(&hash, current)?.is_some() {
2097 return Ok(false);
2098 }
2099 }
2100 Err(StoreError::Other(format!(
2101 "pool location changed while repairing {hash:?}"
2102 )))
2103 }
2104
2105 fn read_verified_location(
2106 &self,
2107 hash: &Hash,
2108 location: LocationRecord,
2109 ) -> Result<Option<Vec<u8>>, StoreError> {
2110 let mut ids = match location {
2111 LocationRecord::Pending { member, .. } | LocationRecord::Stored { member, .. } => {
2112 vec![member]
2113 }
2114 LocationRecord::Moving { source, target, .. } => vec![target, source],
2115 };
2116 self.adaptive
2117 .lock()
2118 .map_err(|_| StoreError::Other("pool adaptive lock poisoned".into()))?
2119 .order_reads(&mut ids);
2120
2121 let mut first_error = None;
2122 for id in ids {
2123 let store = match self.get_member(id) {
2124 Ok(store) => store,
2125 Err(error) => {
2126 first_error.get_or_insert(error);
2127 continue;
2128 }
2129 };
2130 match self.read_verified_member(id, &store, hash) {
2131 Ok(Some(data)) => return Ok(Some(data)),
2132 Ok(None) => {}
2133 Err(error) => {
2134 first_error.get_or_insert(error);
2135 }
2136 }
2137 }
2138 match first_error {
2139 Some(error) => Err(error),
2140 None => Ok(None),
2141 }
2142 }
2143
2144 fn read_verified_member(
2145 &self,
2146 id: PoolMemberId,
2147 store: &LmdbBlobStore,
2148 hash: &Hash,
2149 ) -> Result<Option<Vec<u8>>, StoreError> {
2150 let gate = self.member_gate(id, false)?;
2151 let _permit = gate.acquire()?;
2152 let started = Instant::now();
2153 let result = store.get_sync(hash);
2154 match result {
2155 Ok(Some(data)) if sha256(&data) == *hash => {
2156 self.record_read(id, started.elapsed(), true);
2157 Ok(Some(data))
2158 }
2159 Ok(Some(_)) => {
2160 self.record_read(id, started.elapsed(), false);
2161 Err(StoreError::Other(format!(
2162 "pool member {id} returned corrupt bytes"
2163 )))
2164 }
2165 Ok(None) => {
2166 self.record_read(id, started.elapsed(), true);
2167 Ok(None)
2168 }
2169 Err(error) => {
2170 self.record_read(id, started.elapsed(), false);
2171 Err(error)
2172 }
2173 }
2174 }
2175
2176 fn write_verified_member(
2177 &self,
2178 id: PoolMemberId,
2179 store: &LmdbBlobStore,
2180 hash: Hash,
2181 data: &[u8],
2182 ) -> Result<bool, StoreError> {
2183 let gate = self.member_gate(id, true)?;
2184 let _permit = gate.acquire()?;
2185 if let Some(existing) = store.get_sync(&hash)? {
2186 if sha256(&existing) == hash {
2187 return Ok(false);
2188 }
2189 store.delete_sync(&hash)?;
2190 }
2191 let started = Instant::now();
2192 let result = store.put_sync(hash, data);
2193 let success = result.is_ok();
2194 self.record_write(id, started.elapsed(), data.len(), success);
2195 let inserted = result?;
2196 let written = store
2197 .get_sync(&hash)?
2198 .ok_or_else(|| StoreError::Other(format!("pool member {id} lost a committed write")))?;
2199 if sha256(&written) != hash {
2200 self.record_member_failure(id, true);
2201 return Err(StoreError::Other(format!(
2202 "pool member {id} committed corrupt bytes"
2203 )));
2204 }
2205 Ok(inserted)
2206 }
2207
2208 fn delete_member_blob(
2209 &self,
2210 id: PoolMemberId,
2211 store: &LmdbBlobStore,
2212 hash: &Hash,
2213 ) -> Result<bool, StoreError> {
2214 let gate = self.member_gate(id, true)?;
2215 let _permit = gate.acquire()?;
2216 store.delete_sync(hash)
2217 }
2218
2219 fn record_read(&self, id: PoolMemberId, elapsed: Duration, success: bool) {
2220 if let Ok(mut adaptive) = self.adaptive.lock() {
2221 adaptive.record_read(id, elapsed, success);
2222 }
2223 }
2224
2225 fn record_write(&self, id: PoolMemberId, elapsed: Duration, bytes: usize, success: bool) {
2226 if let Ok(mut adaptive) = self.adaptive.lock() {
2227 adaptive.record_write(id, elapsed, bytes, success);
2228 }
2229 }
2230
2231 fn record_member_failure(&self, id: PoolMemberId, write: bool) {
2232 if write {
2233 self.record_write(id, Duration::ZERO, 0, false);
2234 } else {
2235 self.record_read(id, Duration::ZERO, false);
2236 }
2237 }
2238
2239 fn sample_temperature_access(&self, hash: Hash, location: LocationRecord) {
2240 if !self.temperature_config.enabled {
2241 return;
2242 }
2243 let sample_rate = u64::from(self.temperature_config.read_sample_rate.max(1));
2244 let access = self
2245 .temperature_access_counter
2246 .fetch_add(1, Ordering::Relaxed)
2247 .saturating_add(1);
2248 if !access.is_multiple_of(sample_rate) {
2249 return;
2250 }
2251 if let Ok(mut temperature) = self.temperature.lock() {
2252 temperature
2253 .samples
2254 .observe(hash, location, unix_timestamp_now());
2255 }
2256 }
2257}
2258
2259#[async_trait]
2260impl Store for PoolStore {
2261 async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError> {
2262 self.put_sync(hash, &data)
2263 }
2264
2265 async fn put_many(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
2266 self.put_many_sync(&items)
2267 }
2268
2269 async fn put_many_optimistic(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
2270 self.put_many_optimistic_sync(&items)
2271 }
2272
2273 async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
2274 self.get_sync(hash)
2275 }
2276
2277 async fn get_range(
2278 &self,
2279 hash: &Hash,
2280 start: u64,
2281 end_inclusive: u64,
2282 ) -> Result<Option<Vec<u8>>, StoreError> {
2283 self.get_range_sync(hash, start, end_inclusive)
2284 }
2285
2286 async fn blob_size(&self, hash: &Hash) -> Result<Option<u64>, StoreError> {
2287 self.blob_size_sync(hash)
2288 }
2289
2290 async fn has(&self, hash: &Hash) -> Result<bool, StoreError> {
2291 self.exists(hash)
2292 }
2293
2294 async fn delete(&self, hash: &Hash) -> Result<bool, StoreError> {
2295 self.delete_sync(hash)
2296 }
2297
2298 async fn delete_many(&self, hashes: Vec<Hash>) -> Result<usize, StoreError> {
2299 self.delete_many_sync(&hashes)
2300 }
2301
2302 async fn stats(&self) -> StoreStats {
2303 PoolStore::stats(self).unwrap_or_default()
2304 }
2305
2306 async fn pin(&self, hash: &Hash) -> Result<(), StoreError> {
2307 self.pin_sync(hash)
2308 }
2309
2310 async fn unpin(&self, hash: &Hash) -> Result<(), StoreError> {
2311 self.unpin_sync(hash)
2312 }
2313
2314 fn pin_count(&self, hash: &Hash) -> u32 {
2315 self.pin_count_sync(hash).unwrap_or(0)
2316 }
2317}
2318
2319fn encode_manifest(manifest: &PoolManifest) -> Result<Vec<u8>, StoreError> {
2320 rmp_serde::to_vec_named(manifest)
2321 .map_err(|error| StoreError::Other(format!("encode pool manifest: {error}")))
2322}
2323
2324fn decode_manifest(bytes: &[u8]) -> Result<PoolManifest, StoreError> {
2325 let manifest: PoolManifest = rmp_serde::from_slice(bytes)
2326 .map_err(|error| StoreError::Other(format!("decode pool manifest: {error}")))?;
2327 if manifest.version != 1 {
2328 return Err(StoreError::Other(format!(
2329 "unsupported pool manifest version {}",
2330 manifest.version
2331 )));
2332 }
2333 Ok(manifest)
2334}
2335
2336fn member_hash_key(member: PoolMemberId, hash: Hash) -> [u8; 48] {
2337 let mut key = [0u8; 48];
2338 key[..16].copy_from_slice(member.as_bytes());
2339 key[16..].copy_from_slice(&hash);
2340 key
2341}
2342
2343fn decode_pin_count(bytes: &[u8]) -> Result<u32, StoreError> {
2344 Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
2345 StoreError::Other("invalid pool pin count".into())
2346 })?))
2347}
2348
2349fn unix_timestamp_now() -> u64 {
2350 SystemTime::now()
2351 .duration_since(UNIX_EPOCH)
2352 .unwrap_or_default()
2353 .as_secs()
2354}
2355
2356fn put_many_report(
2357 total: usize,
2358 ordered: &[(Hash, u64)],
2359 inserted: &HashSet<Hash>,
2360) -> PutManyReport {
2361 let mut report = PutManyReport {
2362 total,
2363 ..PutManyReport::default()
2364 };
2365 for (hash, bytes) in ordered {
2366 if inserted.contains(hash) {
2367 report.inserted = report.inserted.saturating_add(1);
2368 report.inserted_bytes = report.inserted_bytes.saturating_add(*bytes);
2369 report.inserted_hashes.push(*hash);
2370 }
2371 }
2372 report
2373}
2374
2375fn map_heed(error: heed::Error) -> StoreError {
2376 super::map_heed_error(error)
2377}