mirror of
https://github.com/rommapp/romm.git
synced 2026-06-27 22:35:57 +00:00
fix(webp): backfill cover conversion on startup when enabled
The frontend rewrites every cover URL to .webp as soon as the heartbeat reports ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP, but existing covers have no .webp sibling until the scheduled cron eventually runs (the inline conversion only covers art fetched after enabling). This produced 404s on all existing covers until the cron fired. Enqueue a one-off backfill run of the conversion task on startup when the feature is enabled, mirroring the recompute-save-hashes pattern. A fixed job_id + Job.exists guard prevents duplicate jobs across restarts, and the task already skips covers that have a .webp sibling so repeated runs are cheap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,7 @@ from utils.context import initialize_context
|
||||
tracer = trace.get_tracer(__name__)
|
||||
|
||||
RECOMPUTE_SAVE_HASHES_JOB_ID = "recompute_save_content_hashes_bootstrap"
|
||||
CONVERT_IMAGES_TO_WEBP_JOB_ID = "convert_images_to_webp_bootstrap"
|
||||
|
||||
|
||||
def _enqueue_recompute_save_hashes_if_needed() -> None:
|
||||
@@ -98,6 +99,40 @@ def _enqueue_recompute_save_hashes_if_needed() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _enqueue_convert_images_to_webp() -> None:
|
||||
"""Backfill .webp covers when WebP conversion is enabled.
|
||||
|
||||
The frontend rewrites cover URLs to .webp as soon as the feature flag is
|
||||
on, but the scheduled task only runs at its next cron time and the inline
|
||||
conversion in the resources handler only fires for covers fetched after
|
||||
enabling. Without a backfill, existing covers have no .webp sibling and
|
||||
every request 404s until the cron eventually runs. Enqueue a one-off run
|
||||
so existing covers are converted on startup. The task skips covers that
|
||||
already have a .webp sibling, so repeated restarts are cheap."""
|
||||
try:
|
||||
if Job.exists(CONVERT_IMAGES_TO_WEBP_JOB_ID, low_prio_queue.connection):
|
||||
log.info(
|
||||
"convert_images_to_webp already queued or running from a "
|
||||
"previous restart; skipping enqueue"
|
||||
)
|
||||
return
|
||||
|
||||
low_prio_queue.enqueue(
|
||||
convert_images_to_webp_task.run,
|
||||
job_id=CONVERT_IMAGES_TO_WEBP_JOB_ID,
|
||||
job_timeout=TASK_TIMEOUT,
|
||||
meta={
|
||||
"task_name": convert_images_to_webp_task.title,
|
||||
"task_type": convert_images_to_webp_task.task_type.value,
|
||||
},
|
||||
)
|
||||
log.info("Enqueued convert_images_to_webp backfill on low-priority worker")
|
||||
except Exception:
|
||||
log.exception(
|
||||
"Failed to enqueue convert_images_to_webp; admins can run it manually"
|
||||
)
|
||||
|
||||
|
||||
@tracer.start_as_current_span("main")
|
||||
async def main() -> None:
|
||||
"""Run startup tasks."""
|
||||
@@ -121,6 +156,7 @@ async def main() -> None:
|
||||
if ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP:
|
||||
log.info("Starting scheduled convert images to webp")
|
||||
convert_images_to_webp_task.init()
|
||||
_enqueue_convert_images_to_webp()
|
||||
if ENABLE_SCHEDULED_RETROACHIEVEMENTS_PROGRESS_SYNC:
|
||||
log.info("Starting scheduled RetroAchievements progress sync")
|
||||
sync_retroachievements_progress_task.init()
|
||||
|
||||
@@ -88,3 +88,47 @@ def test_enqueue_recompute_swallows_enqueue_error(mocker):
|
||||
)
|
||||
|
||||
startup._enqueue_recompute_save_hashes_if_needed()
|
||||
|
||||
|
||||
def test_enqueue_convert_webp_fires_when_not_queued(mocker):
|
||||
"""No in-flight bootstrap job -> enqueue the backfill exactly once."""
|
||||
mocker.patch.object(startup.Job, "exists", return_value=False)
|
||||
enqueue = mocker.patch.object(startup.low_prio_queue, "enqueue")
|
||||
|
||||
startup._enqueue_convert_images_to_webp()
|
||||
|
||||
enqueue.assert_called_once()
|
||||
args, kwargs = enqueue.call_args
|
||||
assert args[0].__self__ is startup.convert_images_to_webp_task
|
||||
assert kwargs["meta"]["task_name"] == startup.convert_images_to_webp_task.title
|
||||
assert kwargs["meta"]["task_type"] == (
|
||||
startup.convert_images_to_webp_task.task_type.value
|
||||
)
|
||||
assert kwargs["job_timeout"] == startup.TASK_TIMEOUT
|
||||
assert kwargs["job_id"] == startup.CONVERT_IMAGES_TO_WEBP_JOB_ID
|
||||
|
||||
|
||||
def test_convert_webp_job_id_is_valid_rq_id():
|
||||
"""An invalid job_id raises in set_id, which the broad except swallows ->
|
||||
backfill silently never enqueues. Assert the id matches RQ's contract."""
|
||||
assert JOB_ID_PATTERN.fullmatch(startup.CONVERT_IMAGES_TO_WEBP_JOB_ID)
|
||||
|
||||
|
||||
def test_enqueue_convert_webp_skips_when_already_queued(mocker):
|
||||
"""An in-flight job from a previous restart -> skip enqueue, don't double up."""
|
||||
mocker.patch.object(startup.Job, "exists", return_value=True)
|
||||
enqueue = mocker.patch.object(startup.low_prio_queue, "enqueue")
|
||||
|
||||
startup._enqueue_convert_images_to_webp()
|
||||
|
||||
enqueue.assert_not_called()
|
||||
|
||||
|
||||
def test_enqueue_convert_webp_swallows_enqueue_error(mocker):
|
||||
"""A failed enqueue must not crash startup."""
|
||||
mocker.patch.object(startup.Job, "exists", return_value=False)
|
||||
mocker.patch.object(
|
||||
startup.low_prio_queue, "enqueue", side_effect=RuntimeError("redis gone")
|
||||
)
|
||||
|
||||
startup._enqueue_convert_images_to_webp()
|
||||
|
||||
Reference in New Issue
Block a user