fix: atomic counters, plan limit re-check, and double-submit guard

Backend:
- Tree usage_count: use SQL-level UPDATE (Tree.usage_count + 1) instead
  of Python-level increment to prevent lost updates under concurrency
- Tag usage_count: same SQL-level atomic increment/decrement in both
  create_tree and update_tree (delete_tree already used this pattern)
- Plan tree limit: re-check count after db.flush() to close the TOCTOU
  window where two concurrent creates could both pass the pre-check

Frontend:
- TreeEditorPage: add isSaving early-return guard inside handleSaveDraft
  and handlePublish callbacks so Ctrl+S can't bypass the button disabled
  prop and fire duplicate save requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-09 17:27:09 -04:00
parent c724ad8062
commit 7e3b383a65
3 changed files with 46 additions and 11 deletions

View File

@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel, Field as PydanticField
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy import select, update as sa_update
from app.core.database import get_db
from app.models.tree import Tree
@@ -189,8 +189,10 @@ async def start_session(
session_variables=session_variables,
)
# Increment tree usage count
tree.usage_count += 1
# Atomically increment tree usage count (SQL-level to avoid lost updates)
await db.execute(
sa_update(Tree).where(Tree.id == tree.id).values(usage_count=Tree.usage_count + 1)
)
db.add(new_session)
await db.commit()