use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tracing::{debug, warn};
use crate::block::router::{rpc_endpoint, WorkerRouter, WorkerRouterView};
use crate::client::{WorkerClient, WorkerClientPool};
use crate::config::GoosefsConfig;
use crate::error::Result;
use crate::proto::grpc::block::WorkerInfo;
use crate::proto::grpc::file::FileInfo;
use crate::proto::grpc::BlockLocation;
async fn acquire_worker(
addr: &str,
pool: Option<&Arc<WorkerClientPool>>,
config: &GoosefsConfig,
) -> Result<WorkerClient> {
if let Some(pool) = pool {
pool.acquire(addr).await
} else {
WorkerClient::connect(addr, config).await
}
}
pub fn ensure_block_ids_from_file_block_infos(file_info: &mut FileInfo) {
if !file_info.block_ids.is_empty() || file_info.file_block_infos.is_empty() {
return;
}
let mut pairs: Vec<(i64, i64)> = file_info
.file_block_infos
.iter()
.filter_map(|fbi| {
let offset = fbi.offset.unwrap_or(0);
let id = fbi.block_info.as_ref()?.block_id?;
if id > 0 {
Some((offset, id))
} else {
None
}
})
.collect();
pairs.sort_by_key(|(offset, _)| *offset);
file_info.block_ids = pairs.into_iter().map(|(_, id)| id).collect();
}
struct ProbedLocations {
locations: HashMap<i64, Vec<BlockLocation>>,
probed_ok: HashSet<i64>,
}
pub async fn enrich_file_block_locations_with_router(
file_info: &mut FileInfo,
router: &WorkerRouter,
pool: Option<&Arc<WorkerClientPool>>,
config: &GoosefsConfig,
check_count: usize,
) -> Result<()> {
let view = WorkerRouterView::from_shared(router);
enrich_file_block_locations(file_info, &view, pool, config, check_count).await
}
pub async fn enrich_file_block_locations(
file_info: &mut FileInfo,
router: &WorkerRouterView,
pool: Option<&Arc<WorkerClientPool>>,
config: &GoosefsConfig,
check_count: usize,
) -> Result<()> {
ensure_block_ids_from_file_block_infos(file_info);
if check_count == 0 || file_info.file_block_infos.is_empty() {
return Ok(());
}
let block_ids: Vec<i64> = file_info
.file_block_infos
.iter()
.filter_map(|fbi| fbi.block_info.as_ref()?.block_id)
.filter(|id| *id > 0)
.collect();
if block_ids.is_empty() {
return Ok(());
}
let probed = fetch_block_locations(router, pool, config, &block_ids, check_count).await?;
apply_probed_locations(file_info, &probed);
Ok(())
}
fn apply_probed_locations(file_info: &mut FileInfo, probed: &ProbedLocations) {
for fbi in &mut file_info.file_block_infos {
let Some(bi) = fbi.block_info.as_mut() else {
continue;
};
let Some(block_id) = bi.block_id else {
continue;
};
if !probed.probed_ok.contains(&block_id) {
continue;
}
bi.locations = probed.locations.get(&block_id).cloned().unwrap_or_default();
}
recompute_in_goosefs_percentage(file_info);
}
enum WorkerCheckOutcome {
Ok {
worker: WorkerInfo,
queried: Vec<i64>,
cached_bytes: HashMap<i64, i64>,
},
Failed,
}
async fn fetch_block_locations(
router: &WorkerRouterView,
pool: Option<&Arc<WorkerClientPool>>,
config: &GoosefsConfig,
block_ids: &[i64],
check_count: usize,
) -> Result<ProbedLocations> {
let mut locations: HashMap<i64, Vec<BlockLocation>> = HashMap::new();
let mut probed_ok: HashSet<i64> = HashSet::new();
let mut block_to_workers: HashMap<i64, Vec<WorkerInfo>> = HashMap::new();
for &block_id in block_ids {
match router.select_workers(block_id, check_count).await {
Ok(workers) => {
block_to_workers.insert(block_id, workers);
}
Err(e) => {
warn!(
block_id,
error = %e,
"checkBlocks: failed to select workers for block, skipping"
);
}
}
}
let mut worker_to_blocks: HashMap<String, (WorkerInfo, HashSet<i64>)> = HashMap::new();
for (block_id, workers) in &block_to_workers {
for w in workers {
let Some(addr) = w.address.as_ref() else {
continue;
};
let key = rpc_endpoint(addr);
worker_to_blocks
.entry(key)
.and_modify(|(_, ids)| {
ids.insert(*block_id);
})
.or_insert_with(|| (w.clone(), HashSet::from([*block_id])));
}
}
let mut tasks = Vec::with_capacity(worker_to_blocks.len());
for (endpoint, (worker, ids)) in worker_to_blocks {
let queried: Vec<i64> = ids.into_iter().collect();
let pool = pool.cloned();
let config = config.clone();
tasks.push(async move {
let client = match acquire_worker(&endpoint, pool.as_ref(), &config).await {
Ok(c) => c,
Err(e) => {
warn!(
worker = %endpoint,
error = %e,
"checkBlocks: failed to connect worker"
);
return WorkerCheckOutcome::Failed;
}
};
match client.check_blocks(&queried).await {
Ok(cached_bytes) => WorkerCheckOutcome::Ok {
worker,
queried,
cached_bytes,
},
Err(e) => {
warn!(
worker = %endpoint,
error = %e,
"checkBlocks: RPC failed"
);
WorkerCheckOutcome::Failed
}
}
});
}
let outcomes = futures::future::join_all(tasks).await;
for outcome in outcomes {
let WorkerCheckOutcome::Ok {
worker,
queried,
cached_bytes,
} = outcome
else {
continue;
};
for &block_id in &queried {
probed_ok.insert(block_id);
locations.entry(block_id).or_default();
}
let worker_id = worker.id;
let address = worker.address.clone();
for (block_id, bytes) in cached_bytes {
if bytes <= 0 {
continue;
}
locations.entry(block_id).or_default().push(BlockLocation {
worker_id,
worker_address: address.clone(),
});
debug!(
block_id,
worker_id = ?worker_id,
cached_bytes = bytes,
"checkBlocks: block present on worker"
);
}
}
Ok(ProbedLocations {
locations,
probed_ok,
})
}
fn recompute_in_goosefs_percentage(file_info: &mut FileInfo) {
let file_length = file_info.length.unwrap_or(0);
if file_length == 0 {
file_info.in_goose_fs_percentage = Some(100);
return;
}
let mut cache_size: i64 = 0;
for fbi in &file_info.file_block_infos {
let Some(bi) = fbi.block_info.as_ref() else {
continue;
};
if bi.locations.is_empty() {
continue;
}
let block_length = bi.length.unwrap_or(0).max(0);
cache_size += block_length;
}
let pct = ((cache_size.saturating_mul(100)) / file_length).min(100);
file_info.in_goose_fs_percentage = Some(pct as i32);
}
pub fn fill_in_goosefs_percentage_without_probe(file_info: &mut FileInfo) {
if file_info.folder.unwrap_or(false) {
return;
}
if file_info.length.unwrap_or(0) == 0 {
file_info.in_goose_fs_percentage = Some(100);
return;
}
let state = file_info.persistence_state.as_deref().unwrap_or("");
if state.eq_ignore_ascii_case("TO_BE_PERSISTED") {
file_info.in_goose_fs_percentage = Some(100);
return;
}
if file_info.completed.unwrap_or(false)
&& file_info.cacheable.unwrap_or(false)
&& !file_info.persisted.unwrap_or(false)
&& (state.is_empty() || state.eq_ignore_ascii_case("NOT_PERSISTED"))
{
file_info.in_goose_fs_percentage = Some(100);
return;
}
recompute_in_goosefs_percentage(file_info);
}
pub async fn maybe_enrich_file_block_locations(
file_info: &mut FileInfo,
router: &WorkerRouterView,
pool: Option<&Arc<WorkerClientPool>>,
config: &GoosefsConfig,
check_count: i32,
) {
let count = check_count.max(0) as usize;
if count == 0 {
ensure_block_ids_from_file_block_infos(file_info);
return;
}
if let Err(e) = enrich_file_block_locations(file_info, router, pool, config, count).await {
warn!(
error = %e,
"checkBlocks location enrichment failed; continuing with Master locations"
);
ensure_block_ids_from_file_block_infos(file_info);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::grpc::file::FileBlockInfo;
use crate::proto::grpc::{BlockInfo, WorkerNetAddress};
fn fbi(block_id: i64, offset: i64, length: i64) -> FileBlockInfo {
FileBlockInfo {
block_info: Some(BlockInfo {
block_id: Some(block_id),
length: Some(length),
max_replicas: None,
locations: vec![],
}),
offset: Some(offset),
ufs_locations: vec![],
ufs_string_locations: vec![],
}
}
fn master_loc(worker_id: i64, host: &str) -> BlockLocation {
BlockLocation {
worker_id: Some(worker_id),
worker_address: Some(WorkerNetAddress {
host: Some(host.into()),
..Default::default()
}),
}
}
#[test]
fn test_ensure_block_ids_from_file_block_infos() {
let mut fi = FileInfo {
length: Some(200),
block_size_bytes: Some(100),
file_block_infos: vec![fbi(20, 100, 100), fbi(10, 0, 100)],
block_ids: vec![],
..Default::default()
};
ensure_block_ids_from_file_block_infos(&mut fi);
assert_eq!(fi.block_ids, vec![10, 20]);
}
#[test]
fn test_ensure_block_ids_preserves_existing() {
let mut fi = FileInfo {
file_block_infos: vec![fbi(99, 0, 1)],
block_ids: vec![1, 2],
..Default::default()
};
ensure_block_ids_from_file_block_infos(&mut fi);
assert_eq!(fi.block_ids, vec![1, 2]);
}
#[test]
fn test_recompute_percentage_full() {
let mut fi = FileInfo {
length: Some(200),
file_block_infos: vec![fbi(1, 0, 100), fbi(2, 100, 100)],
..Default::default()
};
fi.file_block_infos[0]
.block_info
.as_mut()
.unwrap()
.locations = vec![master_loc(1, "w1")];
fi.file_block_infos[1]
.block_info
.as_mut()
.unwrap()
.locations = vec![master_loc(2, "w2")];
recompute_in_goosefs_percentage(&mut fi);
assert_eq!(fi.in_goose_fs_percentage, Some(100));
}
#[test]
fn test_recompute_percentage_empty_locations() {
let mut fi = FileInfo {
length: Some(200),
file_block_infos: vec![fbi(1, 0, 100), fbi(2, 100, 100)],
..Default::default()
};
recompute_in_goosefs_percentage(&mut fi);
assert_eq!(fi.in_goose_fs_percentage, Some(0));
}
#[test]
fn fill_percentage_must_cache_completed_file_is_100() {
let mut fi = FileInfo {
length: Some(4096),
completed: Some(true),
folder: Some(false),
cacheable: Some(true),
persisted: Some(false),
persistence_state: Some("NOT_PERSISTED".to_string()),
file_block_infos: vec![fbi(1, 0, 4096)],
in_goose_fs_percentage: Some(0),
..Default::default()
};
fill_in_goosefs_percentage_without_probe(&mut fi);
assert_eq!(fi.in_goose_fs_percentage, Some(100));
}
#[test]
fn fill_percentage_to_be_persisted_is_100() {
let mut fi = FileInfo {
length: Some(100),
completed: Some(true),
folder: Some(false),
cacheable: Some(true),
persisted: Some(false),
persistence_state: Some("TO_BE_PERSISTED".to_string()),
in_goose_fs_percentage: Some(0),
..Default::default()
};
fill_in_goosefs_percentage_without_probe(&mut fi);
assert_eq!(fi.in_goose_fs_percentage, Some(100));
}
#[test]
fn fill_percentage_through_file_stays_zero_without_locations() {
let mut fi = FileInfo {
length: Some(200),
completed: Some(true),
folder: Some(false),
cacheable: Some(false),
persisted: Some(true),
persistence_state: Some("PERSISTED".to_string()),
file_block_infos: vec![fbi(1, 0, 200)],
in_goose_fs_percentage: Some(0),
..Default::default()
};
fill_in_goosefs_percentage_without_probe(&mut fi);
assert_eq!(fi.in_goose_fs_percentage, Some(0));
}
#[test]
fn fill_percentage_empty_file_is_100() {
let mut fi = FileInfo {
length: Some(0),
completed: Some(true),
folder: Some(false),
in_goose_fs_percentage: Some(0),
..Default::default()
};
fill_in_goosefs_percentage_without_probe(&mut fi);
assert_eq!(fi.in_goose_fs_percentage, Some(100));
}
#[test]
fn apply_keeps_master_locations_when_probe_failed() {
let mut fi = FileInfo {
length: Some(100),
file_block_infos: vec![fbi(1, 0, 100)],
..Default::default()
};
fi.file_block_infos[0]
.block_info
.as_mut()
.unwrap()
.locations = vec![master_loc(9, "master-known")];
apply_probed_locations(
&mut fi,
&ProbedLocations {
locations: HashMap::new(),
probed_ok: HashSet::new(),
},
);
let locs = &fi.file_block_infos[0]
.block_info
.as_ref()
.unwrap()
.locations;
assert_eq!(locs.len(), 1);
assert_eq!(locs[0].worker_id, Some(9));
assert_eq!(fi.in_goose_fs_percentage, Some(100));
}
#[test]
fn apply_overwrites_with_empty_on_authoritative_miss() {
let mut fi = FileInfo {
length: Some(100),
file_block_infos: vec![fbi(1, 0, 100)],
..Default::default()
};
fi.file_block_infos[0]
.block_info
.as_mut()
.unwrap()
.locations = vec![master_loc(9, "stale")];
let mut locations = HashMap::new();
locations.insert(1, Vec::new());
apply_probed_locations(
&mut fi,
&ProbedLocations {
locations,
probed_ok: HashSet::from([1]),
},
);
assert!(fi.file_block_infos[0]
.block_info
.as_ref()
.unwrap()
.locations
.is_empty());
assert_eq!(fi.in_goose_fs_percentage, Some(0));
}
#[test]
fn apply_overwrites_with_probed_locations() {
let mut fi = FileInfo {
length: Some(100),
file_block_infos: vec![fbi(1, 0, 100)],
..Default::default()
};
fi.file_block_infos[0]
.block_info
.as_mut()
.unwrap()
.locations = vec![master_loc(9, "stale")];
let mut locations = HashMap::new();
locations.insert(1, vec![master_loc(3, "probed")]);
apply_probed_locations(
&mut fi,
&ProbedLocations {
locations,
probed_ok: HashSet::from([1]),
},
);
let locs = &fi.file_block_infos[0]
.block_info
.as_ref()
.unwrap()
.locations;
assert_eq!(locs.len(), 1);
assert_eq!(locs[0].worker_id, Some(3));
assert_eq!(fi.in_goose_fs_percentage, Some(100));
}
}