perf(api,indexer): optimiser pages, thumbnails, watcher et robustesse fd
- Pages: mode Original (zero-transcoding), ETag/304, cache index CBZ, préfetch next 2 pages, filtre Triangle par défaut - Thumbnails: DCT scaling JPEG via jpeg-decoder (decode 7x plus rapide), img.thumbnail() pour resize, support format Original, fix JPEG RGBA8 - API fallback thumbnail: OutputFormat::Original + DCT scaling au lieu de WebP full-decode, retour (bytes, content_type) dynamique - Watcher: remplacement notify par poll léger sans inotify/fd, skip poll quand job actif, snapshots en mémoire - Jobs: mutex exclusif corrigé (tous statuts actifs, tous types exclusifs) - Robustesse: suppression fs::canonicalize (problèmes fd Docker), list_folders avec erreurs explicites, has_children default true - Backoffice: FormRow items-start pour alignement inputs avec helper text, labels settings clarifiés Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,27 +37,57 @@ pub async fn cleanup_stale_jobs(pool: &PgPool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Job types that modify book/thumbnail data and must not run concurrently.
|
||||
const EXCLUSIVE_JOB_TYPES: &[&str] = &[
|
||||
"rebuild",
|
||||
"full_rebuild",
|
||||
"scan",
|
||||
"thumbnail_rebuild",
|
||||
"thumbnail_regenerate",
|
||||
];
|
||||
|
||||
/// Active statuses (job is still in progress, not just queued).
|
||||
const ACTIVE_STATUSES: &[&str] = &[
|
||||
"running",
|
||||
"extracting_pages",
|
||||
"generating_thumbnails",
|
||||
];
|
||||
|
||||
pub async fn claim_next_job(pool: &PgPool) -> Result<Option<(Uuid, Option<Uuid>)>> {
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
// Check if any exclusive job is currently active
|
||||
let has_active_exclusive: bool = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM index_jobs
|
||||
WHERE status = ANY($1)
|
||||
AND type = ANY($2)
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(ACTIVE_STATUSES)
|
||||
.bind(EXCLUSIVE_JOB_TYPES)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT j.id, j.type, j.library_id
|
||||
FROM index_jobs j
|
||||
WHERE j.status = 'pending'
|
||||
AND (
|
||||
(j.type IN ('rebuild', 'full_rebuild') AND NOT EXISTS (
|
||||
SELECT 1 FROM index_jobs
|
||||
WHERE status = 'running'
|
||||
AND type IN ('rebuild', 'full_rebuild')
|
||||
))
|
||||
-- Exclusive jobs: only if no other exclusive job is active
|
||||
(j.type = ANY($1) AND NOT $2::bool)
|
||||
OR
|
||||
j.type NOT IN ('rebuild', 'full_rebuild')
|
||||
-- Non-exclusive jobs (cbr_to_cbz): can always run
|
||||
j.type != ALL($1)
|
||||
)
|
||||
ORDER BY
|
||||
CASE j.type
|
||||
WHEN 'full_rebuild' THEN 1
|
||||
WHEN 'rebuild' THEN 2
|
||||
WHEN 'scan' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
j.created_at ASC
|
||||
@@ -65,6 +95,8 @@ pub async fn claim_next_job(pool: &PgPool) -> Result<Option<(Uuid, Option<Uuid>)
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(EXCLUSIVE_JOB_TYPES)
|
||||
.bind(has_active_exclusive)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -74,30 +106,8 @@ pub async fn claim_next_job(pool: &PgPool) -> Result<Option<(Uuid, Option<Uuid>)
|
||||
};
|
||||
|
||||
let id: Uuid = row.get("id");
|
||||
let job_type: String = row.get("type");
|
||||
let library_id: Option<Uuid> = row.get("library_id");
|
||||
|
||||
if job_type == "rebuild" || job_type == "full_rebuild" {
|
||||
let has_running_rebuild: bool = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM index_jobs
|
||||
WHERE status = 'running'
|
||||
AND type IN ('rebuild', 'full_rebuild')
|
||||
AND id != $1
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if has_running_rebuild {
|
||||
tx.rollback().await?;
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE index_jobs SET status = 'running', started_at = NOW(), error_opt = NULL WHERE id = $1",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user