v4.0.39 — Kobo cover preview is fast now (and Edge blur stops freezing the app)
If you tried "Show how each cover would look on a Kobo" with Edge blur in v4.0.38, you saw the previews never finish — and login + page navigation stalled while the burst was in flight. Both problems are fixed.
What was wrong
Three issues stacked on top of each other:
- The padding pipeline blocked the gevent loop. The thread pool that bounded Wand work used
threading.BoundedSemaphore(and laterconcurrent.futures.ThreadPoolExecutor), both of which do synchronous waits that don't yield to gevent. Confirmed live withpy-spy: while a worker did the C call, the gevent main thread was stuck inconcurrent.futures._base.wait. That's why login and/static/requests stalled until the burst drained. - External URL fetches ran on the main thread.
_fetch_url_byteswas inside the request handler on the gevent main thread; a slow Amazon / OpenLibrary upstream stalled every other request. - Every settings change re-fetched all 18 candidate URLs. Switching Edge blur → Gradient was as slow as the initial toggle-on.
What's fixed
- Gevent-aware thread pool. Switched to
gevent.threadpool.ThreadPoolwithpool.apply, which yields the calling greenlet through the gevent hub. The whole fetch-then-pad pipeline now runs in worker threads; the gevent main thread keeps serving login,/static/, metadata-search throughout the burst. - 64 MB LRU URL byte cache. Subsequent settings changes (Edge blur → Gradient → Edge mirror, etc.) skip the external SSL handshake and pay only the Wand cost. Fast.
- Tighter picker fetch timeout (
(5, 8)instead of(10, 30)). The picker is interactive — a slow upstream should drop fast and let the user move on. edge_bluralgorithm is 3× faster. Blur at quarter resolution and upscale; the bilinear upscale acts as its own low-pass filter so the visual difference is imperceptible. 0.54 s → 0.24 s on a real cover.- Pool size raised from 4 to 8 (matching client-side concurrency cap). Most workers spend their time blocked on external SSL reads, so 8 lets the burst saturate IO+CPU without starving the gevent loop.
Live verification
After the fix, py-spy shows the gevent main thread parked in gevent/hub.py:run (i.e. handling other requests) while 4–8 workers do parallel Wand + SSL work. The 18-cover burst on book 421 now re-renders in ~1 second (was: never completed in 20+ seconds). Subsequent settings changes hit the URL cache and finish in <1 second.
How to update
docker pull ghcr.io/new-usemame/calibre-web-nextgen:latest
docker pull ghcr.io/new-usemame/calibre-web-nextgen:v4.0.39
Why this didn't surface in tests before
Every unit test imports cover_padding without gevent installed, so the gevent codepath was never exercised — the regression was invisible to the harness. v4.0.39 adds a behavioral check that pins the gevent.threadpool.apply preference and the reentrancy detection.
Tests
33 unit tests pass. Same surface as v4.0.38 with one new pin on the gevent path and one new pin on the reentrant _run_in_pool so a future refactor can't reintroduce the deadlock.