- Lock per-campagna in dm_cycle: serializza le richieste per la stessa campagna, evitando che due DM cycle leggano stato stale e si sovrascrivano - begin_write()/end_write() in db.py per lock scritture - _conn_lock finalmente usato (era definito ma mai acquisito) - 72 test, tutti passano
This commit is contained in:
1 parent
08edeed8bb
commit
950b7c4672
2 files changed
+56
No files matched your search
@@ -43,6 +43,39 @@ def get_conn() -> sqlite3.Connection:
|
||||
return conn
|
||||
|
||||
|
||||
def begin_write():
|
||||
"""Acquisisce lock di scrittura e avvia transazione IMMEDIATE.
|
||||
|
||||
SQLite permette un solo writer per volta, anche in WAL mode.
|
||||
Questo lock serializza le scritture tra thread diversi,
|
||||
evitando race condition quando due richieste modificano
|
||||
lo stesso stato contemporaneamente.
|
||||
"""
|
||||
_conn_lock.acquire()
|
||||
conn = get_conn()
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
|
||||
|
||||
def end_write():
|
||||
"""Commuta la transazione e rilascia il lock di scrittura."""
|
||||
conn = get_conn()
|
||||
conn.commit()
|
||||
_conn_lock.release()
|
||||
|
||||
|
||||
def _write(sql: str, params: tuple = ()) -> sqlite3.Cursor:
|
||||
"""Esegue una scrittura SQL con lock."""
|
||||
begin_write()
|
||||
try:
|
||||
cur = get_conn().execute(sql, params)
|
||||
end_write()
|
||||
return cur
|
||||
except Exception:
|
||||
get_conn().rollback()
|
||||
_conn_lock.release()
|
||||
raise
|
||||
|
||||
|
||||
def init_schema(conn: sqlite3.Connection) -> None:
|
||||
conn.executescript("""
|
||||
-- ============================================================
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
from src.engine import db, tools
|
||||
@@ -12,6 +13,19 @@ from src.engine.prompts import SYSTEM_PROMPT, build_prompt
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Lock per-campagna: evita race condition quando due richieste
|
||||
# arrivano simultaneamente per la stessa campagna.
|
||||
# La chiave è campaign_id, il valore è un threading.Lock.
|
||||
_campaign_locks: dict[int, threading.Lock] = {}
|
||||
_campaign_locks_lock = threading.Lock()
|
||||
|
||||
|
||||
def _get_campaign_lock(campaign_id: int) -> threading.Lock:
|
||||
with _campaign_locks_lock:
|
||||
if campaign_id not in _campaign_locks:
|
||||
_campaign_locks[campaign_id] = threading.Lock()
|
||||
return _campaign_locks[campaign_id]
|
||||
|
||||
|
||||
def _llm_decision(ctx: GameContext, player_action: str, current_area_id: str = "") -> tuple[str, dict, str]:
|
||||
prompt = build_prompt(ctx, player_action, current_area_id=current_area_id)
|
||||
@@ -51,6 +65,15 @@ def _llm_decision(ctx: GameContext, player_action: str, current_area_id: str = "
|
||||
|
||||
|
||||
def dm_cycle(campaign_id: int, player_action: str) -> dict:
|
||||
lock = _get_campaign_lock(campaign_id)
|
||||
lock.acquire()
|
||||
try:
|
||||
return _dm_cycle_impl(campaign_id, player_action)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
|
||||
def _dm_cycle_impl(campaign_id: int, player_action: str) -> dict:
|
||||
t0 = time.perf_counter()
|
||||
ctx = GameContext.load(campaign_id)
|
||||
|
||||
|
||||
Reference in new issue
Block a user