Skip to content

fix: Database Connection Pool - #803

Draft
jprzimba wants to merge 7 commits into
mainfrom
database
Draft

fix: Database Connection Pool#803
jprzimba wants to merge 7 commits into
mainfrom
database

Conversation

@jprzimba

@jprzimba jprzimba commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Database Connection Pool

Problem

The Database class used a single MySQL connection (MYSQL* handle) protected by a std::recursive_mutex. Every single query — whether from the main game loop, a thread pool task, or an asynchronous DatabaseTasks call — serialized through this mutex.

Under high concurrency (e.g., many players browsing, creating, or accepting market offers simultaneously), queries would pile up behind the single connection lock. The server became unresponsive and eventually crashed as requests accumulated faster than they could be processed.

Solution

Replaced the single-connection architecture with a connection pool of N reusable MySQL connections (default: 4, configurable up to 32).

Key Changes

src/database/database.hpp

  • Added DBConnection struct wrapping a MYSQL* handle and a std::mutex
  • Replaced MYSQL* handle + std::recursive_mutex databaseLock with std::vector<std::unique_ptr<DBConnection>> connections
  • Added getConnection() / putConnection() for round-robin pool management
  • Added thread-local tls_pinnedConnection for safe transaction support
  • Changed escapeString(), escapeBlob(), and getLastInsertId() from const to non-const (they now acquire a connection from the pool)

src/database/database.cpp

  • connect() now initializes N connections instead of one
  • executeQuery() / storeQuery() acquire a connection from the pool, execute the query, and return it
  • escapeString() / escapeBlob() acquire any available connection for the escape operation
  • getLastInsertId() queries the last insert ID from the connection used
  • beginTransaction() acquires a connection from the pool and pins it to the calling thread via thread_local
  • commit() / rollback() use the pinned connection, then unpin and release it back to the pool
  • Added safety checks for empty pool and null handles

Configuration

  • New config key MYSQL_POOL_SIZE in config_enums.hpp
  • New config mysqlPoolSize (default: 4) in config.lua.dist
  • Loaded in configmanager.cpp with bounds clamping (min 1, max 32)

How It Works

Normal Queries

Thread A → getConnection() → locks conn[0] → executes query → unlocks conn[0]
Thread B → getConnection() → locks conn[1] → executes query → unlocks conn[1]

Threads A and B now run their queries concurrently on different connections.

Transactions

beginTransaction() → getConnection() → locks conn[0] → executes BEGIN → pins conn[0] to thread
executeQuery()     → sees pinned conn[0] → reuses it (no lock attempt)
commit()           → unpins conn[0] → unlocks → returns to pool

The pinned connection is stored in thread_local storage, so nested queries within the same transaction automatically use the correct connection without deadlocking.

Connection Selection

Round-robin via std::atomic<size_t> nextIndex ensures even distribution across all connections. If a connection is busy, the calling thread blocks on that connection's mutex — identical to the old behavior but with N parallel slots instead of 1.

Benefits

Aspect Before After
Max concurrent queries 1 N (configurable, default 4)
Connection contention Global mutex Per-connection mutex
Market throughput Serialized, crash-prone Parallel, scalable
Transaction safety Recursive mutex Thread-local pinning
Thread safety (escape) None (const, no lock) Pool-acquired, properly locked

Configuration

In config.lua:

mysqlPoolSize = 4  -- Number of connections in the pool (1-32)

Recommended values:

  • 4 (default) — small to medium servers
  • 8–16 — high-traffic servers with heavy market usage
  • 1 — equivalent to the old single-connection behavior

jprzimba and others added 6 commits June 26, 2026 11:19
Fix a race condition where consecutive executeQuery() and getLastInsertId() calls could return incorrect IDs when using a connection pool. Introduces insertAndGetId() that atomically executes INSERT and retrieves the ID on the same pinned connection. Replaces unsafe patterns in guild creation, player statements, and market offers. Also adds market query limits to prevent client debug issues.
Enforce market offer creation limits to prevent abuse:
- Maximum offers per player per side (buy/sell): 1000
- Maximum offers per item per side: 700

Add helper functions getPlayerOfferCountPerSide() and getItemOfferCountPerSide() to query current offer counts.
@jprzimba jprzimba changed the title fix: Database Connection Pool fix: Database Connection Pool + Market Bug Jun 29, 2026
@jprzimba jprzimba changed the title fix: Database Connection Pool + Market Bug fix: Database Connection Pool Jun 29, 2026
@jprzimba
jprzimba marked this pull request as draft July 10, 2026 12:51
@iloveShaders

Copy link
Copy Markdown
Contributor

works fine didn't notice any issues with this and player gets a kick when trying abuse the market now, only issue were the amount of existing market offers so I had to raise from default 4 to 16.

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