mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 12:37:51 -06:00
LR-BEHAV-BG-TASKS: a render job that panics fails instead of never answering
A panic inside RenderJob::run or QuickJob::run (a raw decoder or pipeline bug) unwound the job pool's worker without sending a result. The slot stayed pending and its key was never asked for again, so the loupe or the thumbnail rendered forever; and the pool never replaces a dead worker, so after as many such panics as it has workers (2–6) no photo rendered until LightCraft was restarted. Both now catch the panic (guard::catch) and return it as the job's failure, answering the request it came from, so the renderer records it like any failed render: no retry until the catalog changes, and the workers live on. run() is the one place every render goes through (the pool, export, Build Previews, contact sheets, face and AI-mask workers); callers that had their own guard keep working, and the watermark preview's, now redundant, is gone. The web build aborts on panic, so nothing changes there. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
76b5377868
commit
2df78c9e58
+68
-12
@@ -803,7 +803,31 @@ impl RenderJob {
|
||||
self
|
||||
}
|
||||
|
||||
/// Render. A panic inside (a decoder or pipeline bug) is this job's failure: the view waiting
|
||||
/// for it is told, instead of waiting forever on a worker that died (the pool doesn't replace
|
||||
/// it, so a few such panics used to stop every render).
|
||||
pub fn run(self) -> RenderResult {
|
||||
let failed = self.failed(String::new());
|
||||
crate::guard::catch("the render", || self.run_unguarded()).unwrap_or_else(|e| RenderResult { rendered: Err(e), ..failed })
|
||||
}
|
||||
|
||||
/// This job's result when it fails with `e`.
|
||||
fn failed(&self, e: String) -> RenderResult {
|
||||
RenderResult {
|
||||
request_id: self.request_id,
|
||||
source_key: self.source_key,
|
||||
photo: self.photo,
|
||||
level: self.level,
|
||||
key: self.key,
|
||||
rendered: Err(e),
|
||||
gpu_fallback: None,
|
||||
loaded: None,
|
||||
quick: None,
|
||||
display: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_unguarded(self) -> RenderResult {
|
||||
if let Some((cache, key)) = &self.cache
|
||||
&& let Some(img) = cache.get_at(self.cache_generation, *key)
|
||||
{
|
||||
@@ -877,18 +901,7 @@ impl RenderJob {
|
||||
display,
|
||||
}
|
||||
}
|
||||
Err(e) => RenderResult {
|
||||
request_id: self.request_id,
|
||||
source_key: self.source_key,
|
||||
photo: self.photo,
|
||||
level: self.level,
|
||||
key: self.key,
|
||||
rendered: Err(e),
|
||||
gpu_fallback: None,
|
||||
loaded: None,
|
||||
quick: None,
|
||||
display: None,
|
||||
},
|
||||
Err(e) => self.failed(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -976,7 +989,25 @@ pub struct QuickJob {
|
||||
}
|
||||
|
||||
impl QuickJob {
|
||||
/// The stand-in, from the first source that has one. A panic inside is this job's failure
|
||||
/// (see [`RenderJob::run`]).
|
||||
pub fn run(self) -> RenderResult {
|
||||
let (request_id, photo, key) = (self.request_id, self.photo, self.key);
|
||||
crate::guard::catch("the render", || self.run_unguarded()).unwrap_or_else(|e| RenderResult {
|
||||
request_id,
|
||||
source_key: None,
|
||||
photo,
|
||||
level: SourceLevel::Thumb,
|
||||
key,
|
||||
rendered: Err(e),
|
||||
gpu_fallback: None,
|
||||
loaded: None,
|
||||
quick: None,
|
||||
display: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_unguarded(self) -> RenderResult {
|
||||
let (photo, key) = (self.photo, self.key);
|
||||
let request_id = self.request_id;
|
||||
let done = |rendered: Result<Rendered, String>, quick| RenderResult {
|
||||
@@ -1588,6 +1619,31 @@ mod thumbnail_hash_tests {
|
||||
s.accept(&old);
|
||||
assert_eq!(s.media.source_usage().0, 0);
|
||||
}
|
||||
/// A panic while rendering (here in the decoder) used to end the pool's worker thread without
|
||||
/// an answer, so the view that asked showed "Rendering…" forever. It is the job's failure now,
|
||||
/// for renders and for stand-ins, answering the request it came from.
|
||||
#[test]
|
||||
fn a_render_that_panics_fails_instead_of_never_answering() {
|
||||
let mut s = session();
|
||||
s.media.file_loader = Some(Arc::new(|_, _| panic!("decoder bug")));
|
||||
let mut job = s.thumb_job(PhotoId(1), 256).unwrap();
|
||||
job.request_id = 42;
|
||||
let (key, level, source_key) = (job.key, job.level, job.source_key);
|
||||
let r = job.run();
|
||||
assert_eq!((r.request_id, r.photo, r.key, r.level, r.source_key), (42, PhotoId(1), key, level, source_key));
|
||||
assert_eq!(r.rendered.err().as_deref(), Some("the render failed unexpectedly: decoder bug"));
|
||||
let quick = QuickJob {
|
||||
request_id: 7,
|
||||
photo: PhotoId(1),
|
||||
key: 9,
|
||||
cached: Vec::new(),
|
||||
embedded: Some(("x.arw".into(), Arc::new(|_: &str, _: usize| -> Option<Rgba8> { panic!("preview reader bug") }), 256)),
|
||||
small: None,
|
||||
};
|
||||
let r = quick.run();
|
||||
assert_eq!((r.request_id, r.key), (7, 9));
|
||||
assert_eq!(r.rendered.err().as_deref(), Some("the render failed unexpectedly: preview reader bug"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user