mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-10-01 06:25:34 +00:00
779 lines
25 KiB
Rust
779 lines
25 KiB
Rust
use std::collections::HashSet;
|
|
use std::fmt;
|
|
use std::path::Path;
|
|
|
|
use crate::blockstore::CID_SIZE;
|
|
use crate::blockstore::hash_index::BlockIndex;
|
|
use crate::blockstore::{DataFileId, TranquilBlockStore};
|
|
use crate::clock::{Clock, SystemClock};
|
|
use crate::eventlog::{EventLog, EventSequence, SequenceContiguityResult};
|
|
use crate::io::{RealIO, StorageIO};
|
|
use crate::metastore::Metastore;
|
|
use crate::metastore::encoding::KeyBuilder;
|
|
use crate::metastore::event_keys::metastore_cursor_key;
|
|
use crate::metastore::keys::{KeyTag, UserHash};
|
|
use crate::metastore::partitions::Partition;
|
|
use crate::metastore::records::RecordValue;
|
|
use crate::metastore::repo_meta::RepoMetaValue;
|
|
|
|
const CLEAN_SHUTDOWN_MARKER: &str = ".clean_shutdown";
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct ConsistencyReport {
|
|
pub repos_checked: u64,
|
|
pub records_checked: u64,
|
|
pub user_blocks_checked: u64,
|
|
pub handles_checked: u64,
|
|
pub dangling_record_cids: Vec<DanglingCid>,
|
|
pub dangling_root_cids: Vec<DanglingRootCid>,
|
|
pub orphaned_user_repos: Vec<OrphanedUserRepo>,
|
|
pub inconsistent_handles: Vec<InconsistentHandle>,
|
|
pub orphan_data_files: Vec<DataFileId>,
|
|
pub orphan_hint_files: Vec<DataFileId>,
|
|
pub missing_indexed_files: Vec<DataFileId>,
|
|
pub deserialization_failures: u64,
|
|
pub eventlog_contiguity: Option<SequenceContiguityResult>,
|
|
pub cursor_ahead_of_eventlog: bool,
|
|
pub metastore_cursor: Option<EventSequence>,
|
|
pub eventlog_max_seq: Option<EventSequence>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DanglingCid {
|
|
pub user_hash: UserHash,
|
|
pub collection: String,
|
|
pub rkey: String,
|
|
pub cid_bytes: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DanglingRootCid {
|
|
pub user_hash: UserHash,
|
|
pub root_cid_bytes: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct OrphanedUserRepo {
|
|
pub user_hash: UserHash,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct InconsistentHandle {
|
|
pub handle: String,
|
|
pub mapped_user_hash: UserHash,
|
|
pub problem: HandleProblem,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum HandleProblem {
|
|
NoRepoMeta,
|
|
HandleMismatch { repo_handle: String },
|
|
}
|
|
|
|
impl ConsistencyReport {
|
|
pub fn is_consistent(&self) -> bool {
|
|
self.dangling_record_cids.is_empty()
|
|
&& self.dangling_root_cids.is_empty()
|
|
&& self.orphaned_user_repos.is_empty()
|
|
&& self.inconsistent_handles.is_empty()
|
|
&& self.orphan_data_files.is_empty()
|
|
&& self.orphan_hint_files.is_empty()
|
|
&& self.missing_indexed_files.is_empty()
|
|
&& self.deserialization_failures == 0
|
|
&& self
|
|
.eventlog_contiguity
|
|
.as_ref()
|
|
.is_none_or(|c| c.is_contiguous())
|
|
&& !self.cursor_ahead_of_eventlog
|
|
}
|
|
|
|
pub fn has_repairable_issues(&self) -> bool {
|
|
!self.orphan_data_files.is_empty()
|
|
|| !self.orphan_hint_files.is_empty()
|
|
|| !self.missing_indexed_files.is_empty()
|
|
}
|
|
|
|
pub fn has_unrecoverable_issues(&self) -> bool {
|
|
!self.dangling_root_cids.is_empty()
|
|
|| !self.dangling_record_cids.is_empty()
|
|
|| self.deserialization_failures > 0
|
|
|| self.cursor_ahead_of_eventlog
|
|
}
|
|
|
|
pub fn log_findings(&self) {
|
|
if self.is_consistent() {
|
|
tracing::info!(
|
|
repos = self.repos_checked,
|
|
records = self.records_checked,
|
|
user_blocks = self.user_blocks_checked,
|
|
handles = self.handles_checked,
|
|
"consistency check passed"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if !self.dangling_record_cids.is_empty() {
|
|
tracing::warn!(
|
|
count = self.dangling_record_cids.len(),
|
|
"records reference missing blocks"
|
|
);
|
|
}
|
|
if !self.dangling_root_cids.is_empty() {
|
|
tracing::warn!(
|
|
count = self.dangling_root_cids.len(),
|
|
"repo roots reference missing blocks"
|
|
);
|
|
}
|
|
if !self.orphaned_user_repos.is_empty() {
|
|
tracing::warn!(
|
|
count = self.orphaned_user_repos.len(),
|
|
"repos with user_blocks but no repo_meta"
|
|
);
|
|
}
|
|
if !self.inconsistent_handles.is_empty() {
|
|
tracing::warn!(
|
|
count = self.inconsistent_handles.len(),
|
|
"handle index inconsistencies"
|
|
);
|
|
}
|
|
if !self.orphan_data_files.is_empty() {
|
|
tracing::warn!(
|
|
count = self.orphan_data_files.len(),
|
|
files = ?self.orphan_data_files,
|
|
"orphan data files with no index references"
|
|
);
|
|
}
|
|
if !self.orphan_hint_files.is_empty() {
|
|
tracing::warn!(
|
|
count = self.orphan_hint_files.len(),
|
|
files = ?self.orphan_hint_files,
|
|
"orphan hint files with no matching data file"
|
|
);
|
|
}
|
|
if !self.missing_indexed_files.is_empty() {
|
|
tracing::warn!(
|
|
count = self.missing_indexed_files.len(),
|
|
files = ?self.missing_indexed_files,
|
|
"index references data files that are missing on disk"
|
|
);
|
|
}
|
|
if self.deserialization_failures > 0 {
|
|
tracing::error!(
|
|
count = self.deserialization_failures,
|
|
"metastore values failed to deserialize"
|
|
);
|
|
}
|
|
if let Some(c) = &self.eventlog_contiguity
|
|
&& !c.is_contiguous()
|
|
{
|
|
tracing::warn!(gaps = c.gaps.len(), "eventlog sequence gaps detected");
|
|
c.gaps.iter().take(5).for_each(|gap| {
|
|
tracing::warn!(
|
|
after_segment = %gap.after_segment,
|
|
expected = ?gap.expected_seq,
|
|
actual = ?gap.actual_seq,
|
|
"eventlog gap"
|
|
);
|
|
});
|
|
}
|
|
if self.cursor_ahead_of_eventlog {
|
|
tracing::error!(
|
|
cursor = ?self.metastore_cursor,
|
|
eventlog_max = ?self.eventlog_max_seq,
|
|
"metastore cursor is ahead of eventlog max sequence"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ConsistencyReport {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
if self.is_consistent() {
|
|
return write!(
|
|
f,
|
|
"consistent (repos={}, records={}, user_blocks={}, handles={})",
|
|
self.repos_checked,
|
|
self.records_checked,
|
|
self.user_blocks_checked,
|
|
self.handles_checked,
|
|
);
|
|
}
|
|
|
|
write!(
|
|
f,
|
|
"INCONSISTENT: dangling_roots={}, dangling_records={}, orphaned_repos={}, \
|
|
inconsistent_handles={}, orphan_files={}, orphan_hints={}, missing_indexed_files={}, \
|
|
deserialize_failures={}, eventlog_gaps={}, cursor_ahead={}",
|
|
self.dangling_root_cids.len(),
|
|
self.dangling_record_cids.len(),
|
|
self.orphaned_user_repos.len(),
|
|
self.inconsistent_handles.len(),
|
|
self.orphan_data_files.len(),
|
|
self.orphan_hint_files.len(),
|
|
self.missing_indexed_files.len(),
|
|
self.deserialization_failures,
|
|
self.eventlog_contiguity
|
|
.as_ref()
|
|
.map_or(0, |c| c.gaps.len()),
|
|
self.cursor_ahead_of_eventlog,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct ConsistencyCheckOptions {
|
|
pub check_block_references: bool,
|
|
pub check_handles: bool,
|
|
pub check_user_blocks: bool,
|
|
pub check_eventlog: bool,
|
|
pub check_orphan_files: bool,
|
|
pub check_missing_indexed_files: bool,
|
|
pub check_orphan_hint_files: bool,
|
|
}
|
|
|
|
impl Default for ConsistencyCheckOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
check_block_references: true,
|
|
check_handles: true,
|
|
check_user_blocks: true,
|
|
check_eventlog: true,
|
|
check_orphan_files: true,
|
|
check_missing_indexed_files: true,
|
|
check_orphan_hint_files: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn verify_store_consistency<BS, BC, ES>(
|
|
blockstore: &TranquilBlockStore<BS, BC>,
|
|
metastore: &Metastore,
|
|
eventlog: &EventLog<ES>,
|
|
) -> ConsistencyReport
|
|
where
|
|
BS: StorageIO + Send + Sync + 'static,
|
|
BC: Clock,
|
|
ES: StorageIO + 'static,
|
|
{
|
|
verify_store_consistency_with_options(
|
|
blockstore,
|
|
metastore,
|
|
eventlog,
|
|
ConsistencyCheckOptions::default(),
|
|
)
|
|
}
|
|
|
|
pub fn verify_store_consistency_with_options<BS, BC, ES>(
|
|
blockstore: &TranquilBlockStore<BS, BC>,
|
|
metastore: &Metastore,
|
|
eventlog: &EventLog<ES>,
|
|
options: ConsistencyCheckOptions,
|
|
) -> ConsistencyReport
|
|
where
|
|
BS: StorageIO + Send + Sync + 'static,
|
|
BC: Clock,
|
|
ES: StorageIO + 'static,
|
|
{
|
|
let mut report = ConsistencyReport::default();
|
|
|
|
let block_index = blockstore.block_index();
|
|
let repo_data = metastore.partition(Partition::RepoData);
|
|
|
|
let known_user_hashes = if options.check_block_references {
|
|
let hashes = check_repo_root_cids(repo_data, block_index, &mut report);
|
|
check_record_cids(repo_data, block_index, &mut report);
|
|
hashes
|
|
} else if options.check_user_blocks {
|
|
collect_known_user_hashes(repo_data)
|
|
} else {
|
|
HashSet::new()
|
|
};
|
|
|
|
if options.check_user_blocks {
|
|
check_user_blocks(repo_data, &known_user_hashes, &mut report);
|
|
}
|
|
|
|
if options.check_handles {
|
|
check_handle_consistency(repo_data, &mut report);
|
|
}
|
|
|
|
if options.check_eventlog {
|
|
check_eventlog_contiguity(eventlog, &mut report);
|
|
check_cursor_vs_eventlog(repo_data, eventlog, &mut report);
|
|
}
|
|
|
|
if options.check_orphan_files {
|
|
check_orphan_data_files(blockstore, block_index, &mut report);
|
|
}
|
|
|
|
if options.check_missing_indexed_files {
|
|
check_missing_indexed_files(blockstore, block_index, &mut report);
|
|
}
|
|
|
|
if options.check_orphan_hint_files {
|
|
check_orphan_hint_files(blockstore, &mut report);
|
|
}
|
|
|
|
report
|
|
}
|
|
|
|
fn check_repo_root_cids(
|
|
repo_data: &fjall::Keyspace,
|
|
block_index: &BlockIndex,
|
|
report: &mut ConsistencyReport,
|
|
) -> HashSet<UserHash> {
|
|
let prefix = KeyBuilder::new().tag(KeyTag::REPO_META).build();
|
|
let mut known_user_hashes = HashSet::new();
|
|
|
|
repo_data.prefix(prefix.as_slice()).for_each(|guard| {
|
|
let Ok((key_bytes, value_bytes)) = guard.into_inner() else {
|
|
return;
|
|
};
|
|
|
|
report.repos_checked = report.repos_checked.saturating_add(1);
|
|
|
|
if let Some(h) = extract_user_hash(&key_bytes) {
|
|
known_user_hashes.insert(h);
|
|
}
|
|
|
|
let Some(meta) = RepoMetaValue::deserialize(&value_bytes) else {
|
|
tracing::warn!(
|
|
user_hash = ?extract_user_hash(&key_bytes),
|
|
"repo_meta value failed to deserialize"
|
|
);
|
|
report.deserialization_failures = report.deserialization_failures.saturating_add(1);
|
|
return;
|
|
};
|
|
|
|
if meta.repo_root_cid.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let Some(cid_fixed) = try_cid_bytes_to_fixed(&meta.repo_root_cid) else {
|
|
let Some(user_hash) = extract_user_hash(&key_bytes) else {
|
|
return;
|
|
};
|
|
tracing::warn!(
|
|
%user_hash,
|
|
cid_len = meta.repo_root_cid.len(),
|
|
"repo_meta has non-standard CID length"
|
|
);
|
|
report.dangling_root_cids.push(DanglingRootCid {
|
|
user_hash,
|
|
root_cid_bytes: meta.repo_root_cid,
|
|
});
|
|
return;
|
|
};
|
|
|
|
if !block_index.has(&cid_fixed) {
|
|
let Some(user_hash) = extract_user_hash(&key_bytes) else {
|
|
return;
|
|
};
|
|
report.dangling_root_cids.push(DanglingRootCid {
|
|
user_hash,
|
|
root_cid_bytes: meta.repo_root_cid,
|
|
});
|
|
}
|
|
});
|
|
|
|
known_user_hashes
|
|
}
|
|
|
|
fn collect_known_user_hashes(repo_data: &fjall::Keyspace) -> HashSet<UserHash> {
|
|
let prefix = KeyBuilder::new().tag(KeyTag::REPO_META).build();
|
|
let mut hashes = HashSet::new();
|
|
|
|
repo_data.prefix(prefix.as_slice()).for_each(|guard| {
|
|
if let Ok((key_bytes, _)) = guard.into_inner()
|
|
&& let Some(h) = extract_user_hash(&key_bytes)
|
|
{
|
|
hashes.insert(h);
|
|
}
|
|
});
|
|
|
|
hashes
|
|
}
|
|
|
|
fn check_record_cids(
|
|
repo_data: &fjall::Keyspace,
|
|
block_index: &BlockIndex,
|
|
report: &mut ConsistencyReport,
|
|
) {
|
|
let prefix = KeyBuilder::new().tag(KeyTag::RECORDS).build();
|
|
|
|
repo_data.prefix(prefix.as_slice()).for_each(|guard| {
|
|
let Ok((key_bytes, value_bytes)) = guard.into_inner() else {
|
|
return;
|
|
};
|
|
|
|
report.records_checked = report.records_checked.saturating_add(1);
|
|
|
|
let Some(record) = RecordValue::deserialize(&value_bytes) else {
|
|
tracing::warn!(
|
|
user_hash = ?extract_user_hash(&key_bytes),
|
|
"record value failed to deserialize"
|
|
);
|
|
report.deserialization_failures = report.deserialization_failures.saturating_add(1);
|
|
return;
|
|
};
|
|
|
|
let Some(cid_fixed) = try_cid_bytes_to_fixed(&record.record_cid) else {
|
|
let (user_hash, collection, rkey) = parse_record_key(&key_bytes);
|
|
let Some(user_hash) = user_hash else {
|
|
return;
|
|
};
|
|
tracing::warn!(
|
|
%user_hash,
|
|
collection,
|
|
rkey,
|
|
cid_len = record.record_cid.len(),
|
|
"record has non-standard CID length"
|
|
);
|
|
report.dangling_record_cids.push(DanglingCid {
|
|
user_hash,
|
|
collection,
|
|
rkey,
|
|
cid_bytes: record.record_cid,
|
|
});
|
|
return;
|
|
};
|
|
|
|
if !block_index.has(&cid_fixed) {
|
|
let (user_hash, collection, rkey) = parse_record_key(&key_bytes);
|
|
let Some(user_hash) = user_hash else {
|
|
return;
|
|
};
|
|
report.dangling_record_cids.push(DanglingCid {
|
|
user_hash,
|
|
collection,
|
|
rkey,
|
|
cid_bytes: record.record_cid,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
fn check_user_blocks(
|
|
repo_data: &fjall::Keyspace,
|
|
known_user_hashes: &HashSet<UserHash>,
|
|
report: &mut ConsistencyReport,
|
|
) {
|
|
let prefix = KeyBuilder::new().tag(KeyTag::USER_BLOCKS).build();
|
|
let mut seen_orphan_hashes: HashSet<UserHash> = HashSet::new();
|
|
|
|
repo_data.prefix(prefix.as_slice()).for_each(|guard| {
|
|
let Ok((key_bytes, _)) = guard.into_inner() else {
|
|
return;
|
|
};
|
|
|
|
report.user_blocks_checked = report.user_blocks_checked.saturating_add(1);
|
|
|
|
let Some(user_hash) = extract_user_hash(&key_bytes) else {
|
|
return;
|
|
};
|
|
|
|
if !known_user_hashes.contains(&user_hash) && seen_orphan_hashes.insert(user_hash) {
|
|
report
|
|
.orphaned_user_repos
|
|
.push(OrphanedUserRepo { user_hash });
|
|
}
|
|
});
|
|
}
|
|
|
|
fn check_handle_consistency(repo_data: &fjall::Keyspace, report: &mut ConsistencyReport) {
|
|
let prefix = KeyBuilder::new().tag(KeyTag::HANDLES).build();
|
|
|
|
repo_data.prefix(prefix.as_slice()).for_each(|guard| {
|
|
let Ok((key_bytes, value_bytes)) = guard.into_inner() else {
|
|
return;
|
|
};
|
|
|
|
report.handles_checked = report.handles_checked.saturating_add(1);
|
|
|
|
let handle = parse_handle_from_key(&key_bytes);
|
|
let Some(mapped_hash) = parse_user_hash_from_value(&value_bytes) else {
|
|
return;
|
|
};
|
|
|
|
let meta_key = crate::metastore::repo_meta::repo_meta_key(mapped_hash);
|
|
match repo_data.get(meta_key.as_slice()) {
|
|
Ok(Some(meta_bytes)) => {
|
|
let Some(meta) = RepoMetaValue::deserialize(&meta_bytes) else {
|
|
tracing::warn!(
|
|
%mapped_hash,
|
|
handle,
|
|
"repo_meta value failed to deserialize during handle check"
|
|
);
|
|
report.deserialization_failures =
|
|
report.deserialization_failures.saturating_add(1);
|
|
return;
|
|
};
|
|
let meta_handle_lower = meta.handle.to_lowercase();
|
|
let handle_lower = handle.to_lowercase();
|
|
if meta_handle_lower != handle_lower {
|
|
report.inconsistent_handles.push(InconsistentHandle {
|
|
handle,
|
|
mapped_user_hash: mapped_hash,
|
|
problem: HandleProblem::HandleMismatch {
|
|
repo_handle: meta.handle,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
Ok(None) => {
|
|
report.inconsistent_handles.push(InconsistentHandle {
|
|
handle,
|
|
mapped_user_hash: mapped_hash,
|
|
problem: HandleProblem::NoRepoMeta,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, handle, "repo_meta lookup failed during handle check");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn check_eventlog_contiguity<S: StorageIO + 'static>(
|
|
eventlog: &EventLog<S>,
|
|
report: &mut ConsistencyReport,
|
|
) {
|
|
let reader = eventlog.reader();
|
|
if let Err(e) = reader.refresh_segment_ranges() {
|
|
tracing::warn!(error = %e, "failed to refresh segment ranges for contiguity check");
|
|
return;
|
|
}
|
|
report.eventlog_contiguity = Some(reader.check_sequence_contiguity());
|
|
}
|
|
|
|
fn check_cursor_vs_eventlog<S: StorageIO + 'static>(
|
|
repo_data: &fjall::Keyspace,
|
|
eventlog: &EventLog<S>,
|
|
report: &mut ConsistencyReport,
|
|
) {
|
|
let cursor_key = metastore_cursor_key();
|
|
let cursor_seq = repo_data
|
|
.get(cursor_key.as_slice())
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|bytes| {
|
|
let arr: [u8; 8] = bytes.as_ref().try_into().ok()?;
|
|
Some(match u64::from_be_bytes(arr) {
|
|
0 => EventSequence::BEFORE_ALL,
|
|
n => EventSequence::new(n),
|
|
})
|
|
});
|
|
|
|
let max_seq = eventlog.max_seq();
|
|
|
|
report.metastore_cursor = cursor_seq;
|
|
report.eventlog_max_seq = (max_seq != EventSequence::BEFORE_ALL).then_some(max_seq);
|
|
|
|
if let Some(cursor) = cursor_seq
|
|
&& max_seq != EventSequence::BEFORE_ALL
|
|
&& cursor > max_seq
|
|
{
|
|
report.cursor_ahead_of_eventlog = true;
|
|
}
|
|
}
|
|
|
|
fn check_orphan_data_files<BS: StorageIO + Send + Sync + 'static, BC: Clock>(
|
|
blockstore: &TranquilBlockStore<BS, BC>,
|
|
block_index: &BlockIndex,
|
|
report: &mut ConsistencyReport,
|
|
) {
|
|
let disk_files = match blockstore.list_data_files() {
|
|
Ok(files) => files,
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "failed to list data files for orphan check");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let epoch = blockstore.epoch().current();
|
|
let now = blockstore.clock().wall_millis();
|
|
let indexed_files = block_index.liveness_by_file(epoch, now, 0);
|
|
|
|
let indexed_file_ids: HashSet<DataFileId> = indexed_files.keys().copied().collect();
|
|
|
|
let active_file_id = block_index.read_write_cursor().map(|c| c.file_id);
|
|
|
|
if active_file_id.is_none() && indexed_file_ids.is_empty() {
|
|
return;
|
|
}
|
|
|
|
disk_files.iter().for_each(|&fid| {
|
|
let is_active = active_file_id.is_some_and(|active| fid >= active);
|
|
if !is_active && !indexed_file_ids.contains(&fid) {
|
|
report.orphan_data_files.push(fid);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn check_missing_indexed_files<BS: StorageIO + Send + Sync + 'static, BC: Clock>(
|
|
blockstore: &TranquilBlockStore<BS, BC>,
|
|
block_index: &BlockIndex,
|
|
report: &mut ConsistencyReport,
|
|
) {
|
|
let disk_files: HashSet<DataFileId> = match blockstore.list_data_files() {
|
|
Ok(files) => files.into_iter().collect(),
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "failed to list data files for missing-file check");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let epoch = blockstore.epoch().current();
|
|
let now = blockstore.clock().wall_millis();
|
|
let indexed_files = block_index.liveness_by_file(epoch, now, 0);
|
|
|
|
indexed_files
|
|
.iter()
|
|
.filter(|(fid, _)| !disk_files.contains(fid))
|
|
.for_each(|(fid, _)| report.missing_indexed_files.push(*fid));
|
|
}
|
|
|
|
fn check_orphan_hint_files<BS: StorageIO + Send + Sync + 'static, BC: Clock>(
|
|
blockstore: &TranquilBlockStore<BS, BC>,
|
|
report: &mut ConsistencyReport,
|
|
) {
|
|
let data_files: HashSet<DataFileId> = match blockstore.list_data_files() {
|
|
Ok(files) => files.into_iter().collect(),
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "failed to list data files for orphan-hint check");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let hint_files = match blockstore.list_hint_files() {
|
|
Ok(files) => files,
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "failed to list hint files for orphan-hint check");
|
|
return;
|
|
}
|
|
};
|
|
|
|
hint_files
|
|
.iter()
|
|
.filter(|fid| !data_files.contains(fid))
|
|
.for_each(|fid| report.orphan_hint_files.push(*fid));
|
|
}
|
|
|
|
fn try_cid_bytes_to_fixed(bytes: &[u8]) -> Option<[u8; CID_SIZE]> {
|
|
bytes.try_into().ok()
|
|
}
|
|
|
|
fn extract_user_hash(key_bytes: &[u8]) -> Option<UserHash> {
|
|
key_bytes
|
|
.get(1..9)?
|
|
.try_into()
|
|
.ok()
|
|
.map(|arr| UserHash::from_raw(u64::from_be_bytes(arr)))
|
|
}
|
|
|
|
fn parse_record_key(key_bytes: &[u8]) -> (Option<UserHash>, String, String) {
|
|
let user_hash = extract_user_hash(key_bytes);
|
|
let mut reader = crate::metastore::encoding::KeyReader::new(key_bytes);
|
|
let _ = reader.tag();
|
|
let _ = reader.u64();
|
|
let collection = reader.string().unwrap_or_default();
|
|
let rkey = reader.string().unwrap_or_default();
|
|
(user_hash, collection, rkey)
|
|
}
|
|
|
|
fn parse_handle_from_key(key_bytes: &[u8]) -> String {
|
|
let mut reader = crate::metastore::encoding::KeyReader::new(key_bytes);
|
|
let _ = reader.tag();
|
|
reader.string().unwrap_or_default()
|
|
}
|
|
|
|
fn parse_user_hash_from_value(value_bytes: &[u8]) -> Option<UserHash> {
|
|
value_bytes
|
|
.get(..8)?
|
|
.try_into()
|
|
.ok()
|
|
.map(|arr| UserHash::from_raw(u64::from_be_bytes(arr)))
|
|
}
|
|
|
|
pub fn repair_known_issues(
|
|
blockstore: &TranquilBlockStore<RealIO, SystemClock>,
|
|
report: &ConsistencyReport,
|
|
) -> RepairResult {
|
|
let mut result = RepairResult::default();
|
|
|
|
report.orphan_data_files.iter().for_each(|&file_id| {
|
|
let path = blockstore.data_file_path(file_id);
|
|
match std::fs::remove_file(&path) {
|
|
Ok(()) => {
|
|
tracing::info!(%file_id, "removed orphan data file");
|
|
result.orphan_files_removed = result.orphan_files_removed.saturating_add(1);
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(%file_id, error = %e, "failed to remove orphan data file");
|
|
result.repair_errors = result.repair_errors.saturating_add(1);
|
|
}
|
|
}
|
|
});
|
|
|
|
report.orphan_hint_files.iter().for_each(|&file_id| {
|
|
let path = blockstore.hint_file_path(file_id);
|
|
match std::fs::remove_file(&path) {
|
|
Ok(()) => {
|
|
tracing::info!(%file_id, "removed orphan hint file");
|
|
result.orphan_hints_removed = result.orphan_hints_removed.saturating_add(1);
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(%file_id, error = %e, "failed to remove orphan hint file");
|
|
result.repair_errors = result.repair_errors.saturating_add(1);
|
|
}
|
|
}
|
|
});
|
|
|
|
report.missing_indexed_files.iter().for_each(|&file_id| {
|
|
let purged = blockstore.block_index().purge_by_file_id(file_id);
|
|
tracing::info!(
|
|
%file_id,
|
|
purged,
|
|
"purged phantom index entries for missing data file"
|
|
);
|
|
result.phantom_index_entries_purged =
|
|
result.phantom_index_entries_purged.saturating_add(purged);
|
|
});
|
|
|
|
result
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct RepairResult {
|
|
pub orphan_files_removed: u64,
|
|
pub orphan_hints_removed: u64,
|
|
pub phantom_index_entries_purged: u64,
|
|
pub repair_errors: u64,
|
|
}
|
|
|
|
impl RepairResult {
|
|
pub fn had_errors(&self) -> bool {
|
|
self.repair_errors > 0
|
|
}
|
|
}
|
|
|
|
pub fn write_clean_shutdown_marker(data_dir: &Path) -> std::io::Result<()> {
|
|
let marker_path = data_dir.join(CLEAN_SHUTDOWN_MARKER);
|
|
let f = std::fs::File::create(&marker_path)?;
|
|
f.sync_all()?;
|
|
std::fs::File::open(data_dir)?.sync_all()
|
|
}
|
|
|
|
pub fn remove_clean_shutdown_marker(data_dir: &Path) -> std::io::Result<()> {
|
|
let marker_path = data_dir.join(CLEAN_SHUTDOWN_MARKER);
|
|
match std::fs::remove_file(&marker_path) {
|
|
Ok(()) => std::fs::File::open(data_dir)?.sync_all(),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
pub fn had_clean_shutdown(data_dir: &Path) -> bool {
|
|
data_dir.join(CLEAN_SHUTDOWN_MARKER).exists()
|
|
}
|