feat(admin): observability — job history, live now-analyzing, durations & metrics (0.84.0) (#6)
This commit was merged in pull request #6.
This commit is contained in:
@@ -216,16 +216,23 @@ impl WorkerContext {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the page breadcrumb once so live events carry the
|
||||
// manga/chapter/number the dashboard keys on. A missing breadcrumb
|
||||
// (page deleted) just suppresses events — the dispatch still runs
|
||||
// and acks normally.
|
||||
let breadcrumb = repo::page::locate(&self.pool, page_id).await.ok().flatten();
|
||||
if let Some((manga_id, chapter_id, page_number)) = breadcrumb {
|
||||
// Resolve the labeled page breadcrumb once so live events carry the
|
||||
// manga/chapter/number AND human labels the dashboard's "now
|
||||
// analyzing" banner names. A missing breadcrumb (page deleted) just
|
||||
// suppresses events — the dispatch still runs and acks normally.
|
||||
let breadcrumb = repo::page::locate_labeled(&self.pool, page_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some((manga_id, manga_title, chapter_id, chapter_number, page_number)) =
|
||||
breadcrumb.clone()
|
||||
{
|
||||
self.events.publish(AnalysisEvent::Started {
|
||||
page_id,
|
||||
manga_id,
|
||||
manga_title,
|
||||
chapter_id,
|
||||
chapter_number,
|
||||
page_number,
|
||||
});
|
||||
}
|
||||
@@ -246,8 +253,10 @@ impl WorkerContext {
|
||||
})
|
||||
};
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let dispatch = AssertUnwindSafe(self.dispatcher.dispatch(page_id)).catch_unwind();
|
||||
let outcome = tokio::time::timeout(self.job_timeout, dispatch).await;
|
||||
let elapsed_ms = started.elapsed().as_millis() as i64;
|
||||
heartbeat.abort();
|
||||
|
||||
// Flatten timeout / panic / dispatch error into one Result + message.
|
||||
@@ -258,11 +267,27 @@ impl WorkerContext {
|
||||
Ok(Ok(Ok(()))) => Ok(()),
|
||||
};
|
||||
|
||||
if let Some((manga_id, chapter_id, page_number)) = breadcrumb {
|
||||
if let Some((manga_id, manga_title, chapter_id, chapter_number, page_number)) =
|
||||
breadcrumb
|
||||
{
|
||||
let event = if result.is_ok() {
|
||||
AnalysisEvent::Completed { page_id, manga_id, chapter_id, page_number }
|
||||
AnalysisEvent::Completed {
|
||||
page_id,
|
||||
manga_id,
|
||||
manga_title,
|
||||
chapter_id,
|
||||
chapter_number,
|
||||
page_number,
|
||||
}
|
||||
} else {
|
||||
AnalysisEvent::Failed { page_id, manga_id, chapter_id, page_number }
|
||||
AnalysisEvent::Failed {
|
||||
page_id,
|
||||
manga_id,
|
||||
manga_title,
|
||||
chapter_id,
|
||||
chapter_number,
|
||||
page_number,
|
||||
}
|
||||
};
|
||||
self.events.publish(event);
|
||||
}
|
||||
@@ -294,6 +319,11 @@ impl WorkerContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stamp how long the vision dispatch took. Best-effort and ordered
|
||||
// after the row is written (persist on success / mark_failed on
|
||||
// terminal failure); a transient failure with no row updates nothing.
|
||||
let _ = repo::page_analysis::record_duration(&self.pool, page_id, elapsed_ms).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,25 +25,33 @@ pub enum AnalysisEvent {
|
||||
manga_id: Option<Uuid>,
|
||||
chapter_id: Option<Uuid>,
|
||||
},
|
||||
/// The worker began analyzing a page.
|
||||
/// The worker began analyzing a page. Carries the manga title and
|
||||
/// chapter number (alongside the ids) so the dashboard's "now
|
||||
/// analyzing" banner can name the target without a lookup.
|
||||
Started {
|
||||
page_id: Uuid,
|
||||
manga_id: Uuid,
|
||||
manga_title: String,
|
||||
chapter_id: Uuid,
|
||||
chapter_number: i32,
|
||||
page_number: i32,
|
||||
},
|
||||
/// The worker finished a page successfully.
|
||||
Completed {
|
||||
page_id: Uuid,
|
||||
manga_id: Uuid,
|
||||
manga_title: String,
|
||||
chapter_id: Uuid,
|
||||
chapter_number: i32,
|
||||
page_number: i32,
|
||||
},
|
||||
/// The worker failed a page (this attempt).
|
||||
Failed {
|
||||
page_id: Uuid,
|
||||
manga_id: Uuid,
|
||||
manga_title: String,
|
||||
chapter_id: Uuid,
|
||||
chapter_number: i32,
|
||||
page_number: i32,
|
||||
},
|
||||
}
|
||||
@@ -76,3 +84,25 @@ impl Default for AnalysisEvents {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn started_serializes_kind_and_labels() {
|
||||
let ev = AnalysisEvent::Started {
|
||||
page_id: Uuid::nil(),
|
||||
manga_id: Uuid::nil(),
|
||||
manga_title: "Berserk".into(),
|
||||
chapter_id: Uuid::nil(),
|
||||
chapter_number: 12,
|
||||
page_number: 7,
|
||||
};
|
||||
let v = serde_json::to_value(&ev).unwrap();
|
||||
assert_eq!(v["kind"], "started");
|
||||
assert_eq!(v["manga_title"], "Berserk");
|
||||
assert_eq!(v["chapter_number"], 12);
|
||||
assert_eq!(v["page_number"], 7);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user