use tonic::Status;
use crate::ir::{
ComparisonOp, LogicalDelete, LogicalFilter, LogicalPagination, LogicalRead, LogicalSort,
LogicalValue, NullOrder, SortDirection,
};
use crate::proto::udb::core::storage::entity::v1 as storage_entity_pb;
use super::StorageServiceImpl;
use super::config::FILE_MSG;
use super::model::file_from_json;
use super::store::{file_eq, file_projection, logical_string};
impl StorageServiceImpl {
pub(crate) async fn reap_orphans(
&self,
older_than_minutes: i64,
batch_size: i64,
) -> Result<u64, Status> {
let runtime = self.require_runtime()?;
let batch_size = batch_size.clamp(1, 10_000);
let context = crate::RequestContext {
correlation_id: "storage-orphan-reaper".to_string(),
..crate::RequestContext::default()
};
let cutoff = chrono::Utc::now() - chrono::Duration::minutes(older_than_minutes.max(0));
let read = LogicalRead {
message_type: FILE_MSG.to_string(),
filter: Some(LogicalFilter::And(vec![
file_eq("status", "PENDING"),
LogicalFilter::Comparison {
field: "created_at".to_string(),
op: ComparisonOp::Lt,
value: LogicalValue::Timestamp(cutoff),
},
])),
projection: Some(file_projection()),
sort: vec![LogicalSort {
field: "created_at".to_string(),
direction: SortDirection::Asc,
nulls: NullOrder::Default,
}],
include: Vec::new(),
pagination: Some(LogicalPagination::limit(batch_size as u32)),
};
let doomed: Vec<storage_entity_pb::File> = runtime
.native_entity_read_for_service("storage", &context, read)
.await?
.iter()
.map(file_from_json)
.collect();
if doomed.is_empty() {
return Ok(0);
}
let delete = LogicalDelete {
message_type: FILE_MSG.to_string(),
filter: LogicalFilter::InList {
field: "file_id".to_string(),
values: doomed
.iter()
.map(|f| logical_string(f.file_id.as_str()))
.collect(),
},
return_fields: Vec::new(),
};
runtime
.native_entity_delete_for_service("storage", &context, delete)
.await?;
for file in &doomed {
self.delete_object_bytes(&file.project_id, &file.object_key)
.await;
}
Ok(doomed.len() as u64)
}
}