fix(ws): per-character lock for inventory_delta to prevent FK race
The previous commit moved inventory_delta handling to fire-and-forget asyncio tasks. That removed the WS-loop blockage but introduced a race: when the same character generated multiple deltas in quick succession (mana burn, ID refresh, loot bursts), the tasks ran concurrently and inventory-service's DELETE-then-INSERT path raced on the items table: asyncpg.exceptions.ForeignKeyViolationError: update or delete on table 'items' violates foreign key constraint 'item_combat_stats_item_id_fkey' The 500 errors caused inventory_delta updates to be dropped silently (likely the source of the 'items in wrong container' bug the user reported earlier — every delta returning 500 means the DB never updates). Fix: per-character asyncio.Lock — deltas for the same character serialize, deltas for different characters still run in parallel. Restores correctness without losing the non-blocking-WS-loop benefit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e512c1c296
commit
aeddaf9925
1 changed files with 24 additions and 0 deletions
24
main.py
24
main.py
|
|
@ -2491,6 +2491,19 @@ _inventory_http_client: httpx.AsyncClient | None = None
|
|||
# Held in a set so they aren't garbage-collected mid-flight.
|
||||
_inventory_delta_tasks: set[asyncio.Task] = set()
|
||||
|
||||
# Per-character locks so deltas for the SAME character serialize
|
||||
# (preventing FK race conditions in inventory-service's DELETE+INSERT path)
|
||||
# while deltas for DIFFERENT characters still run concurrently.
|
||||
_inventory_char_locks: Dict[str, asyncio.Lock] = {}
|
||||
|
||||
|
||||
def _get_inventory_char_lock(char_name: str) -> asyncio.Lock:
|
||||
lock = _inventory_char_locks.get(char_name)
|
||||
if lock is None:
|
||||
lock = asyncio.Lock()
|
||||
_inventory_char_locks[char_name] = lock
|
||||
return lock
|
||||
|
||||
|
||||
async def _handle_inventory_delta(data: dict):
|
||||
"""Forward an inventory_delta to inventory-service and broadcast to browsers.
|
||||
|
|
@ -2498,7 +2511,18 @@ async def _handle_inventory_delta(data: dict):
|
|||
Runs as a background task so a slow inventory-service POST never blocks
|
||||
the plugin WebSocket receive loop. If the receive loop blocks long enough,
|
||||
Starlette stops processing keepalives and the connection drops.
|
||||
|
||||
Per-character serialization via _inventory_char_locks: rapid deltas for
|
||||
the SAME character (e.g. mana burn, ID refresh, loot bursts) processed
|
||||
one at a time so they don't race in inventory-service's DELETE+INSERT
|
||||
path (FK violations on item_combat_stats etc).
|
||||
"""
|
||||
char_name = data.get("character_name", "unknown")
|
||||
async with _get_inventory_char_lock(char_name):
|
||||
await _do_handle_inventory_delta(data)
|
||||
|
||||
|
||||
async def _do_handle_inventory_delta(data: dict):
|
||||
try:
|
||||
action = data.get("action")
|
||||
char_name = data.get("character_name", "unknown")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue