feat: CMS-1202 send image to queue to process instead of handling it sync - #2
Open
baoc75 wants to merge 6 commits into
Open
feat: CMS-1202 send image to queue to process instead of handling it sync#2baoc75 wants to merge 6 commits into
baoc75 wants to merge 6 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
Resolves CMS-1202 https://linear.app/cmsmax/issue/CMS-1202/queue-image-optimization-instead-of-processing-in-http-requests
Overview
https://www.loom.com/share/69df07ef764a4f2aa5812026064533b5
On a cache miss, image transforms no longer run synchronously on the request (blocking PHP-FPM workers with Imagick decode/encode). Instead, the request gets an immediate 302 redirect to the original (short
max-age=60TTL) while a queued job (ProcessImageTransform, Redis) transforms and caches in the background. The next request for the same URL hits the populated cache. Feature-flagged (IMAGE_TRANSFORM_ASYNC_ENABLED, default on) for instant rollback to the old synchronous behavior.Also closes an abuse gap this uncovered:
width/height/qualitypreviously accepted any value, so a client could enumerate distinct params to flood the cache/queue with unbounded variants, bypassing job dedup. Added config whitelists (sizes,qualities,allowed_formats) that clamp requests to the nearest allowed value before the cache/dedup key is computed, plus an IP+path throttle on the dispatch. The throttle gates only the enqueue — an over-limit miss still gets the redirect, so a cache miss never hands the client a broken image (no 429 on the miss path).Permanent-failure handling is classified: only a genuine decode/encode fault writes the failure sentinel (request path stops re-dispatching); a transient infra fault (S3/network) is left un-sentineled so the next request can re-dispatch and recover.
Requires: a running queue worker (
php artisan queue:work redis) in Laravel Cloud, andCACHE_STORE=redis(the dedup lock + cache flags need a cache store; a fresh app ships nocache_locksmigration).Timeframes / TTLs
The feature juggles several independent clocks — here's what each one is and where to tune it:
IMAGE_TRANSFORM_PENDING_MAX_AGE(async.pending_redirect_max_age)302 → originalafter a miss before re-checking origin. Kept short so the CDN picks up the optimizedHITquickly once the job finishes.IMAGE_TRANSFORM_JOB_UNIQUE_FOR(async.unique_for)ShouldBeUniquewindow — at most one queued job per identical transform while it processes.IMAGE_TRANSFORM_FAILED_LIFETIME(async.failed_lifetime)immutable(2 592 000s)IMAGE_TRANSFORM_HEADER_CACHE_CONTROL(headers.Cache-Control)200s and for the long-cache302 → originalused on the frame-guard and permanent-failure redirects.IMAGE_TRANSFORM_CACHE_LIFETIME(cache.lifetime)IMAGE_TRANSFORM_RATE_LIMIT_MAX_REQUESTS/_DECAY_SECONDSProcessImageTransform::$triesfailed()runs.Two clocks on a permanent failure
A decode failure sets two independent timers that don't match on purpose:
immutable→ a CDN edge won't re-ask origin for 30 days, so it keeps serving the (valid, unoptimized) original at the edge regardless of the 1h sentinel.Net: a broken image degrades gracefully to its original for up to 30 days at the edge; the 1h sentinel mainly governs direct-to-origin retries. A re-uploaded/fixed source recovers at origin after 1h, but cached edges finish their 30 days first.
Testing instructions
composer install, copy.env.example→.env, set S3 credentials,QUEUE_CONNECTION=redis,CACHE_STORE=redis.vendor/bin/pest— 41 tests should pass (AsyncTransformTest,AllowedSizesAndFormatsTest,AnimatedWebpTransformTest).Start a worker:
php artisan queue:work redis.Request a not-yet-cached transform URL → expect
302to the original withCache-Control: max-age=60. For example: http://image-server.test/production/width=1280,quality=80,format=webp/pyg2bhwsdu01ky1odnkcg/sunsetgrovecopingkit_3.jpg http://image-server.test/production/width=600,quality=80,format=webp/pyg2bhwsdu01ky1odnkcg/sunsetgrovecopingkit_3.jpghttp://image-server.test/production/width=800,quality=80,format=webp/pyg2bhwsdu01ky1odnkcg/sunsetgrovecopingkit_3.jpg
Repeat the same request after the worker processes the job → expect
200withX-Cache: HITand the optimized image.IMAGE_TRANSFORM_ASYNC_ENABLED=false→ misses should transform synchronously again (old behavior).