Skip to content

feat(core): add database player concurrency lock to prevent web/game race conditions - #908

Open
berrytern wants to merge 8 commits into
zimbadev:mainfrom
berrytern:feat/player-concurrency-lock
Open

feat(core): add database player concurrency lock to prevent web/game race conditions#908
berrytern wants to merge 8 commits into
zimbadev:mainfrom
berrytern:feat/player-concurrency-lock

Conversation

@berrytern

Copy link
Copy Markdown

Description

When external web applications (such as Account Access Controls - AACs, Web Shops, Character Bazaars, or custom administration APIs) perform transactional modifications on character data (e.g. updating stats, modifying skills, delivering depot items, deleting characters, or transferring ownership), a critical race condition occurs if the player attempts to log into the game client at the same time.

Currently, if a player connects while the database row is being mutated, the C++ engine loads the stale/intermediate state into RAM memory. Upon subsequent player save or logout, the RAM memory overwrites the database, causing item duplication, currency rollback, or data corruption.

This PR introduces an atomic, non-blocking Player Concurrency Lock mechanism directly in IOLoginDataLoad::preLoadPlayer:

  • Rejects player login during active web transactions before any memory allocation or item parsing occurs.
  • Configurable via togglePlayerLock and playerLockTimeout (deadman switch / auto-healing).
  • Uses composite index (is_locked, locked_at) for sub-millisecond query execution.

Behaviour

Actual

  • If a web process alters a player's inventory, depot, or balance while the player logs in, the game server loads uncommitted or concurrent data into RAM without coordination.
  • When the player logs out, savePlayer overwrites the database with RAM state, leading to rollbacks and item duplication exploits.

Expected

  • If is_locked = 1 and time(nullptr) - locked_at < timeout, preLoadPlayer immediately aborts the login handshake with zero RAM allocations.
  • Once the web operation commits and unlocks (is_locked = 0), the player logs in normally and receives updated data.
  • If a web worker crashes mid-transaction, the lock automatically expires after playerLockTimeout (default: 60s) without server intervention.

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How Has This Been Tested

Test A: Lock active prevents character login

  1. Set player lock in database:
    UPDATE players SET is_locked = 1, locked_at = UNIX_TIMESTAMP(), lock_reason = 'web_trade' WHERE id = 4;
  2. Attempt login via Tibia client.
  3. Result: Login is rejected immediately. Server logs warning:
    Player Paladin Sample login rejected: character is currently locked for a web/market transaction (reason: web_trade)

Test B: Auto-healing / Expired Lock TTL (>60s) allows login

  1. Set expired player lock:
    UPDATE players SET is_locked = 1, locked_at = UNIX_TIMESTAMP() - 120, lock_reason = 'crashed_web_op' WHERE id = 4;
  2. Attempt login with character.
  3. Result: Login succ

@berrytern
berrytern force-pushed the feat/player-concurrency-lock branch from 36ae6ef to fd68403 Compare August 19, 2026 02:12
@jprzimba

Copy link
Copy Markdown
Collaborator

you missing update db_version in schema.sql, and now have a small conflict in configmanager, fix ir please

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants