fix: HEIC vips fallback, DB pool exhaustion, auth session persistence
HEIC thumbnails: - Switch fallback from ffmpeg to vips for HEIC files that pillow-heif rejects. ffmpeg decoded gain map tiles instead of the primary image, producing inverted/negative thumbnails. vips uses libheif's item references correctly and extracts the full-resolution primary image. Database pool exhaustion: - Add idle_in_transaction_session_timeout=60s so Postgres auto-kills leaked connections from disconnected thumbnail requests. - Add pool_timeout=10 so new requests fail fast instead of hanging. - Bump pool from 5+5 to 10+10 for thumbnail concurrency headroom. - get_db rolls back on exception before closing. Auth session persistence: - Narrow 401 interceptor exclusion to only /auth/refresh and /auth/login (was excluding all /auth/* including /auth/me, preventing token refresh on boot). - fetchMe only clears tokens on 401/403, not network errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,8 +26,8 @@ class PerformanceSettings(BaseModel):
|
||||
"""Performance tuning settings"""
|
||||
max_concurrent_thumbnails: int = 10
|
||||
cache_ttl: int = 3600
|
||||
db_pool_size: int = 5
|
||||
db_pool_max_overflow: int = 5
|
||||
db_pool_size: int = 10
|
||||
db_pool_max_overflow: int = 10
|
||||
db_pool_recycle: int = 3600
|
||||
|
||||
class EmbedderSettings(BaseModel):
|
||||
|
||||
@@ -76,6 +76,10 @@ else:
|
||||
max_overflow=settings.performance.db_pool_max_overflow,
|
||||
pool_recycle=settings.performance.db_pool_recycle,
|
||||
pool_pre_ping=True,
|
||||
pool_timeout=10, # fail fast if pool exhausted (default 30)
|
||||
# Kill connections idle in a transaction for >60s. Prevents leaked
|
||||
# sessions from thumbnail requests that disconnect mid-flight.
|
||||
connect_args={"server_settings": {"idle_in_transaction_session_timeout": "60000"}},
|
||||
)
|
||||
|
||||
# Create async session factory
|
||||
@@ -89,10 +93,17 @@ AsyncSessionLocal = async_sessionmaker(
|
||||
Base = declarative_base()
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
"""Dependency to get database session"""
|
||||
"""Dependency to get database session.
|
||||
|
||||
Rolls back any uncommitted transaction before closing so a client
|
||||
disconnect doesn't leave idle-in-transaction connections in the pool.
|
||||
"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
@@ -114,23 +114,27 @@ def process_heic_image(filepath: str) -> Image.Image:
|
||||
img = img.convert('RGB')
|
||||
return img
|
||||
except Exception as e:
|
||||
logger.warning(f"pillow-heif failed for {filepath}: {e} — trying ffmpeg")
|
||||
logger.warning(f"pillow-heif failed for {filepath}: {e} — trying vips")
|
||||
|
||||
# ffmpeg fallback: decode HEIC to PNG in memory.
|
||||
import subprocess
|
||||
from io import BytesIO
|
||||
# vips fallback: handles tiled Apple HEIC files (bursts, HDR gain
|
||||
# maps, depth maps) that pillow-heif/libheif rejects due to too many
|
||||
# auxiliary image references.
|
||||
import subprocess, tempfile
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
result = subprocess.run(
|
||||
['ffmpeg', '-i', filepath, '-frames:v', '1',
|
||||
'-f', 'image2pipe', '-vcodec', 'png', 'pipe:1'],
|
||||
capture_output=True, timeout=30, stdin=subprocess.DEVNULL,
|
||||
['vips', 'heifload', filepath, tmp_path],
|
||||
capture_output=True, timeout=60, stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout:
|
||||
img = Image.open(BytesIO(result.stdout)).convert('RGB')
|
||||
if result.returncode == 0:
|
||||
img = Image.open(tmp_path).convert('RGB')
|
||||
os.unlink(tmp_path)
|
||||
return img
|
||||
logger.error(f"ffmpeg HEIC decode failed for {filepath}: {result.stderr.decode()[-200:]}")
|
||||
logger.error(f"vips HEIC decode failed for {filepath}: {result.stderr.decode()[-200:]}")
|
||||
os.unlink(tmp_path)
|
||||
except Exception as e2:
|
||||
logger.error(f"ffmpeg fallback failed for {filepath}: {e2}")
|
||||
logger.error(f"vips fallback failed for {filepath}: {e2}")
|
||||
raise RuntimeError(f"Cannot decode HEIC: {filepath}")
|
||||
|
||||
def process_video_thumbnail(filepath: str) -> Image.Image:
|
||||
|
||||
@@ -72,9 +72,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
const res = await api.get('/auth/me')
|
||||
setUser(res.data)
|
||||
} catch {
|
||||
clearTokens()
|
||||
setUser(null)
|
||||
} catch (err: any) {
|
||||
// Only clear tokens on auth failure (401/403), not network errors.
|
||||
if (err?.response?.status === 401 || err?.response?.status === 403) {
|
||||
clearTokens()
|
||||
setUser(null)
|
||||
}
|
||||
// Network errors: leave tokens in place, user stays on login screen
|
||||
// but can retry without re-entering credentials.
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -136,7 +141,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
if (
|
||||
error.response?.status === 401 &&
|
||||
!original._retry &&
|
||||
!original.url?.includes('/auth/')
|
||||
!original.url?.includes('/auth/refresh') &&
|
||||
!original.url?.includes('/auth/login')
|
||||
) {
|
||||
original._retry = true
|
||||
const rt = getStoredRefreshToken()
|
||||
|
||||
Reference in New Issue
Block a user