fix(backend): bound thumbnail generation and decode larger images

Thumbnails/previews are generated lazily per request with no concurrency
limit, and the imaging resize already fans out across every core — so
scrolling to a handful of large images spawned that many all-core,
hundreds-of-MB decodes at once and pegged the server. Add a generation
semaphore (THUMB_CONCURRENCY, default = half the CPUs) so only a bounded
number run at a time; queued requests wait and re-check the cache.

Also raise the decode cap from 64 Mpx to a configurable ~300 Mpx default
(THUMB_MAX_PIXELS) so genuinely large photos (e.g. 13000×17000 ≈ 221
Mpx) get a real thumbnail instead of falling back to a placeholder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 01:07:30 +03:00
parent a371045b41
commit 2d2a42d523
5 changed files with 157 additions and 13 deletions
+14 -4
View File
@@ -35,6 +35,14 @@ type Config struct {
ThumbHeight int
PreviewWidth int
PreviewHeight int
// ThumbMaxPixels caps the pixel count of a source image we will decode into
// memory to generate a thumbnail/preview (a decode bombs guard, and a memory
// bound). Larger images fall back to a placeholder.
ThumbMaxPixels int
// ThumbConcurrency bounds how many thumbnails/previews are generated at once,
// so a burst of large images can't saturate every core or exhaust RAM. 0 =
// auto (half the available CPUs).
ThumbConcurrency int
// Import
ImportPath string
@@ -119,10 +127,12 @@ func Load() (*Config, error) {
ThumbsCachePath: requireStr("THUMBS_CACHE_PATH"),
MaxUploadBytes: parseInt64("MAX_UPLOAD_BYTES", 500<<20), // 500 MiB
ThumbWidth: parseInt("THUMB_WIDTH", 160),
ThumbHeight: parseInt("THUMB_HEIGHT", 160),
PreviewWidth: parseInt("PREVIEW_WIDTH", 1920),
PreviewHeight: parseInt("PREVIEW_HEIGHT", 1080),
ThumbWidth: parseInt("THUMB_WIDTH", 160),
ThumbHeight: parseInt("THUMB_HEIGHT", 160),
PreviewWidth: parseInt("PREVIEW_WIDTH", 1920),
PreviewHeight: parseInt("PREVIEW_HEIGHT", 1080),
ThumbMaxPixels: parseInt("THUMB_MAX_PIXELS", 300_000_000), // ~300 Mpx (e.g. 13000×17000)
ThumbConcurrency: parseInt("THUMB_CONCURRENCY", 0), // 0 = auto
ImportPath: requireStr("IMPORT_PATH"),