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"""
|
"""Performance tuning settings"""
|
||||||
max_concurrent_thumbnails: int = 10
|
max_concurrent_thumbnails: int = 10
|
||||||
cache_ttl: int = 3600
|
cache_ttl: int = 3600
|
||||||
db_pool_size: int = 5
|
db_pool_size: int = 10
|
||||||
db_pool_max_overflow: int = 5
|
db_pool_max_overflow: int = 10
|
||||||
db_pool_recycle: int = 3600
|
db_pool_recycle: int = 3600
|
||||||
|
|
||||||
class EmbedderSettings(BaseModel):
|
class EmbedderSettings(BaseModel):
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ else:
|
|||||||
max_overflow=settings.performance.db_pool_max_overflow,
|
max_overflow=settings.performance.db_pool_max_overflow,
|
||||||
pool_recycle=settings.performance.db_pool_recycle,
|
pool_recycle=settings.performance.db_pool_recycle,
|
||||||
pool_pre_ping=True,
|
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
|
# Create async session factory
|
||||||
@@ -89,10 +93,17 @@ AsyncSessionLocal = async_sessionmaker(
|
|||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
async def get_db() -> AsyncSession:
|
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:
|
async with AsyncSessionLocal() as session:
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
|||||||
@@ -114,23 +114,27 @@ def process_heic_image(filepath: str) -> Image.Image:
|
|||||||
img = img.convert('RGB')
|
img = img.convert('RGB')
|
||||||
return img
|
return img
|
||||||
except Exception as e:
|
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.
|
# vips fallback: handles tiled Apple HEIC files (bursts, HDR gain
|
||||||
import subprocess
|
# maps, depth maps) that pillow-heif/libheif rejects due to too many
|
||||||
from io import BytesIO
|
# auxiliary image references.
|
||||||
|
import subprocess, tempfile
|
||||||
try:
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['ffmpeg', '-i', filepath, '-frames:v', '1',
|
['vips', 'heifload', filepath, tmp_path],
|
||||||
'-f', 'image2pipe', '-vcodec', 'png', 'pipe:1'],
|
capture_output=True, timeout=60, stdin=subprocess.DEVNULL,
|
||||||
capture_output=True, timeout=30, stdin=subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
if result.returncode == 0 and result.stdout:
|
if result.returncode == 0:
|
||||||
img = Image.open(BytesIO(result.stdout)).convert('RGB')
|
img = Image.open(tmp_path).convert('RGB')
|
||||||
|
os.unlink(tmp_path)
|
||||||
return img
|
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:
|
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}")
|
raise RuntimeError(f"Cannot decode HEIC: {filepath}")
|
||||||
|
|
||||||
def process_video_thumbnail(filepath: str) -> Image.Image:
|
def process_video_thumbnail(filepath: str) -> Image.Image:
|
||||||
|
|||||||
@@ -72,9 +72,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
const res = await api.get('/auth/me')
|
const res = await api.get('/auth/me')
|
||||||
setUser(res.data)
|
setUser(res.data)
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
clearTokens()
|
// Only clear tokens on auth failure (401/403), not network errors.
|
||||||
setUser(null)
|
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 (
|
if (
|
||||||
error.response?.status === 401 &&
|
error.response?.status === 401 &&
|
||||||
!original._retry &&
|
!original._retry &&
|
||||||
!original.url?.includes('/auth/')
|
!original.url?.includes('/auth/refresh') &&
|
||||||
|
!original.url?.includes('/auth/login')
|
||||||
) {
|
) {
|
||||||
original._retry = true
|
original._retry = true
|
||||||
const rt = getStoredRefreshToken()
|
const rt = getStoredRefreshToken()
|
||||||
|
|||||||
Reference in New Issue
Block a user