mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-31 01:09:23 +00:00
fix(heal): stop O(window) memmove in the heal result window (#6272)
This commit is contained in:
+178
-194
@@ -227,11 +227,11 @@ impl ForegroundPressure {
|
|||||||
struct CompletedHealStatus {
|
struct CompletedHealStatus {
|
||||||
heal_type: HealType,
|
heal_type: HealType,
|
||||||
status: HealTaskStatus,
|
status: HealTaskStatus,
|
||||||
result_items: Vec<HealResultItem>,
|
|
||||||
result_items_truncated: bool,
|
result_items_truncated: bool,
|
||||||
completed_at: SystemTime,
|
completed_at: SystemTime,
|
||||||
/// Sequence-stamped retained window, archived with the completion so
|
/// Sequence-stamped retained window, archived with the completion so
|
||||||
/// incremental consumers keep their cursor across the transition (HS-06).
|
/// incremental consumers keep their cursor across the transition (HS-06).
|
||||||
|
/// The un-stamped legacy view is derived from it on demand.
|
||||||
seqed_items: Vec<(u64, HealResultItem)>,
|
seqed_items: Vec<(u64, HealResultItem)>,
|
||||||
next_seq: u64,
|
next_seq: u64,
|
||||||
min_seq: u64,
|
min_seq: u64,
|
||||||
@@ -293,7 +293,7 @@ fn empty_task_report(status: HealTaskStatus) -> HealTaskReport {
|
|||||||
fn completed_task_report(completed: &CompletedHealStatus, since: Option<u64>) -> HealTaskReport {
|
fn completed_task_report(completed: &CompletedHealStatus, since: Option<u64>) -> HealTaskReport {
|
||||||
let mut lagged = false;
|
let mut lagged = false;
|
||||||
let result_items = match since {
|
let result_items = match since {
|
||||||
None => completed.result_items.clone(),
|
None => completed.seqed_items.iter().map(|(_, item)| item.clone()).collect(),
|
||||||
Some(cursor) => {
|
Some(cursor) => {
|
||||||
if cursor + 1 < completed.min_seq {
|
if cursor + 1 < completed.min_seq {
|
||||||
lagged = true;
|
lagged = true;
|
||||||
@@ -1027,8 +1027,10 @@ pub struct HealManager {
|
|||||||
active_heals: Arc<Mutex<HashMap<String, Arc<HealTask>>>>,
|
active_heals: Arc<Mutex<HashMap<String, Arc<HealTask>>>>,
|
||||||
/// Heal queue (priority-based)
|
/// Heal queue (priority-based)
|
||||||
heal_queue: Arc<Mutex<PriorityHealQueue>>,
|
heal_queue: Arc<Mutex<PriorityHealQueue>>,
|
||||||
/// Recently completed heal statuses retained for status queries.
|
/// Recently completed heal statuses retained for status queries. Values
|
||||||
completed_heals: Arc<Mutex<HashMap<String, CompletedHealStatus>>>,
|
/// are shared so the lookup helper can hand a completed entry to a
|
||||||
|
/// caller without cloning the retained result window.
|
||||||
|
completed_heals: Arc<Mutex<HashMap<String, Arc<CompletedHealStatus>>>>,
|
||||||
/// Client tokens merged into an existing task id.
|
/// Client tokens merged into an existing task id.
|
||||||
task_aliases: Arc<Mutex<HashMap<String, HealTaskAlias>>>,
|
task_aliases: Arc<Mutex<HashMap<String, HealTaskAlias>>>,
|
||||||
/// Heal tasks waiting for a retry backoff to expire.
|
/// Heal tasks waiting for a retry backoff to expire.
|
||||||
@@ -1051,10 +1053,21 @@ pub struct HealManager {
|
|||||||
workload_provider: Option<WorkloadSnapshotProviderRef>,
|
workload_provider: Option<WorkloadSnapshotProviderRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where a task-id lookup resolved. The variants carry the resolved state
|
||||||
|
/// so both the status and the report adapters can consume one shared
|
||||||
|
/// cascade without re-locking.
|
||||||
|
enum TaskStateLookup {
|
||||||
|
Active(Arc<HealTask>),
|
||||||
|
Retrying(HealTaskStatus),
|
||||||
|
Completed(Arc<CompletedHealStatus>),
|
||||||
|
Queued,
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
struct HealQueueContext<'a> {
|
struct HealQueueContext<'a> {
|
||||||
heal_queue: &'a Arc<Mutex<PriorityHealQueue>>,
|
heal_queue: &'a Arc<Mutex<PriorityHealQueue>>,
|
||||||
active_heals: &'a Arc<Mutex<HashMap<String, Arc<HealTask>>>>,
|
active_heals: &'a Arc<Mutex<HashMap<String, Arc<HealTask>>>>,
|
||||||
completed_heals: &'a Arc<Mutex<HashMap<String, CompletedHealStatus>>>,
|
completed_heals: &'a Arc<Mutex<HashMap<String, Arc<CompletedHealStatus>>>>,
|
||||||
retrying_heals: &'a Arc<Mutex<HashMap<String, RetryingHeal>>>,
|
retrying_heals: &'a Arc<Mutex<HashMap<String, RetryingHeal>>>,
|
||||||
replacement_recovery_anchors: &'a Arc<std::sync::Mutex<HashMap<String, String>>>,
|
replacement_recovery_anchors: &'a Arc<std::sync::Mutex<HashMap<String, String>>>,
|
||||||
config: &'a Arc<RwLock<HealConfig>>,
|
config: &'a Arc<RwLock<HealConfig>>,
|
||||||
@@ -2160,47 +2173,79 @@ impl HealManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get task status
|
/// Get task status
|
||||||
pub async fn get_task_status(&self, task_id: &str) -> Result<HealTaskStatus> {
|
/// Ordered task-state lookup shared by every status/report query. The
|
||||||
let canonical_task_id = self.canonical_task_id(task_id).await;
|
/// map precedence mirrors the historical per-method cascades exactly:
|
||||||
|
/// active, then retrying, then completed — where a completed entry in a
|
||||||
|
/// retrying state outranks the queue so a retrying task reports
|
||||||
|
/// Retrying, never Pending — then the queue, and finally a terminal
|
||||||
|
/// completed entry. `heal_path` additionally constrains the map matches
|
||||||
|
/// the way the `*_for_path` variants always have.
|
||||||
|
async fn lookup_task_state(&self, canonical_task_id: &str, heal_path: Option<&str>) -> TaskStateLookup {
|
||||||
|
let matches_path = |heal_type: &HealType| heal_path.is_none_or(|path| heal_type_matches_path(heal_type, path));
|
||||||
|
|
||||||
{
|
{
|
||||||
let active_heals = self.active_heals.lock().await;
|
let active_heals = self.active_heals.lock().await;
|
||||||
if let Some(task) = active_heals.get(&canonical_task_id) {
|
if let Some(task) = active_heals
|
||||||
return Ok(task.get_status().await);
|
.get(canonical_task_id)
|
||||||
|
.filter(|task| matches_path(&task.heal_type))
|
||||||
|
{
|
||||||
|
return TaskStateLookup::Active(Arc::clone(task));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let retrying_heals = self.retrying_heals.lock().await;
|
let retrying_heals = self.retrying_heals.lock().await;
|
||||||
if let Some(retrying) = retrying_heals.get(&canonical_task_id) {
|
if let Some(retrying) = retrying_heals
|
||||||
return Ok(retrying.status());
|
.get(canonical_task_id)
|
||||||
|
.filter(|retrying| matches_path(&retrying.request.heal_type))
|
||||||
|
{
|
||||||
|
return TaskStateLookup::Retrying(retrying.status());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One completed-map pass (single lock + prune): a retrying completion
|
||||||
|
// returns immediately; a terminal completion is held back until the
|
||||||
|
// queue has been checked, so queued work outranks it.
|
||||||
|
let mut terminal_completed: Option<Arc<CompletedHealStatus>> = None;
|
||||||
|
{
|
||||||
|
let mut completed_heals = self.completed_heals.lock().await;
|
||||||
|
prune_completed_heal_statuses(&mut completed_heals);
|
||||||
|
if let Some(completed) = completed_heals.get(canonical_task_id).filter(|c| matches_path(&c.heal_type)) {
|
||||||
|
if completed_status_is_retrying(&completed.status) {
|
||||||
|
return TaskStateLookup::Completed(Arc::clone(completed));
|
||||||
|
}
|
||||||
|
terminal_completed = Some(Arc::clone(completed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut completed_heals = self.completed_heals.lock().await;
|
let queue = self.heal_queue.lock().await;
|
||||||
prune_completed_heal_statuses(&mut completed_heals);
|
let queued = match heal_path {
|
||||||
if let Some(completed) = completed_heals.get(&canonical_task_id)
|
Some(path) => queue.contains_request_id_matching_path(canonical_task_id, path),
|
||||||
&& completed_status_is_retrying(&completed.status)
|
None => queue.contains_request_id(canonical_task_id),
|
||||||
{
|
};
|
||||||
return Ok(completed.status.clone());
|
if queued {
|
||||||
|
return TaskStateLookup::Queued;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let queue = self.heal_queue.lock().await;
|
match terminal_completed {
|
||||||
if queue.contains_request_id(&canonical_task_id) {
|
Some(completed) => TaskStateLookup::Completed(completed),
|
||||||
return Ok(HealTaskStatus::Pending);
|
None => TaskStateLookup::NotFound,
|
||||||
}
|
}
|
||||||
drop(queue);
|
}
|
||||||
|
|
||||||
let mut completed_heals = self.completed_heals.lock().await;
|
pub async fn get_task_status(&self, task_id: &str) -> Result<HealTaskStatus> {
|
||||||
prune_completed_heal_statuses(&mut completed_heals);
|
let canonical_task_id = self.canonical_task_id(task_id).await;
|
||||||
if let Some(completed) = completed_heals.get(&canonical_task_id) {
|
match self.lookup_task_state(&canonical_task_id, None).await {
|
||||||
return Ok(completed.status.clone());
|
TaskStateLookup::Active(task) => Ok(task.get_status().await),
|
||||||
|
TaskStateLookup::Retrying(status) => Ok(status),
|
||||||
|
TaskStateLookup::Completed(completed) => Ok(completed.status.clone()),
|
||||||
|
TaskStateLookup::Queued => Ok(HealTaskStatus::Pending),
|
||||||
|
TaskStateLookup::NotFound => Err(Error::TaskNotFound {
|
||||||
|
task_id: task_id.to_string(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(Error::TaskNotFound {
|
|
||||||
task_id: task_id.to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_task_report(&self, task_id: &str) -> Result<HealTaskReport> {
|
pub async fn get_task_report(&self, task_id: &str) -> Result<HealTaskReport> {
|
||||||
@@ -2212,46 +2257,15 @@ impl HealManager {
|
|||||||
/// full-snapshot semantics.
|
/// full-snapshot semantics.
|
||||||
pub async fn get_task_report_since(&self, task_id: &str, since: Option<u64>) -> Result<HealTaskReport> {
|
pub async fn get_task_report_since(&self, task_id: &str, since: Option<u64>) -> Result<HealTaskReport> {
|
||||||
let canonical_task_id = self.canonical_task_id(task_id).await;
|
let canonical_task_id = self.canonical_task_id(task_id).await;
|
||||||
{
|
match self.lookup_task_state(&canonical_task_id, None).await {
|
||||||
let active_heals = self.active_heals.lock().await;
|
TaskStateLookup::Active(task) => Ok(active_task_report(&task, since).await),
|
||||||
if let Some(task) = active_heals.get(&canonical_task_id) {
|
TaskStateLookup::Retrying(status) => Ok(empty_task_report(status)),
|
||||||
return Ok(active_task_report(task, since).await);
|
TaskStateLookup::Completed(completed) => Ok(completed_task_report(&completed, since)),
|
||||||
}
|
TaskStateLookup::Queued => Ok(empty_task_report(HealTaskStatus::Pending)),
|
||||||
|
TaskStateLookup::NotFound => Err(Error::TaskNotFound {
|
||||||
|
task_id: task_id.to_string(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
let retrying_heals = self.retrying_heals.lock().await;
|
|
||||||
if let Some(retrying) = retrying_heals.get(&canonical_task_id) {
|
|
||||||
return Ok(empty_task_report(retrying.status()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut completed_heals = self.completed_heals.lock().await;
|
|
||||||
prune_completed_heal_statuses(&mut completed_heals);
|
|
||||||
if let Some(completed) = completed_heals.get(&canonical_task_id)
|
|
||||||
&& completed_status_is_retrying(&completed.status)
|
|
||||||
{
|
|
||||||
return Ok(completed_task_report(completed, since));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = self.heal_queue.lock().await;
|
|
||||||
if queue.contains_request_id(&canonical_task_id) {
|
|
||||||
return Ok(empty_task_report(HealTaskStatus::Pending));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut completed_heals = self.completed_heals.lock().await;
|
|
||||||
prune_completed_heal_statuses(&mut completed_heals);
|
|
||||||
if let Some(completed) = completed_heals.get(&canonical_task_id) {
|
|
||||||
return Ok(completed_task_report(completed, since));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(Error::TaskNotFound {
|
|
||||||
task_id: task_id.to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_task_report_for_path(&self, heal_path: &str, task_id: &str) -> Result<HealTaskReport> {
|
pub async fn get_task_report_for_path(&self, heal_path: &str, task_id: &str) -> Result<HealTaskReport> {
|
||||||
@@ -2266,59 +2280,20 @@ impl HealManager {
|
|||||||
since: Option<u64>,
|
since: Option<u64>,
|
||||||
) -> Result<HealTaskReport> {
|
) -> Result<HealTaskReport> {
|
||||||
let canonical_task_id = self.canonical_task_id(task_id).await;
|
let canonical_task_id = self.canonical_task_id(task_id).await;
|
||||||
{
|
match self.lookup_task_state(&canonical_task_id, Some(heal_path)).await {
|
||||||
let active_heals = self.active_heals.lock().await;
|
TaskStateLookup::Active(task) => Ok(active_task_report(&task, since).await),
|
||||||
if let Some(task) = active_heals.get(&canonical_task_id)
|
TaskStateLookup::Retrying(status) => Ok(empty_task_report(status)),
|
||||||
&& heal_type_matches_path(&task.heal_type, heal_path)
|
TaskStateLookup::Completed(completed) => Ok(completed_task_report(&completed, since)),
|
||||||
{
|
TaskStateLookup::Queued => Ok(empty_task_report(HealTaskStatus::Pending)),
|
||||||
return Ok(active_task_report(task, since).await);
|
TaskStateLookup::NotFound => {
|
||||||
|
if self.path_has_task(heal_path).await {
|
||||||
|
return Err(Error::InvalidClientToken);
|
||||||
|
}
|
||||||
|
Err(Error::TaskNotFound {
|
||||||
|
task_id: task_id.to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
let retrying_heals = self.retrying_heals.lock().await;
|
|
||||||
if let Some(retrying) = retrying_heals.get(&canonical_task_id)
|
|
||||||
&& heal_type_matches_path(&retrying.request.heal_type, heal_path)
|
|
||||||
{
|
|
||||||
return Ok(empty_task_report(retrying.status()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut completed_heals = self.completed_heals.lock().await;
|
|
||||||
prune_completed_heal_statuses(&mut completed_heals);
|
|
||||||
if let Some(completed) = completed_heals.get(&canonical_task_id)
|
|
||||||
&& heal_type_matches_path(&completed.heal_type, heal_path)
|
|
||||||
&& completed_status_is_retrying(&completed.status)
|
|
||||||
{
|
|
||||||
return Ok(completed_task_report(completed, since));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = self.heal_queue.lock().await;
|
|
||||||
if queue.contains_request_id_matching_path(&canonical_task_id, heal_path) {
|
|
||||||
return Ok(empty_task_report(HealTaskStatus::Pending));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut completed_heals = self.completed_heals.lock().await;
|
|
||||||
prune_completed_heal_statuses(&mut completed_heals);
|
|
||||||
if let Some(completed) = completed_heals.get(&canonical_task_id)
|
|
||||||
&& heal_type_matches_path(&completed.heal_type, heal_path)
|
|
||||||
{
|
|
||||||
return Ok(completed_task_report(completed, since));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.path_has_task(heal_path).await {
|
|
||||||
return Err(Error::InvalidClientToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(Error::TaskNotFound {
|
|
||||||
task_id: task_id.to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get task status for a path-bound client token.
|
/// Get task status for a path-bound client token.
|
||||||
@@ -2328,59 +2303,20 @@ impl HealManager {
|
|||||||
/// recently completed task, a different token is invalid for that path.
|
/// recently completed task, a different token is invalid for that path.
|
||||||
pub async fn get_task_status_for_path(&self, heal_path: &str, task_id: &str) -> Result<HealTaskStatus> {
|
pub async fn get_task_status_for_path(&self, heal_path: &str, task_id: &str) -> Result<HealTaskStatus> {
|
||||||
let canonical_task_id = self.canonical_task_id(task_id).await;
|
let canonical_task_id = self.canonical_task_id(task_id).await;
|
||||||
{
|
match self.lookup_task_state(&canonical_task_id, Some(heal_path)).await {
|
||||||
let active_heals = self.active_heals.lock().await;
|
TaskStateLookup::Active(task) => Ok(task.get_status().await),
|
||||||
if let Some(task) = active_heals.get(&canonical_task_id)
|
TaskStateLookup::Retrying(status) => Ok(status),
|
||||||
&& heal_type_matches_path(&task.heal_type, heal_path)
|
TaskStateLookup::Completed(completed) => Ok(completed.status.clone()),
|
||||||
{
|
TaskStateLookup::Queued => Ok(HealTaskStatus::Pending),
|
||||||
return Ok(task.get_status().await);
|
TaskStateLookup::NotFound => {
|
||||||
|
if self.path_has_task(heal_path).await {
|
||||||
|
return Err(Error::InvalidClientToken);
|
||||||
|
}
|
||||||
|
Err(Error::TaskNotFound {
|
||||||
|
task_id: task_id.to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
let retrying_heals = self.retrying_heals.lock().await;
|
|
||||||
if let Some(retrying) = retrying_heals.get(&canonical_task_id)
|
|
||||||
&& heal_type_matches_path(&retrying.request.heal_type, heal_path)
|
|
||||||
{
|
|
||||||
return Ok(retrying.status());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut completed_heals = self.completed_heals.lock().await;
|
|
||||||
prune_completed_heal_statuses(&mut completed_heals);
|
|
||||||
if let Some(completed) = completed_heals.get(&canonical_task_id)
|
|
||||||
&& heal_type_matches_path(&completed.heal_type, heal_path)
|
|
||||||
&& completed_status_is_retrying(&completed.status)
|
|
||||||
{
|
|
||||||
return Ok(completed.status.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = self.heal_queue.lock().await;
|
|
||||||
if queue.contains_request_id_matching_path(&canonical_task_id, heal_path) {
|
|
||||||
return Ok(HealTaskStatus::Pending);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut completed_heals = self.completed_heals.lock().await;
|
|
||||||
prune_completed_heal_statuses(&mut completed_heals);
|
|
||||||
if let Some(completed) = completed_heals.get(&canonical_task_id)
|
|
||||||
&& heal_type_matches_path(&completed.heal_type, heal_path)
|
|
||||||
{
|
|
||||||
return Ok(completed.status.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.path_has_task(heal_path).await {
|
|
||||||
return Err(Error::InvalidClientToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(Error::TaskNotFound {
|
|
||||||
task_id: task_id.to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn path_has_task(&self, heal_path: &str) -> bool {
|
async fn path_has_task(&self, heal_path: &str) -> bool {
|
||||||
@@ -3503,20 +3439,23 @@ impl HealManager {
|
|||||||
completed_task.get_status().await
|
completed_task.get_status().await
|
||||||
};
|
};
|
||||||
let completed_progress = completed_task.get_progress().await;
|
let completed_progress = completed_task.get_progress().await;
|
||||||
let final_window = completed_task.get_result_items_since(None).await;
|
// Single snapshot of the retained window: the task is
|
||||||
|
// finished and already off the active map, so there is
|
||||||
|
// no concurrent writer to race with.
|
||||||
|
let seqed_items = completed_task.get_seqed_result_items().await;
|
||||||
|
let (next_seq, min_seq) = completed_task.result_seq_cursors();
|
||||||
let completed_status_entry = CompletedHealStatus {
|
let completed_status_entry = CompletedHealStatus {
|
||||||
heal_type: completed_task.heal_type.clone(),
|
heal_type: completed_task.heal_type.clone(),
|
||||||
status: completed_status.clone(),
|
status: completed_status.clone(),
|
||||||
result_items: final_window.items.clone(),
|
|
||||||
result_items_truncated: completed_task.result_items_truncated(),
|
result_items_truncated: completed_task.result_items_truncated(),
|
||||||
completed_at: SystemTime::now(),
|
completed_at: SystemTime::now(),
|
||||||
seqed_items: completed_task.get_seqed_result_items().await,
|
seqed_items,
|
||||||
next_seq: final_window.next_seq,
|
next_seq,
|
||||||
min_seq: final_window.min_seq,
|
min_seq,
|
||||||
};
|
};
|
||||||
let mut completed_heals_guard = completed_heals_clone.lock().await;
|
let mut completed_heals_guard = completed_heals_clone.lock().await;
|
||||||
prune_completed_heal_statuses(&mut completed_heals_guard);
|
prune_completed_heal_statuses(&mut completed_heals_guard);
|
||||||
completed_heals_guard.insert(task_id.clone(), completed_status_entry);
|
completed_heals_guard.insert(task_id.clone(), Arc::new(completed_status_entry));
|
||||||
// update statistics
|
// update statistics
|
||||||
let mut stats = statistics_clone.write().await;
|
let mut stats = statistics_clone.write().await;
|
||||||
match completed_status {
|
match completed_status {
|
||||||
@@ -3808,7 +3747,7 @@ fn heal_request_set_key_for_task(task: &HealTask) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune_completed_heal_statuses(completed_heals: &mut HashMap<String, CompletedHealStatus>) {
|
fn prune_completed_heal_statuses(completed_heals: &mut HashMap<String, Arc<CompletedHealStatus>>) {
|
||||||
let Ok(now) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) else {
|
let Ok(now) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -5275,19 +5214,18 @@ mod tests {
|
|||||||
);
|
);
|
||||||
manager.completed_heals.lock().await.insert(
|
manager.completed_heals.lock().await.insert(
|
||||||
task_id,
|
task_id,
|
||||||
CompletedHealStatus {
|
Arc::new(CompletedHealStatus {
|
||||||
heal_type: request.heal_type,
|
heal_type: request.heal_type,
|
||||||
status: HealTaskStatus::Retrying {
|
status: HealTaskStatus::Retrying {
|
||||||
error: "Lock acquisition timeout".to_string(),
|
error: "Lock acquisition timeout".to_string(),
|
||||||
retry_attempt: request.retry_attempts,
|
retry_attempt: request.retry_attempts,
|
||||||
},
|
},
|
||||||
result_items: Vec::new(),
|
|
||||||
result_items_truncated: false,
|
result_items_truncated: false,
|
||||||
seqed_items: Vec::new(),
|
seqed_items: Vec::new(),
|
||||||
next_seq: 0,
|
next_seq: 0,
|
||||||
min_seq: 0,
|
min_seq: 0,
|
||||||
completed_at: SystemTime::now(),
|
completed_at: SystemTime::now(),
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
cancel_token
|
cancel_token
|
||||||
}
|
}
|
||||||
@@ -5985,6 +5923,47 @@ mod tests {
|
|||||||
assert!(report.result_items.is_empty());
|
assert!(report.result_items.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_retrying_completion_outranks_the_queue_for_the_same_id() {
|
||||||
|
let storage: Arc<dyn HealStorageAPI> = Arc::new(MockStorage);
|
||||||
|
let manager = HealManager::new(storage, None);
|
||||||
|
|
||||||
|
// A completed entry recorded in a Retrying state for a task whose
|
||||||
|
// request is also (still) queued under the same id: the retrying
|
||||||
|
// completion must win the lookup, or the task would read back as
|
||||||
|
// Pending while it is actually waiting out a retry backoff.
|
||||||
|
let request = HealRequest::object("bucket".to_string(), "object".to_string(), None);
|
||||||
|
let task_id = request.id.clone();
|
||||||
|
manager.completed_heals.lock().await.insert(
|
||||||
|
task_id.clone(),
|
||||||
|
Arc::new(CompletedHealStatus {
|
||||||
|
heal_type: request.heal_type.clone(),
|
||||||
|
status: HealTaskStatus::Retrying {
|
||||||
|
error: "transient disk failure".to_string(),
|
||||||
|
retry_attempt: 1,
|
||||||
|
},
|
||||||
|
result_items_truncated: false,
|
||||||
|
seqed_items: Vec::new(),
|
||||||
|
next_seq: 0,
|
||||||
|
min_seq: 0,
|
||||||
|
completed_at: SystemTime::now(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
manager.heal_queue.lock().await.push(HealRequest {
|
||||||
|
id: task_id.clone(),
|
||||||
|
heal_type: request.heal_type,
|
||||||
|
..request
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
manager.get_task_status(&task_id).await.expect("task must resolve"),
|
||||||
|
HealTaskStatus::Retrying {
|
||||||
|
error: "transient disk failure".to_string(),
|
||||||
|
retry_attempt: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_task_status_reads_recent_completed_status() {
|
async fn test_get_task_status_reads_recent_completed_status() {
|
||||||
let storage: Arc<dyn HealStorageAPI> = Arc::new(MockStorage);
|
let storage: Arc<dyn HealStorageAPI> = Arc::new(MockStorage);
|
||||||
@@ -5992,18 +5971,17 @@ mod tests {
|
|||||||
|
|
||||||
manager.completed_heals.lock().await.insert(
|
manager.completed_heals.lock().await.insert(
|
||||||
"completed-token".to_string(),
|
"completed-token".to_string(),
|
||||||
CompletedHealStatus {
|
Arc::new(CompletedHealStatus {
|
||||||
heal_type: HealType::Bucket {
|
heal_type: HealType::Bucket {
|
||||||
bucket: "bucket".to_string(),
|
bucket: "bucket".to_string(),
|
||||||
},
|
},
|
||||||
status: HealTaskStatus::Completed,
|
status: HealTaskStatus::Completed,
|
||||||
result_items: Vec::new(),
|
|
||||||
result_items_truncated: false,
|
result_items_truncated: false,
|
||||||
seqed_items: Vec::new(),
|
seqed_items: Vec::new(),
|
||||||
next_seq: 0,
|
next_seq: 0,
|
||||||
min_seq: 0,
|
min_seq: 0,
|
||||||
completed_at: SystemTime::now(),
|
completed_at: SystemTime::now(),
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -6022,25 +6000,27 @@ mod tests {
|
|||||||
|
|
||||||
manager.completed_heals.lock().await.insert(
|
manager.completed_heals.lock().await.insert(
|
||||||
"completed-token".to_string(),
|
"completed-token".to_string(),
|
||||||
CompletedHealStatus {
|
Arc::new(CompletedHealStatus {
|
||||||
heal_type: HealType::Object {
|
heal_type: HealType::Object {
|
||||||
bucket: "bucket".to_string(),
|
bucket: "bucket".to_string(),
|
||||||
object: "object".to_string(),
|
object: "object".to_string(),
|
||||||
version_id: None,
|
version_id: None,
|
||||||
},
|
},
|
||||||
status: HealTaskStatus::Completed,
|
status: HealTaskStatus::Completed,
|
||||||
result_items: vec![HealResultItem {
|
|
||||||
bucket: "bucket".to_string(),
|
|
||||||
object: "object".to_string(),
|
|
||||||
object_size: 1024,
|
|
||||||
..Default::default()
|
|
||||||
}],
|
|
||||||
result_items_truncated: true,
|
result_items_truncated: true,
|
||||||
seqed_items: Vec::new(),
|
seqed_items: vec![(
|
||||||
next_seq: 0,
|
1,
|
||||||
min_seq: 0,
|
HealResultItem {
|
||||||
|
bucket: "bucket".to_string(),
|
||||||
|
object: "object".to_string(),
|
||||||
|
object_size: 1024,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)],
|
||||||
|
next_seq: 2,
|
||||||
|
min_seq: 1,
|
||||||
completed_at: SystemTime::now(),
|
completed_at: SystemTime::now(),
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let report = manager
|
let report = manager
|
||||||
@@ -6052,6 +6032,10 @@ mod tests {
|
|||||||
assert_eq!(report.status, HealTaskStatus::Completed);
|
assert_eq!(report.status, HealTaskStatus::Completed);
|
||||||
assert_eq!(report.result_items.len(), 1);
|
assert_eq!(report.result_items.len(), 1);
|
||||||
assert_eq!(report.result_items[0].object_size, 1024);
|
assert_eq!(report.result_items[0].object_size, 1024);
|
||||||
|
// The archived cursors pass through to the report so an incremental
|
||||||
|
// consumer can resume against the next expected sequence.
|
||||||
|
assert_eq!(report.next_seq, 2);
|
||||||
|
assert_eq!(report.min_seq, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ use rustfs_madmin::heal_commands::HealResultItem;
|
|||||||
use rustfs_utils::path::SLASH_SEPARATOR;
|
use rustfs_utils::path::SLASH_SEPARATOR;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
future::Future,
|
future::Future,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
@@ -384,7 +385,7 @@ pub struct HealTask {
|
|||||||
/// monotonically increasing sequence number for incremental consumption
|
/// monotonically increasing sequence number for incremental consumption
|
||||||
/// (the client passes the last seen seq back and receives only newer
|
/// (the client passes the last seen seq back and receives only newer
|
||||||
/// items; see `get_result_items_since`).
|
/// items; see `get_result_items_since`).
|
||||||
pub result_items: Arc<RwLock<Vec<(u64, HealResultItem)>>>,
|
pub result_items: Arc<RwLock<VecDeque<(u64, HealResultItem)>>>,
|
||||||
/// Next sequence number to assign; starts at 1.
|
/// Next sequence number to assign; starts at 1.
|
||||||
next_item_seq: Arc<AtomicU64>,
|
next_item_seq: Arc<AtomicU64>,
|
||||||
/// Sequence number of the oldest item still inside the retention window;
|
/// Sequence number of the oldest item still inside the retention window;
|
||||||
@@ -440,7 +441,7 @@ impl HealTask {
|
|||||||
replacement_resume_endpoint: None,
|
replacement_resume_endpoint: None,
|
||||||
status: Arc::new(RwLock::new(HealTaskStatus::Pending)),
|
status: Arc::new(RwLock::new(HealTaskStatus::Pending)),
|
||||||
progress: Arc::new(RwLock::new(HealProgress::new())),
|
progress: Arc::new(RwLock::new(HealProgress::new())),
|
||||||
result_items: Arc::new(RwLock::new(Vec::new())),
|
result_items: Arc::new(RwLock::new(VecDeque::with_capacity(MAX_RETAINED_HEAL_RESULT_ITEMS))),
|
||||||
next_item_seq: Arc::new(AtomicU64::new(1)),
|
next_item_seq: Arc::new(AtomicU64::new(1)),
|
||||||
min_available_seq: Arc::new(AtomicU64::new(1)),
|
min_available_seq: Arc::new(AtomicU64::new(1)),
|
||||||
result_items_truncated: Arc::new(AtomicBool::new(false)),
|
result_items_truncated: Arc::new(AtomicBool::new(false)),
|
||||||
@@ -931,7 +932,14 @@ impl HealTask {
|
|||||||
/// Sequence-stamped retained window, used when archiving a completed
|
/// Sequence-stamped retained window, used when archiving a completed
|
||||||
/// task so incremental cursors survive the transition (HS-06).
|
/// task so incremental cursors survive the transition (HS-06).
|
||||||
pub async fn get_seqed_result_items(&self) -> Vec<(u64, HealResultItem)> {
|
pub async fn get_seqed_result_items(&self) -> Vec<(u64, HealResultItem)> {
|
||||||
self.result_items.read().await.clone()
|
self.result_items.read().await.iter().cloned().collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sequence cursors of the retained window (next to assign, oldest
|
||||||
|
/// retained) — the same pair `get_result_items_since` reports, without
|
||||||
|
/// copying the items. Used when archiving a finished task.
|
||||||
|
pub fn result_seq_cursors(&self) -> (u64, u64) {
|
||||||
|
(self.next_item_seq.load(Ordering::Relaxed), self.min_available_seq.load(Ordering::Relaxed))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Incremental result window (HS-06): `since = None` returns the full
|
/// Incremental result window (HS-06): `since = None` returns the full
|
||||||
@@ -974,14 +982,14 @@ impl HealTask {
|
|||||||
let seq = self.next_item_seq.fetch_add(1, Ordering::Relaxed);
|
let seq = self.next_item_seq.fetch_add(1, Ordering::Relaxed);
|
||||||
let mut result_items = self.result_items.write().await;
|
let mut result_items = self.result_items.write().await;
|
||||||
if result_items.len() < MAX_RETAINED_HEAL_RESULT_ITEMS {
|
if result_items.len() < MAX_RETAINED_HEAL_RESULT_ITEMS {
|
||||||
result_items.push((seq, result));
|
result_items.push_back((seq, result));
|
||||||
} else {
|
} else {
|
||||||
// Slide the window: the oldest item leaves and the cursor for the
|
// Slide the window: the oldest item leaves and the cursor for the
|
||||||
// oldest still-available item moves forward with it.
|
// oldest still-available item moves forward with it.
|
||||||
result_items.remove(0);
|
result_items.pop_front();
|
||||||
self.min_available_seq
|
self.min_available_seq
|
||||||
.store(result_items.first().map_or(seq, |(oldest, _)| *oldest), Ordering::Relaxed);
|
.store(result_items.front().map_or(seq, |(oldest, _)| *oldest), Ordering::Relaxed);
|
||||||
result_items.push((seq, result));
|
result_items.push_back((seq, result));
|
||||||
self.result_items_truncated.store(true, Ordering::Relaxed);
|
self.result_items_truncated.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user