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