Add last_failure to worker state
The `WorkerSnapshot` now includes `last_failure`, which tracks the time of the last recorded error. This allows the UI to display a more accurate status, especially when a failure occurs after a recent success. The `pick_footer_status` function has been updated to prioritize `last_failure`. If a failure occurred more recently than the last success, the footer will show `Offline`, regardless of the success age. This addresses scenarios where the server might have temporarily failed, and the UI should reflect that immediately. The `run_loop` function now updates `last_failure` upon transient or terminal upload errors and poll failures. This ensures that the new state information is correctly propagated.
This commit is contained in:
@@ -65,8 +65,13 @@ pub struct WorkerSnapshot {
|
||||
/// `Uploading`.
|
||||
pub queue_len: usize,
|
||||
/// Set to `Some(now)` after any successful upload or poll (200/304).
|
||||
/// `None` before the first success. Drives Online/Offline heuristics.
|
||||
/// `None` before the first success. Drives Online/Offline heuristics
|
||||
/// together with `last_failure`.
|
||||
pub last_success: Option<Instant>,
|
||||
/// Set to `Some(now)` after any failed upload or poll. Compared to
|
||||
/// `last_success` in the UI: if `last_failure` is younger, the
|
||||
/// footer shows Offline regardless of the success's age.
|
||||
pub last_failure: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Default for WorkerSnapshot {
|
||||
@@ -75,6 +80,7 @@ impl Default for WorkerSnapshot {
|
||||
state: WorkerState::Idle,
|
||||
queue_len: 0,
|
||||
last_success: None,
|
||||
last_failure: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,12 +203,17 @@ async fn run_loop(
|
||||
|
||||
let mut backoff = INITIAL_BACKOFF;
|
||||
let mut last_success: Option<Instant> = None;
|
||||
let mut last_failure: Option<Instant> = None;
|
||||
|
||||
let publish = |state: WorkerState, queue_len: usize, last: Option<Instant>| {
|
||||
let publish = |state: WorkerState,
|
||||
queue_len: usize,
|
||||
last_ok: Option<Instant>,
|
||||
last_fail: Option<Instant>| {
|
||||
let snap = WorkerSnapshot {
|
||||
state,
|
||||
queue_len,
|
||||
last_success: last,
|
||||
last_success: last_ok,
|
||||
last_failure: last_fail,
|
||||
};
|
||||
let _ = snapshot_tx.send(snap);
|
||||
};
|
||||
@@ -216,7 +227,7 @@ async fn run_loop(
|
||||
let next = files.into_iter().next();
|
||||
|
||||
if let Some(upload) = next {
|
||||
publish(WorkerState::Uploading, queue_len, last_success);
|
||||
publish(WorkerState::Uploading, queue_len, last_success, last_failure);
|
||||
let _ = events_tx.send(UploadEvent::Started(upload.case_id));
|
||||
match post_upload(&http, &config, &upload).await {
|
||||
UploadOutcome::Succeeded(status) => {
|
||||
@@ -235,17 +246,24 @@ async fn run_loop(
|
||||
}
|
||||
UploadOutcome::Transient(reason) => {
|
||||
warn!(case_id = %upload.case_id, reason = %reason, "upload failed transiently");
|
||||
last_failure = Some(Instant::now());
|
||||
let _ = events_tx.send(UploadEvent::Failed {
|
||||
case_id: upload.case_id,
|
||||
reason,
|
||||
will_retry: true,
|
||||
});
|
||||
// Flip back to Idle before the backoff sleep so the UI
|
||||
// doesn't misreport "Uploading" during a passive wait.
|
||||
// The footer derives "Offline" from `last_failure` being
|
||||
// younger than `last_success`.
|
||||
publish(WorkerState::Idle, queue_len, last_success, last_failure);
|
||||
tokio::time::sleep(backoff).await;
|
||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||
}
|
||||
UploadOutcome::Terminal(reason) => {
|
||||
warn!(case_id = %upload.case_id, reason = %reason, "upload failed terminally — deleting files");
|
||||
delete_pair(&upload.file).await;
|
||||
last_failure = Some(Instant::now());
|
||||
backoff = INITIAL_BACKOFF;
|
||||
let _ = events_tx.send(UploadEvent::Failed {
|
||||
case_id: upload.case_id,
|
||||
@@ -255,7 +273,7 @@ async fn run_loop(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
publish(WorkerState::Polling, 0, last_success);
|
||||
publish(WorkerState::Polling, 0, last_success, last_failure);
|
||||
match poll_once(&http, &config, &store, &cache, &mut etag).await {
|
||||
Ok(PollOutcome::Success) | Ok(PollOutcome::NotModified) => {
|
||||
let now = Instant::now();
|
||||
@@ -264,13 +282,15 @@ async fn run_loop(
|
||||
}
|
||||
Ok(PollOutcome::TransientHttp(code)) => {
|
||||
warn!(code, "poll returned transient HTTP error");
|
||||
last_failure = Some(Instant::now());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = %e, "poll error");
|
||||
last_failure = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
publish(WorkerState::Idle, 0, last_success);
|
||||
publish(WorkerState::Idle, 0, last_success, last_failure);
|
||||
|
||||
// Wait for next tick OR kick. Cancel-safe on both arms.
|
||||
tokio::select! {
|
||||
|
||||
Reference in New Issue
Block a user