fix: close race conditions in script builder session and slug creation

- script_builder endpoint: pg_advisory_xact_lock on user_id before
  session count check, preventing concurrent creates from both passing
  the MAX_SESSIONS_PER_USER guard
- script_builder_service send_message: pg_advisory_xact_lock on session_id
  before message count check, preventing concurrent sends from both
  passing the MAX_MESSAGES_PER_SESSION guard
- script_builder_service save_to_library: replace check-then-insert slug
  logic with IntegrityError retry loop (3 attempts with fresh UUID suffix);
  add unique constraint on script_templates.slug (migration 070)
- ScriptBuilderPage: add creatingSessionRef to serialize concurrent
  handleSend calls that would otherwise both call createSession() while
  session is still null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-04-01 05:09:42 +00:00
parent 41b0f0a627
commit cb33787c08
5 changed files with 105 additions and 34 deletions

View File

@@ -3,6 +3,7 @@ from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
@@ -67,6 +68,12 @@ async def create_session(
current_user: Annotated[User, Depends(get_current_active_user)],
) -> ScriptBuilderSessionDetail:
"""Start a new Script Builder session."""
# Acquire per-user advisory lock so concurrent create requests are serialized.
# Without this, two simultaneous requests both read count < limit and both
# insert, exceeding MAX_SESSIONS_PER_USER.
user_lock_key = hash(str(current_user.id)) % (2**62)
await db.execute(text("SELECT pg_advisory_xact_lock(:key)"), {"key": user_lock_key})
# Enforce max concurrent sessions
count = await script_builder_service.count_user_sessions(db, current_user.id)
if count >= MAX_SESSIONS_PER_USER: