Files
rustfs/ecstore/src/heal/data_scanner.rs
T
junxiang Mu 8e3c22b595 fix
Signed-off-by: junxiang Mu <1948535941@qq.com>
2025-04-25 08:59:39 +00:00

1250 lines
46 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::{
collections::{HashMap, HashSet},
fs,
future::Future,
io::{Cursor, Read},
path::{Path, PathBuf},
pin::Pin,
sync::{
atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
Arc,
},
time::{Duration, SystemTime},
};
use super::{
data_scanner_metric::{globalScannerMetrics, ScannerMetric, ScannerMetrics},
data_usage::{store_data_usage_in_backend, DATA_USAGE_BLOOM_NAME_PATH},
data_usage_cache::{DataUsageCache, DataUsageEntry, DataUsageHash},
heal_commands::{HealScanMode, HEAL_DEEP_SCAN, HEAL_NORMAL_SCAN},
};
use crate::{
bucket::{versioning::VersioningApi, versioning_sys::BucketVersioningSys},
heal::data_usage::DATA_USAGE_ROOT,
};
use crate::{
cache_value::metacache_set::{list_path_raw, ListPathRawOptions},
config::{
com::{read_config, save_config},
heal::Config,
},
disk::{error::DiskError, DiskInfoOptions, DiskStore, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams},
global::{GLOBAL_BackgroundHealState, GLOBAL_IsErasure, GLOBAL_IsErasureSD},
heal::{
data_usage::BACKGROUND_HEAL_INFO_PATH,
data_usage_cache::{hash_path, DataUsageHashMap},
error::ERR_IGNORE_FILE_CONTRIB,
heal_commands::{HEAL_ITEM_BUCKET, HEAL_ITEM_OBJECT},
heal_ops::{HealSource, BG_HEALING_UUID},
},
new_object_layer_fn,
peer::is_reserved_or_invalid_bucket,
store::ECStore,
utils::path::{path_join, path_to_bucket_object, path_to_bucket_object_with_base_path, SLASH_SEPARATOR},
};
use crate::{disk::local::LocalDisk, heal::data_scanner_metric::current_path_updater};
use crate::{
disk::DiskAPI,
store_api::{FileInfo, ObjectInfo},
};
use chrono::{DateTime, Utc};
use common::error::{Error, Result};
use lazy_static::lazy_static;
use rand::Rng;
use rmp_serde::{Deserializer, Serializer};
use s3s::dto::{BucketLifecycleConfiguration, ExpirationStatus, LifecycleRule, ReplicationConfiguration, ReplicationRuleStatus};
use serde::{Deserialize, Serialize};
use tokio::{
sync::{
broadcast,
mpsc::{self, Sender},
RwLock,
},
time::sleep,
};
use tracing::{error, info};
const DATA_SCANNER_SLEEP_PER_FOLDER: Duration = Duration::from_millis(1); // Time to wait between folders.
const DATA_USAGE_UPDATE_DIR_CYCLES: u32 = 16; // Visit all folders every n cycles.
const DATA_SCANNER_COMPACT_LEAST_OBJECT: u64 = 500; // Compact when there are less than this many objects in a branch.
const DATA_SCANNER_COMPACT_AT_CHILDREN: u64 = 10000; // Compact when there are this many children in a branch.
const DATA_SCANNER_COMPACT_AT_FOLDERS: u64 = DATA_SCANNER_COMPACT_AT_CHILDREN / 4; // Compact when this many subfolders in a single folder.
pub const DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS: u64 = 250_000; // Compact when this many subfolders in a single folder (even top level).
const DATA_SCANNER_START_DELAY: Duration = Duration::from_secs(60); // Time to wait on startup and between cycles.
pub const HEAL_DELETE_DANGLING: bool = true;
const HEAL_OBJECT_SELECT_PROB: u64 = 1024; // Overall probability of a file being scanned; one in n.
static SCANNER_CYCLE: AtomicU64 = AtomicU64::new(DATA_SCANNER_START_DELAY.as_secs());
static _SCANNER_IDLE_MODE: AtomicU32 = AtomicU32::new(0); // default is throttled when idle
static SCANNER_EXCESS_OBJECT_VERSIONS: AtomicU64 = AtomicU64::new(100);
static SCANNER_EXCESS_OBJECT_VERSIONS_TOTAL_SIZE: AtomicU64 = AtomicU64::new(1024 * 1024 * 1024 * 1024); // 1 TB
static SCANNER_EXCESS_FOLDERS: AtomicU64 = AtomicU64::new(50_000);
lazy_static! {
static ref SCANNER_SLEEPER: RwLock<DynamicSleeper> = RwLock::new(new_dynamic_sleeper(2.0, Duration::from_secs(1), true));
pub static ref globalHealConfig: Arc<RwLock<Config>> = Arc::new(RwLock::new(Config::default()));
}
struct DynamicSleeper {
factor: f64,
max_sleep: Duration,
min_sleep: Duration,
_is_scanner: bool,
}
type TimerFn = Pin<Box<dyn Future<Output = ()> + Send>>;
impl DynamicSleeper {
fn timer() -> TimerFn {
let t = SystemTime::now();
Box::pin(async move {
let done_at = SystemTime::now().duration_since(t).unwrap_or_default();
SCANNER_SLEEPER.read().await.sleep(done_at).await;
})
}
async fn sleep(&self, base: Duration) {
let (min_wait, max_wait) = (self.min_sleep, self.max_sleep);
let factor = self.factor;
let want_sleep = {
let tmp = base.mul_f64(factor);
if tmp < min_wait {
return;
}
if max_wait > Duration::from_secs(0) && tmp > max_wait {
max_wait
} else {
tmp
}
};
sleep(want_sleep).await;
}
fn _update(&mut self, factor: f64, max_wait: Duration) -> Result<()> {
if (self.factor - factor).abs() < 1e-10 && self.max_sleep == max_wait {
return Ok(());
}
self.factor = factor;
self.max_sleep = max_wait;
Ok(())
}
}
fn new_dynamic_sleeper(factor: f64, max_wait: Duration, is_scanner: bool) -> DynamicSleeper {
DynamicSleeper {
factor,
max_sleep: max_wait,
min_sleep: Duration::from_micros(100),
_is_scanner: is_scanner,
}
}
pub async fn init_data_scanner() {
tokio::spawn(async move {
loop {
run_data_scanner().await;
let random = {
let mut r = rand::thread_rng();
r.gen_range(0.0..1.0)
};
let duration = Duration::from_secs_f64(random * (SCANNER_CYCLE.load(std::sync::atomic::Ordering::SeqCst) as f64));
let sleep_duration = if duration < Duration::new(1, 0) {
Duration::new(1, 0)
} else {
duration
};
info!("data scanner will sleeping {sleep_duration:?}");
sleep(sleep_duration).await;
}
});
}
async fn run_data_scanner() {
info!("run_data_scanner");
let Some(store) = new_object_layer_fn() else {
error!("errServerNotInitialized");
return;
};
let buf = read_config(store.clone(), &DATA_USAGE_BLOOM_NAME_PATH)
.await
.map_or(Vec::new(), |buf| buf);
let mut buf_t = Deserializer::new(Cursor::new(buf));
let mut cycle_info: CurrentScannerCycle = Deserialize::deserialize(&mut buf_t).unwrap_or_default();
loop {
let stop_fn = ScannerMetrics::log(ScannerMetric::ScanCycle);
cycle_info.current = cycle_info.next;
cycle_info.started = Utc::now();
{
globalScannerMetrics.write().await.set_cycle(Some(cycle_info.clone())).await;
}
let bg_heal_info = read_background_heal_info(store.clone()).await;
let scan_mode =
get_cycle_scan_mode(cycle_info.current, bg_heal_info.bitrot_start_cycle, bg_heal_info.bitrot_start_time).await;
if bg_heal_info.current_scan_mode != scan_mode {
let mut new_heal_info = bg_heal_info;
new_heal_info.current_scan_mode = scan_mode;
if scan_mode == HEAL_DEEP_SCAN {
new_heal_info.bitrot_start_time = SystemTime::now();
new_heal_info.bitrot_start_cycle = cycle_info.current;
}
save_background_heal_info(store.clone(), &new_heal_info).await;
}
// Wait before starting next cycle and wait on startup.
let (tx, rx) = mpsc::channel(100);
tokio::spawn(async {
store_data_usage_in_backend(rx).await;
});
let mut res = HashMap::new();
res.insert("cycle".to_string(), cycle_info.current.to_string());
info!("start ns_scanner");
match store.clone().ns_scanner(tx, cycle_info.current as usize, scan_mode).await {
Ok(_) => {
info!("ns_scanner completed");
cycle_info.next += 1;
cycle_info.current = 0;
cycle_info.cycle_completed.push(Utc::now());
if cycle_info.cycle_completed.len() > DATA_USAGE_UPDATE_DIR_CYCLES as usize {
let _ = cycle_info.cycle_completed.remove(0);
}
globalScannerMetrics.write().await.set_cycle(Some(cycle_info.clone())).await;
let mut wr = Vec::new();
cycle_info.serialize(&mut Serializer::new(&mut wr)).unwrap();
let _ = save_config(store.clone(), &DATA_USAGE_BLOOM_NAME_PATH, wr).await;
}
Err(err) => {
info!("ns_scanner failed: {:?}", err);
res.insert("error".to_string(), err.to_string());
}
}
stop_fn(&res).await;
sleep(Duration::from_secs(SCANNER_CYCLE.load(std::sync::atomic::Ordering::SeqCst))).await;
}
}
#[derive(Debug, Serialize, Deserialize)]
struct BackgroundHealInfo {
bitrot_start_time: SystemTime,
bitrot_start_cycle: u64,
current_scan_mode: HealScanMode,
}
impl Default for BackgroundHealInfo {
fn default() -> Self {
Self {
bitrot_start_time: SystemTime::now(),
bitrot_start_cycle: Default::default(),
current_scan_mode: Default::default(),
}
}
}
async fn read_background_heal_info(store: Arc<ECStore>) -> BackgroundHealInfo {
if *GLOBAL_IsErasureSD.read().await {
return BackgroundHealInfo::default();
}
let buf = read_config(store, &BACKGROUND_HEAL_INFO_PATH)
.await
.map_or(Vec::new(), |buf| buf);
if buf.is_empty() {
return BackgroundHealInfo::default();
}
serde_json::from_slice::<BackgroundHealInfo>(&buf).map_or(BackgroundHealInfo::default(), |b| b)
}
async fn save_background_heal_info(store: Arc<ECStore>, info: &BackgroundHealInfo) {
if *GLOBAL_IsErasureSD.read().await {
return;
}
let b = match serde_json::to_vec(info) {
Ok(info) => info,
Err(_) => return,
};
let _ = save_config(store, &BACKGROUND_HEAL_INFO_PATH, b).await;
}
async fn get_cycle_scan_mode(current_cycle: u64, bitrot_start_cycle: u64, bitrot_start_time: SystemTime) -> HealScanMode {
let bitrot_cycle = globalHealConfig.read().await.bitrot_scan_cycle();
let v = bitrot_cycle.as_secs_f64();
if v == -1.0 {
return HEAL_NORMAL_SCAN;
} else if v == 0.0 {
return HEAL_DEEP_SCAN;
}
if current_cycle - bitrot_start_cycle < HEAL_OBJECT_SELECT_PROB {
return HEAL_DEEP_SCAN;
}
if bitrot_start_time.duration_since(SystemTime::now()).unwrap() > bitrot_cycle {
return HEAL_DEEP_SCAN;
}
HEAL_NORMAL_SCAN
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CurrentScannerCycle {
pub current: u64,
pub next: u64,
pub started: DateTime<Utc>,
pub cycle_completed: Vec<DateTime<Utc>>,
}
impl Default for CurrentScannerCycle {
fn default() -> Self {
Self {
current: Default::default(),
next: Default::default(),
started: Utc::now(),
cycle_completed: Default::default(),
}
}
}
impl CurrentScannerCycle {
pub fn marshal_msg(&self, next_buf: &[u8]) -> Result<Vec<u8>> {
let len: u32 = 4;
let mut wr = Vec::new();
// 字段数量
rmp::encode::write_map_len(&mut wr, len)?;
// write "current"
rmp::encode::write_str(&mut wr, "current")?;
rmp::encode::write_uint(&mut wr, self.current)?;
// write "next"
rmp::encode::write_str(&mut wr, "next")?;
rmp::encode::write_uint(&mut wr, self.next)?;
// write "started"
rmp::encode::write_str(&mut wr, "started")?;
rmp::encode::write_sint(&mut wr, system_time_to_timestamp(&self.started))?;
// write "cycle_completed"
rmp::encode::write_str(&mut wr, "cycle_completed")?;
let mut buf = Vec::new();
self.cycle_completed
.serialize(&mut Serializer::new(&mut buf))
.expect("Serialization failed");
rmp::encode::write_bin(&mut wr, &buf)?;
let mut result = next_buf.to_vec();
result.extend(wr.iter());
Ok(result)
}
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
let mut cur = Cursor::new(buf);
let mut fields_len = rmp::decode::read_map_len(&mut cur)?;
while fields_len > 0 {
fields_len -= 1;
let str_len = rmp::decode::read_str_len(&mut cur)?;
// !!!Vec::with_capacity(str_len) 失败,vec! 正常
let mut field_buff = vec![0u8; str_len as usize];
cur.read_exact(&mut field_buff)?;
let field = String::from_utf8(field_buff)?;
match field.as_str() {
"current" => {
let u: u64 = rmp::decode::read_int(&mut cur)?;
self.current = u;
}
// "next" => {
// let u: u64 = rmp::decode::read_int(&mut cur)?;
// self.next = u;
// }
"started" => {
let u: i64 = rmp::decode::read_int(&mut cur)?;
let started = timestamp_to_system_time(u);
self.started = started;
}
"cycleCompleted" => {
let mut buf = Vec::new();
let _ = cur.read_to_end(&mut buf)?;
let u: Vec<DateTime<Utc>> =
Deserialize::deserialize(&mut Deserializer::new(&buf[..])).expect("Deserialization failed");
self.cycle_completed = u;
}
name => return Err(Error::msg(format!("not suport field name {}", name))),
}
}
Ok(cur.position())
}
}
// 将 SystemTime 转换为时间戳
fn system_time_to_timestamp(time: &DateTime<Utc>) -> i64 {
time.timestamp_micros()
}
// 将时间戳转换为 SystemTime
fn timestamp_to_system_time(timestamp: i64) -> DateTime<Utc> {
DateTime::from_timestamp_micros(timestamp).unwrap_or_default()
}
#[derive(Clone, Debug, Default)]
pub struct Heal {
enabled: bool,
bitrot: bool,
}
#[derive(Clone)]
pub struct ScannerItem {
pub path: String,
pub bucket: String,
pub prefix: String,
pub object_name: String,
pub replication: Option<ReplicationConfiguration>,
pub lifecycle: Option<BucketLifecycleConfiguration>,
// typ: fs::Permissions,
pub heal: Heal,
pub debug: bool,
}
impl ScannerItem {
pub fn transform_meda_dir(&mut self) {
let split = self.prefix.split(SLASH_SEPARATOR).map(PathBuf::from).collect::<Vec<_>>();
if split.len() > 1 {
self.prefix = path_join(&split[0..split.len() - 1]).to_string_lossy().to_string();
} else {
self.prefix = "".to_string();
}
self.object_name = split.last().map_or("".to_string(), |v| v.to_string_lossy().to_string());
}
pub fn object_path(&self) -> PathBuf {
path_join(&[PathBuf::from(self.prefix.clone()), PathBuf::from(self.object_name.clone())])
}
pub async fn apply_versions_actions(&self, fivs: &[FileInfo]) -> Result<Vec<ObjectInfo>> {
let obj_infos = self.apply_newer_noncurrent_version_limit(fivs).await?;
if obj_infos.len() >= SCANNER_EXCESS_OBJECT_VERSIONS.load(Ordering::SeqCst) as usize {
// todo
}
let mut cumulative_size = 0;
for obj_info in obj_infos.iter() {
cumulative_size += obj_info.size;
}
if cumulative_size >= SCANNER_EXCESS_OBJECT_VERSIONS_TOTAL_SIZE.load(Ordering::SeqCst) as usize {
//todo
}
Ok(obj_infos)
}
pub async fn apply_newer_noncurrent_version_limit(&self, fivs: &[FileInfo]) -> Result<Vec<ObjectInfo>> {
// let done = ScannerMetrics::time(ScannerMetric::ApplyNonCurrent);
let versioned = match BucketVersioningSys::get(&self.bucket).await {
Ok(vcfg) => vcfg.versioned(self.object_path().to_str().unwrap_or_default()),
Err(_) => false,
};
let mut object_infos = Vec::with_capacity(fivs.len());
if self.lifecycle.is_none() {
for info in fivs.iter() {
object_infos.push(info.to_object_info(&self.bucket, &self.object_path().to_string_lossy(), versioned));
}
return Ok(object_infos);
}
// done().await;
Ok(object_infos)
}
pub async fn apply_actions(&self, oi: &ObjectInfo, _size_s: &SizeSummary) -> (bool, usize) {
let done = ScannerMetrics::time(ScannerMetric::Ilm);
//todo: lifecycle
done().await;
(false, oi.size)
}
}
#[derive(Debug, Default)]
pub struct SizeSummary {
pub total_size: usize,
pub versions: usize,
pub delete_markers: usize,
pub replicated_size: usize,
pub replicated_count: usize,
pub pending_size: usize,
pub failed_size: usize,
pub replica_size: usize,
pub replica_count: usize,
pub pending_count: usize,
pub failed_count: usize,
pub repl_target_stats: HashMap<String, ReplTargetSizeSummary>,
// Todo: tires
}
#[derive(Debug, Default)]
pub struct ReplTargetSizeSummary {
pub replicated_size: usize,
pub replicated_count: usize,
pub pending_size: usize,
pub failed_size: usize,
pub pending_count: usize,
pub failed_count: usize,
}
#[derive(Debug, Clone)]
struct CachedFolder {
name: String,
parent: DataUsageHash,
object_heal_prob_div: u32,
}
pub type GetSizeFn =
Box<dyn Fn(&ScannerItem) -> Pin<Box<dyn Future<Output = Result<SizeSummary>> + Send>> + Send + Sync + 'static>;
pub type UpdateCurrentPathFn = Arc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync + 'static>;
pub type ShouldSleepFn = Option<Arc<dyn Fn() -> bool + Send + Sync + 'static>>;
struct FolderScanner {
root: String,
get_size: GetSizeFn,
old_cache: DataUsageCache,
new_cache: DataUsageCache,
update_cache: DataUsageCache,
data_usage_scanner_debug: bool,
heal_object_select: u32,
scan_mode: HealScanMode,
disks: Vec<Option<DiskStore>>,
disks_quorum: usize,
updates: Sender<DataUsageEntry>,
last_update: SystemTime,
update_current_path: UpdateCurrentPathFn,
skip_heal: AtomicBool,
drive: LocalDrive,
we_sleep: ShouldSleepFn,
}
impl FolderScanner {
async fn should_heal(&self) -> bool {
if self.skip_heal.load(Ordering::SeqCst) {
return false;
}
if self.heal_object_select == 0 {
return false;
}
if let Ok(info) = self.drive.disk_info(&DiskInfoOptions::default()).await {
if info.healing {
self.skip_heal.store(true, Ordering::SeqCst);
return false;
}
}
true
}
#[tracing::instrument(level = "info", skip_all)]
async fn scan_folder(&mut self, folder: &CachedFolder, into: &mut DataUsageEntry) -> Result<()> {
let this_hash = hash_path(&folder.name);
let was_compacted = into.compacted;
'outer: {
let mut abandoned_children: DataUsageHashMap = if !into.compacted {
self.old_cache.find_children_copy(this_hash.clone())
} else {
HashSet::new()
};
let (_, prefix) = path_to_bucket_object_with_base_path(&self.root, &folder.name);
// Todo: lifeCycle
let active_life_cycle = if let Some(lc) = self.old_cache.info.life_cycle.as_ref() {
if lc_has_active_rules(lc, &prefix) {
self.old_cache.info.life_cycle.clone()
} else {
None
}
} else {
None
};
let replication_cfg = if self.old_cache.info.replication.is_some()
&& rep_has_active_rules(self.old_cache.info.replication.as_ref().unwrap(), &prefix, true)
{
self.old_cache.info.replication.clone()
} else {
None
};
if let Some(should_sleep) = &self.we_sleep {
if should_sleep() {
SCANNER_SLEEPER.read().await.sleep(DATA_SCANNER_SLEEP_PER_FOLDER).await;
}
}
let mut existing_folders = Vec::new();
let mut new_folders = Vec::new();
let mut found_objects: bool = false;
let path = Path::new(&self.root).join(&folder.name);
if path.is_dir() {
for entry in fs::read_dir(path)? {
let entry = entry?;
let sub_path = entry.path();
let ent_name = Path::new(&folder.name).join(&sub_path);
let (bucket, prefix) = path_to_bucket_object_with_base_path(&self.root, ent_name.to_str().unwrap());
if bucket.is_empty() {
continue;
}
if is_reserved_or_invalid_bucket(&bucket, false) {
continue;
}
if sub_path.is_dir() {
let h = hash_path(ent_name.to_str().unwrap());
if h == this_hash {
continue;
}
let this = CachedFolder {
name: ent_name.to_string_lossy().to_string(),
parent: this_hash.clone(),
object_heal_prob_div: folder.object_heal_prob_div,
};
abandoned_children.remove(&h.key());
if self.old_cache.cache.contains_key(&h.key()) {
existing_folders.push(this);
self.update_cache
.copy_with_children(&self.old_cache, &h, &Some(this_hash.clone()));
} else {
new_folders.push(this);
}
continue;
}
let _wait = if let Some(should_sleep) = &self.we_sleep {
if should_sleep() {
DynamicSleeper::timer()
} else {
Box::pin(async {})
}
} else {
Box::pin(async {})
};
let mut item = ScannerItem {
path: Path::new(&self.root).join(&ent_name).to_string_lossy().to_string(),
bucket,
prefix: Path::new(&prefix)
.parent()
.unwrap_or(Path::new(""))
.to_string_lossy()
.to_string(),
object_name: ent_name
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default(),
debug: self.data_usage_scanner_debug,
replication: replication_cfg.clone(),
lifecycle: active_life_cycle.clone(),
heal: Heal::default(),
};
item.heal.enabled = this_hash.mod_alt(
self.old_cache.info.next_cycle / folder.object_heal_prob_div,
self.heal_object_select / folder.object_heal_prob_div,
) && self.should_heal().await;
item.heal.bitrot = self.scan_mode == HEAL_DEEP_SCAN;
let (sz, err) = match (self.get_size)(&item).await {
Ok(sz) => (sz, None),
Err(err) => {
if err.to_string() != ERR_IGNORE_FILE_CONTRIB {
continue;
}
(SizeSummary::default(), Some(err))
}
};
// successfully read means we have a valid object.
found_objects = true;
// Remove filename i.e is the meta file to construct object name
item.transform_meda_dir();
// Object already accounted for, remove from heal map,
// simply because getSize() function already heals the
// object.
abandoned_children.remove(
&path_join(&[PathBuf::from(item.bucket.clone()), item.object_path()])
.to_string_lossy()
.to_string(),
);
if err.is_none() || err.unwrap().to_string() != ERR_IGNORE_FILE_CONTRIB {
into.add_sizes(&sz);
into.objects += 1;
}
}
}
// if found_objects && *GLOBAL_IsErasure.read().await {
if found_objects {
break 'outer;
}
let should_compact = self.new_cache.info.name != folder.name
&& (existing_folders.len() + new_folders.len() >= DATA_SCANNER_COMPACT_AT_FOLDERS as usize
|| existing_folders.len() + new_folders.len() >= DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS as usize);
let total_folders = existing_folders.len() + new_folders.len();
if total_folders > SCANNER_EXCESS_FOLDERS.load(Ordering::SeqCst) as usize {
let _prefix_name = format!("{}/", folder.name.trim_end_matches('/'));
// todo: notification
}
if !into.compacted && should_compact {
into.compacted = true;
new_folders.extend(existing_folders.clone());
existing_folders.clear();
}
// Transfer existing
if !into.compacted {
for folder in existing_folders.iter() {
let h = hash_path(&folder.name);
self.update_cache
.copy_with_children(&self.old_cache, &h, &Some(folder.parent.clone()));
}
}
// Scan new...
for folder in new_folders.iter() {
let h = hash_path(&folder.name);
if !into.compacted {
let mut found_any = false;
let mut parent = this_hash.clone();
while parent != hash_path(&self.update_cache.info.name) {
let e = self.update_cache.find(&parent.key());
if e.is_none() || e.as_ref().unwrap().compacted {
found_any = true;
break;
}
match self.update_cache.search_parent(&parent) {
Some(next) => {
parent = next;
}
None => {
found_any = true;
break;
}
}
}
if !found_any {
self.update_cache
.replace_hashed(&h, &Some(this_hash.clone()), &DataUsageEntry::default());
}
}
(self.update_current_path)(&folder.name).await;
scan(folder, into, self).await;
// Add new folders if this is new and we don't have existing.
if !into.compacted {
if let Some(parent) = self.update_cache.find(&this_hash.key()) {
if !parent.compacted {
self.update_cache.delete_recursive(&h);
self.update_cache
.copy_with_children(&self.new_cache, &h, &Some(this_hash.clone()));
}
}
}
}
// Scan existing...
for folder in existing_folders.iter() {
let h = hash_path(&folder.name);
if !into.compacted
&& self.old_cache.is_compacted(&h)
&& !h.mod_(self.old_cache.info.next_cycle, DATA_USAGE_UPDATE_DIR_CYCLES)
{
self.new_cache
.copy_with_children(&self.old_cache, &h, &Some(folder.parent.clone()));
into.add_child(&h);
continue;
}
(self.update_current_path)(&folder.name).await;
scan(folder, into, self).await;
}
// Scan for healing
if abandoned_children.is_empty() || !self.should_heal().await {
break 'outer;
}
if self.disks.is_empty() || self.disks_quorum == 0 {
break 'outer;
}
let (bg_seq, found) = GLOBAL_BackgroundHealState.get_heal_sequence_by_token(BG_HEALING_UUID).await;
if !found {
break 'outer;
}
let bg_seq = bg_seq.unwrap();
let mut resolver = MetadataResolutionParams {
dir_quorum: self.disks_quorum,
obj_quorum: self.disks_quorum,
bucket: "".to_string(),
strict: false,
..Default::default()
};
for k in abandoned_children.iter() {
if !self.should_heal().await {
break;
}
let (bucket, prefix) = path_to_bucket_object(k);
(self.update_current_path)(k).await;
if bucket != resolver.bucket {
bg_seq
.clone()
.queue_heal_task(
HealSource {
bucket: bucket.clone(),
..Default::default()
},
HEAL_ITEM_BUCKET.to_owned(),
)
.await?;
}
resolver.bucket = bucket.clone();
let found_objs = Arc::new(RwLock::new(false));
let found_objs_clone = found_objs.clone();
let (tx, rx) = broadcast::channel(1);
// let tx_partial = tx.clone();
let tx_finished = tx.clone();
let update_current_path_agreed = self.update_current_path.clone();
let update_current_path_partial = self.update_current_path.clone();
let resolver_clone = resolver.clone();
let bg_seq_clone = bg_seq.clone();
let lopts = ListPathRawOptions {
disks: self.disks.clone(),
bucket: bucket.clone(),
path: prefix.clone(),
recursice: true,
report_not_found: true,
min_disks: self.disks_quorum,
agreed: Some(Box::new(move |entry: MetaCacheEntry| {
Box::pin({
let update_current_path_agreed = update_current_path_agreed.clone();
async move {
update_current_path_agreed(&entry.name).await;
}
})
})),
partial: Some(Box::new(move |entries: MetaCacheEntries, _: &[Option<Error>]| {
Box::pin({
let update_current_path_partial = update_current_path_partial.clone();
// let tx_partial = tx_partial.clone();
let resolver_partial = resolver_clone.clone();
let bucket_partial = bucket.clone();
let found_objs_clone = found_objs_clone.clone();
let bg_seq_partial = bg_seq_clone.clone();
async move {
// Todo
// if !fs.should_heal().await {
// let _ = tx_partial.send(true);
// return;
// }
let entry = match entries.resolve(resolver_partial) {
Ok(Some(entry)) => entry,
_ => match entries.first_found() {
(Some(entry), _) => entry,
_ => return,
},
};
update_current_path_partial(&entry.name).await;
let mut custom = HashMap::new();
if entry.is_dir() {
return;
}
// We got an entry which we should be able to heal.
let fiv = match entry.file_info_versions(&bucket_partial) {
Ok(fiv) => fiv,
Err(_) => {
if let Err(err) = bg_seq_partial
.queue_heal_task(
HealSource {
bucket: bucket_partial.clone(),
object: entry.name.clone(),
version_id: "".to_string(),
..Default::default()
},
HEAL_ITEM_OBJECT.to_string(),
)
.await
{
match err.downcast_ref() {
Some(DiskError::FileNotFound) | Some(DiskError::FileVersionNotFound) => {}
_ => {
info!("{}", err.to_string());
}
}
} else {
let mut w = found_objs_clone.write().await;
*w = true;
}
return;
}
};
custom.insert("versions", fiv.versions.len().to_string());
let (mut success_versions, mut fail_versions) = (0, 0);
for ver in fiv.versions.iter() {
match bg_seq_partial
.queue_heal_task(
HealSource {
bucket: bucket_partial.clone(),
object: fiv.name.clone(),
version_id: ver.version_id.map_or("".to_string(), |ver_id| ver_id.to_string()),
..Default::default()
},
HEAL_ITEM_OBJECT.to_string(),
)
.await
{
Ok(_) => {
success_versions += 1;
let mut w = found_objs_clone.write().await;
*w = true;
}
Err(_) => {
fail_versions += 1;
}
}
}
custom.insert("success_versions", success_versions.to_string());
custom.insert("failed_versions", fail_versions.to_string());
}
})
})),
finished: Some(Box::new(move |_: &[Option<Error>]| {
Box::pin({
let tx_finished = tx_finished.clone();
async move {
let _ = tx_finished.send(true);
}
})
})),
..Default::default()
};
let _ = list_path_raw(rx, lopts).await;
if *found_objs.read().await {
let this: CachedFolder = CachedFolder {
name: k.clone(),
parent: this_hash.clone(),
object_heal_prob_div: 1,
};
scan(&this, into, self).await;
}
}
}
if !was_compacted {
self.new_cache.replace_hashed(&this_hash, &Some(folder.parent.clone()), into);
}
if !into.compacted && self.new_cache.info.name != folder.name {
let mut flat = self.new_cache.size_recursive(&this_hash.key()).unwrap_or_default();
flat.compacted = true;
let compact = if flat.objects < DATA_SCANNER_COMPACT_LEAST_OBJECT as usize {
true
} else {
// Compact if we only have objects as children...
let mut compact = true;
for k in into.children.iter() {
if let Some(v) = self.new_cache.cache.get(k) {
if !v.children.is_empty() || v.objects > 1 {
compact = false;
break;
}
}
}
compact
};
if compact {
self.new_cache.delete_recursive(&this_hash);
self.new_cache.replace_hashed(&this_hash, &Some(folder.parent.clone()), &flat);
let mut total: HashMap<String, String> = HashMap::new();
total.insert("objects".to_string(), flat.objects.to_string());
total.insert("size".to_string(), flat.size.to_string());
if flat.versions > 0 {
total.insert("versions".to_string(), flat.versions.to_string());
}
}
}
// Compact if too many children...
if !into.compacted {
self.new_cache.reduce_children_of(
&this_hash,
DATA_SCANNER_COMPACT_AT_CHILDREN.try_into().unwrap(),
self.new_cache.info.name != folder.name,
);
}
if self.update_cache.cache.contains_key(&this_hash.key()) && !was_compacted {
// Replace if existed before.
if let Some(flat) = self.new_cache.size_recursive(&this_hash.key()) {
self.update_cache.delete_recursive(&this_hash);
self.update_cache
.replace_hashed(&this_hash, &Some(folder.parent.clone()), &flat);
}
}
Ok(())
}
#[tracing::instrument(level = "info", skip_all)]
async fn send_update(&mut self) {
if SystemTime::now().duration_since(self.last_update).unwrap() < Duration::from_secs(60) {
return;
}
if let Some(flat) = self.update_cache.size_recursive(&self.new_cache.info.name) {
let _ = self.updates.send(flat).await;
self.last_update = SystemTime::now();
}
}
}
#[tracing::instrument(level = "info", skip(into, folder_scanner))]
async fn scan(folder: &CachedFolder, into: &mut DataUsageEntry, folder_scanner: &mut FolderScanner) {
let mut dst = if !into.compacted {
DataUsageEntry::default()
} else {
into.clone()
};
if Box::pin(folder_scanner.scan_folder(folder, &mut dst)).await.is_err() {
return;
}
if !into.compacted {
let h = DataUsageHash(folder.name.clone());
into.add_child(&h);
folder_scanner.update_cache.delete_recursive(&h);
folder_scanner
.update_cache
.copy_with_children(&folder_scanner.new_cache, &h, &Some(folder.parent.clone()));
folder_scanner.send_update().await;
}
}
fn lc_get_prefix(rule: &LifecycleRule) -> String {
if let Some(p) = &rule.prefix {
return p.to_string();
} else if let Some(filter) = &rule.filter {
if let Some(p) = &filter.prefix {
return p.to_string();
} else if let Some(and) = &filter.and {
if let Some(p) = &and.prefix {
return p.to_string();
}
}
}
"".into()
}
pub fn lc_has_active_rules(config: &BucketLifecycleConfiguration, prefix: &str) -> bool {
if config.rules.is_empty() {
return false;
}
for rule in config.rules.iter() {
if rule.status == ExpirationStatus::from_static(ExpirationStatus::DISABLED) {
continue;
}
let rule_prefix = lc_get_prefix(rule);
if !prefix.is_empty() && !rule_prefix.is_empty() && !prefix.starts_with(&rule_prefix) && !rule_prefix.starts_with(prefix)
{
continue;
}
if let Some(e) = &rule.noncurrent_version_expiration {
if let Some(true) = e.noncurrent_days.map(|d| d > 0) {
return true;
}
if let Some(true) = e.newer_noncurrent_versions.map(|d| d > 0) {
return true;
}
}
if rule.noncurrent_version_transitions.is_some() {
return true;
}
if let Some(true) = rule.expiration.as_ref().map(|e| e.date.is_some()) {
return true;
}
if let Some(true) = rule.expiration.as_ref().map(|e| e.days.is_some()) {
return true;
}
if let Some(Some(true)) = rule.expiration.as_ref().map(|e| e.expired_object_delete_marker) {
return true;
}
if let Some(true) = rule.transitions.as_ref().map(|t| !t.is_empty()) {
return true;
}
if rule.transitions.is_some() {
return true;
}
}
false
}
pub fn rep_has_active_rules(config: &ReplicationConfiguration, prefix: &str, recursive: bool) -> bool {
if config.rules.is_empty() {
return false;
}
for rule in config.rules.iter() {
if rule
.status
.eq(&ReplicationRuleStatus::from_static(ReplicationRuleStatus::DISABLED))
{
continue;
}
if !prefix.is_empty() {
if let Some(filter) = &rule.filter {
if let Some(r_prefix) = &filter.prefix {
if !r_prefix.is_empty() {
// incoming prefix must be in rule prefix
if !recursive && !prefix.starts_with(r_prefix) {
continue;
}
// If recursive, we can skip this rule if it doesn't match the tested prefix or level below prefix
// does not match
if recursive && !r_prefix.starts_with(prefix) && !prefix.starts_with(r_prefix) {
continue;
}
}
}
}
}
return true;
}
false
}
pub type LocalDrive = Arc<LocalDisk>;
pub async fn scan_data_folder(
disks: &[Option<DiskStore>],
drive: LocalDrive,
cache: &DataUsageCache,
get_size_fn: GetSizeFn,
heal_scan_mode: HealScanMode,
should_sleep: ShouldSleepFn,
) -> Result<DataUsageCache> {
if cache.info.name.is_empty() || cache.info.name == DATA_USAGE_ROOT {
return Err(Error::from_string("internal error: root scan attempted"));
}
let base_path = drive.to_string();
let (update_path, close_disk) = current_path_updater(&base_path, &cache.info.name);
let skip_heal = if *GLOBAL_IsErasure.read().await || cache.info.skip_healing {
AtomicBool::new(true)
} else {
AtomicBool::new(false)
};
let mut s = FolderScanner {
root: base_path,
get_size: get_size_fn,
old_cache: cache.clone(),
new_cache: DataUsageCache {
info: cache.info.clone(),
..Default::default()
},
update_cache: DataUsageCache {
info: cache.info.clone(),
..Default::default()
},
data_usage_scanner_debug: false,
heal_object_select: 0,
scan_mode: heal_scan_mode,
updates: cache.info.updates.clone().unwrap(),
last_update: SystemTime::now(),
update_current_path: update_path,
disks: disks.to_vec(),
disks_quorum: disks.len() / 2,
skip_heal,
drive: drive.clone(),
we_sleep: should_sleep,
};
if *GLOBAL_IsErasure.read().await || !cache.info.skip_healing {
s.heal_object_select = HEAL_OBJECT_SELECT_PROB as u32;
}
let mut root = DataUsageEntry::default();
let folder = CachedFolder {
name: cache.info.name.clone(),
object_heal_prob_div: 1,
parent: DataUsageHash("".to_string()),
};
if s.scan_folder(&folder, &mut root).await.is_err() {
close_disk().await;
}
s.new_cache.force_compact(DATA_SCANNER_COMPACT_AT_CHILDREN as usize);
s.new_cache.info.last_update = Some(SystemTime::now());
s.new_cache.info.next_cycle = cache.info.next_cycle;
close_disk().await;
Ok(s.new_cache)
}
// pub fn eval_action_from_lifecycle(lc: &BucketLifecycleConfiguration, lr: &ObjectLockConfiguration, rcfg: &ReplicationConfiguration obj: &ObjectInfo)
#[cfg(test)]
mod tests {
use std::io::Cursor;
use chrono::Utc;
use rmp_serde::{Deserializer, Serializer};
use serde::{Deserialize, Serialize};
use super::CurrentScannerCycle;
#[test]
fn test_current_cycle() {
let cycle_info = CurrentScannerCycle {
current: 0,
next: 1,
started: Utc::now(),
cycle_completed: vec![Utc::now(), Utc::now()],
};
println!("{cycle_info:?}");
let mut wr = Vec::new();
cycle_info.serialize(&mut Serializer::new(&mut wr)).unwrap();
let mut buf_t = Deserializer::new(Cursor::new(wr));
let c: CurrentScannerCycle = Deserialize::deserialize(&mut buf_t).unwrap();
println!("{c:?}");
}
}