use super::{
NnsAuthenticatedRegistryArchive, NnsCertifiedRegistryArchiveStorageError,
NnsCertifiedRegistryArchiveStorageLimits, nns_certified_registry_archive_manifest_path,
nns_certified_registry_archive_refresh_lock_path,
storage::{
ArchiveBatchAuthenticator, BuiltInArchiveAuthenticator,
load_nns_certified_registry_archive_with_authenticator,
nns_certified_registry_archive_objects_path,
},
};
use crate::{
cache_file::{
CacheFileError, ManagedDirectoryFile, RefreshLockRequest, managed_file_exists,
remove_managed_regular_file, scan_managed_directory_files, with_refresh_lock,
},
nns::registry::NnsRegistryReplaySessionLimits,
subnet_catalog::MAINNET_NETWORK,
};
use std::{cell::Cell, collections::BTreeSet, path::PathBuf};
use thiserror::Error as ThisError;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NnsCertifiedRegistryArchiveCleanupLimits {
pub max_scanned_objects: u64,
pub max_removed_objects: u64,
pub max_removed_bytes: u64,
}
impl NnsCertifiedRegistryArchiveCleanupLimits {
#[must_use]
pub const fn new(
max_scanned_objects: u64,
max_removed_objects: u64,
max_removed_bytes: u64,
) -> Self {
Self {
max_scanned_objects,
max_removed_objects,
max_removed_bytes,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsCertifiedRegistryArchiveCleanupRequest {
pub now_unix_secs: u64,
pub cache_root: PathBuf,
pub archive_root: PathBuf,
pub replay_limits: NnsRegistryReplaySessionLimits,
pub storage_limits: NnsCertifiedRegistryArchiveStorageLimits,
pub cleanup_limits: NnsCertifiedRegistryArchiveCleanupLimits,
pub lock_stale_after_seconds: u64,
}
impl NnsCertifiedRegistryArchiveCleanupRequest {
#[must_use]
pub fn new(
now_unix_secs: u64,
cache_root: impl Into<PathBuf>,
archive_root: impl Into<PathBuf>,
replay_limits: NnsRegistryReplaySessionLimits,
storage_limits: NnsCertifiedRegistryArchiveStorageLimits,
cleanup_limits: NnsCertifiedRegistryArchiveCleanupLimits,
lock_stale_after_seconds: u64,
) -> Self {
Self {
now_unix_secs,
cache_root: cache_root.into(),
archive_root: archive_root.into(),
replay_limits,
storage_limits,
cleanup_limits,
lock_stale_after_seconds,
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct NnsCertifiedRegistryArchiveCleanupReport {
pub archive: NnsAuthenticatedRegistryArchive,
pub scanned_object_count: u64,
pub referenced_object_count: u64,
pub removed_object_count: u64,
pub removed_bytes: u64,
}
#[derive(Debug, ThisError)]
pub enum NnsCertifiedRegistryArchiveCleanupError {
#[error(transparent)]
Storage(#[from] NnsCertifiedRegistryArchiveStorageError),
#[error("certified Registry archive cleanup {field} is {actual}; caller maximum is {maximum}")]
LimitExceeded {
field: &'static str,
maximum: u64,
actual: u64,
},
#[error(
"certified Registry archive cleanup failed after removing {removed_object_count} objects and {removed_bytes} bytes: {source}"
)]
FileOperation {
removed_object_count: u64,
removed_bytes: u64,
#[source]
source: CacheFileError,
},
#[error(
"certified Registry archive orphan {} disappeared after removing {removed_object_count} objects and {removed_bytes} bytes",
path.display()
)]
OrphanDisappeared {
path: PathBuf,
removed_object_count: u64,
removed_bytes: u64,
},
#[error("certified Registry archive cleanup accounting overflow")]
Accounting,
}
pub fn cleanup_nns_certified_registry_archive(
request: &NnsCertifiedRegistryArchiveCleanupRequest,
) -> Result<NnsCertifiedRegistryArchiveCleanupReport, NnsCertifiedRegistryArchiveCleanupError> {
cleanup_archive_with_authenticator(request, &BuiltInArchiveAuthenticator)
}
pub(in crate::nns::registry::replay) fn cleanup_archive_with_authenticator(
request: &NnsCertifiedRegistryArchiveCleanupRequest,
authenticator: &dyn ArchiveBatchAuthenticator,
) -> Result<NnsCertifiedRegistryArchiveCleanupReport, NnsCertifiedRegistryArchiveCleanupError> {
let manifest_path = nns_certified_registry_archive_manifest_path(&request.archive_root);
if !managed_file_exists(&request.cache_root, &manifest_path).map_err(file_operation)? {
return Err(NnsCertifiedRegistryArchiveStorageError::MissingManifest {
path: manifest_path,
}
.into());
}
let lock_path = nns_certified_registry_archive_refresh_lock_path(&request.archive_root);
let completed_removal = Cell::new((0_u64, 0_u64));
with_refresh_lock(
RefreshLockRequest {
cache_root: &request.cache_root,
lock_path: &lock_path,
target_path: &manifest_path,
network: MAINNET_NETWORK,
now_unix_secs: request.now_unix_secs,
lock_stale_after_seconds: request.lock_stale_after_seconds,
},
|source| {
let (removed_object_count, removed_bytes) = completed_removal.get();
NnsCertifiedRegistryArchiveCleanupError::FileOperation {
removed_object_count,
removed_bytes,
source,
}
},
|| {
let report = cleanup_locked(request, authenticator)?;
completed_removal.set((report.removed_object_count, report.removed_bytes));
Ok(report)
},
)
}
fn cleanup_locked(
request: &NnsCertifiedRegistryArchiveCleanupRequest,
authenticator: &dyn ArchiveBatchAuthenticator,
) -> Result<NnsCertifiedRegistryArchiveCleanupReport, NnsCertifiedRegistryArchiveCleanupError> {
let archive = load_nns_certified_registry_archive_with_authenticator(
&request.cache_root,
&request.archive_root,
request.replay_limits,
request.storage_limits,
authenticator,
)?;
let objects_path = nns_certified_registry_archive_objects_path(&request.archive_root);
let scan = scan_managed_directory_files(
&request.cache_root,
&objects_path,
request.cleanup_limits.max_scanned_objects,
)
.map_err(file_operation)?;
if scan.truncated {
return Err(NnsCertifiedRegistryArchiveCleanupError::LimitExceeded {
field: "scanned object count",
maximum: request.cleanup_limits.max_scanned_objects,
actual: checked_add(request.cleanup_limits.max_scanned_objects, 1)?,
});
}
let referenced = archive
.manifest()
.batches
.iter()
.map(|batch| objects_path.join(format!("{}.json", batch.report_sha256)))
.collect::<BTreeSet<_>>();
let orphans = scan
.files
.iter()
.filter(|object| !referenced.contains(&object.path))
.collect::<Vec<_>>();
let scanned_object_count = count(scan.files.len())?;
let referenced_object_count = count(referenced.len())?;
let removed_object_count = count(orphans.len())?;
enforce_limit(
"removed object count",
removed_object_count,
request.cleanup_limits.max_removed_objects,
)?;
let removed_bytes = orphans
.iter()
.try_fold(0_u64, |total, object| checked_add(total, object.bytes))?;
enforce_limit(
"removed object bytes",
removed_bytes,
request.cleanup_limits.max_removed_bytes,
)?;
remove_orphans(request, &orphans)?;
Ok(NnsCertifiedRegistryArchiveCleanupReport {
archive,
scanned_object_count,
referenced_object_count,
removed_object_count,
removed_bytes,
})
}
fn remove_orphans(
request: &NnsCertifiedRegistryArchiveCleanupRequest,
orphans: &[&ManagedDirectoryFile],
) -> Result<(), NnsCertifiedRegistryArchiveCleanupError> {
let mut removed_object_count = 0_u64;
let mut removed_bytes = 0_u64;
for orphan in orphans {
let removed = match remove_managed_regular_file(&request.cache_root, &orphan.path) {
Ok(removed) => removed,
Err(failure) => {
if failure.removed {
removed_object_count = checked_add(removed_object_count, 1)?;
removed_bytes = checked_add(removed_bytes, orphan.bytes)?;
}
return Err(NnsCertifiedRegistryArchiveCleanupError::FileOperation {
removed_object_count,
removed_bytes,
source: failure.source,
});
}
};
if !removed {
return Err(NnsCertifiedRegistryArchiveCleanupError::OrphanDisappeared {
path: orphan.path.clone(),
removed_object_count,
removed_bytes,
});
}
removed_object_count = checked_add(removed_object_count, 1)?;
removed_bytes = checked_add(removed_bytes, orphan.bytes)?;
}
Ok(())
}
const fn enforce_limit(
field: &'static str,
actual: u64,
maximum: u64,
) -> Result<(), NnsCertifiedRegistryArchiveCleanupError> {
if actual > maximum {
return Err(NnsCertifiedRegistryArchiveCleanupError::LimitExceeded {
field,
maximum,
actual,
});
}
Ok(())
}
fn count(value: usize) -> Result<u64, NnsCertifiedRegistryArchiveCleanupError> {
u64::try_from(value).map_err(|_| NnsCertifiedRegistryArchiveCleanupError::Accounting)
}
fn checked_add(left: u64, right: u64) -> Result<u64, NnsCertifiedRegistryArchiveCleanupError> {
left.checked_add(right)
.ok_or(NnsCertifiedRegistryArchiveCleanupError::Accounting)
}
const fn file_operation(source: CacheFileError) -> NnsCertifiedRegistryArchiveCleanupError {
NnsCertifiedRegistryArchiveCleanupError::FileOperation {
removed_object_count: 0,
removed_bytes: 0,
source,
}
}