feat: mysql connection pool..

in progress
This commit is contained in:
bumpsoo 2026-02-08 18:04:12 +09:00
parent 31b4763bc3
commit 5011fb6fb4
3 changed files with 151 additions and 92 deletions

View file

@ -41,7 +41,7 @@ cmake --build build
- Session: 개별 클라이언트 비동기 수신/송신 처리 - Session: 개별 클라이언트 비동기 수신/송신 처리
- PacketHandler: 패킷 ID별 로직 디스패칭 - PacketHandler: 패킷 ID별 로직 디스패칭
- SessionManager: 글로벌 세션 관리 및 브로드캐스팅 - SessionManager: 글로벌 세션 관리 및 브로드캐스팅
- DatabaseManager: 비동기 데이터베이스 작업 및 락 제어 - DatabaseManager: 커넥션 풀을 통한 동기/비동기 데이터베이스 작업 관리
- Packet: Protobuf 기반 직렬화 프로토콜 (proto/Protocol.proto) - Packet: Protobuf 기반 직렬화 프로토콜 (proto/Protocol.proto)
## Python GUI 클라이언트 ## Python GUI 클라이언트

View file

@ -2,10 +2,13 @@
#include <boost/asio.hpp> #include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp> #include <boost/asio/awaitable.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/mysql.hpp> #include <boost/mysql.hpp>
#include <deque>
#include <memory> #include <memory>
#include <queue> #include <queue>
#include <string> #include <string>
#include <vector>
class DatabaseManager { class DatabaseManager {
public: public:
@ -33,24 +36,60 @@ public:
uint64_t myValue; uint64_t myValue;
}; };
class ConnectionProxy {
public:
ConnectionProxy(std::unique_ptr<boost::mysql::tcp_ssl_connection> conn,
DatabaseManager &owner)
: conn_(std::move(conn)), owner_(owner) {}
~ConnectionProxy();
// Move only
ConnectionProxy(ConnectionProxy &&other) noexcept
: conn_(std::move(other.conn_)), owner_(other.owner_) {}
ConnectionProxy &operator=(ConnectionProxy &&other) noexcept {
if (this != &other) {
conn_ = std::move(other.conn_);
}
return *this;
}
ConnectionProxy(const ConnectionProxy &) = delete;
ConnectionProxy &operator=(const ConnectionProxy &) = delete;
boost::mysql::tcp_ssl_connection *operator->() { return conn_.get(); }
boost::mysql::tcp_ssl_connection &operator*() { return *conn_; }
private:
std::unique_ptr<boost::mysql::tcp_ssl_connection> conn_;
DatabaseManager &owner_;
};
boost::asio::awaitable<UserData> LoadUser(std::string nickname); boost::asio::awaitable<UserData> LoadUser(std::string nickname);
boost::asio::awaitable<void> SaveUser(std::string nickname, uint64_t gold, boost::asio::awaitable<void> SaveUser(std::string nickname, uint64_t gold,
uint32_t swordLevel); uint32_t swordLevel);
boost::asio::awaitable<RankingResult> GetRanking(int type, boost::asio::awaitable<RankingResult> GetRanking(int type,
std::string nickname); std::string nickname);
boost::asio::awaitable<ConnectionProxy> GetConnection();
private: private:
DatabaseManager() = default; DatabaseManager() = default;
// 비동기 Mutex 역할을 할 함수들 void ReturnConnection(std::unique_ptr<boost::mysql::tcp_ssl_connection> conn);
boost::asio::awaitable<void> Lock(); boost::asio::awaitable<std::unique_ptr<boost::mysql::tcp_ssl_connection>>
void Unlock(); CreateNewConnection();
std::unique_ptr<boost::mysql::tcp_ssl_connection> conn_; std::deque<std::unique_ptr<boost::mysql::tcp_ssl_connection>> pool_;
std::queue<std::shared_ptr<boost::asio::steady_timer>> waiting_queue_;
boost::asio::io_context *io_context_ = nullptr;
std::unique_ptr<boost::asio::strand<boost::asio::io_context::executor_type>> std::unique_ptr<boost::asio::strand<boost::asio::io_context::executor_type>>
strand_; strand_;
// 비동기 락 시스템을 위한 상태 변수 boost::asio::ssl::context ssl_ctx_{boost::asio::ssl::context::tlsv12_client};
bool is_locked_ = false;
std::queue<std::shared_ptr<boost::asio::steady_timer>> waiting_queue_; std::string host_, user_, password_, db_;
uint16_t port_;
uint32_t max_connections_ = 10;
uint32_t current_total_connections_ = 0;
}; };

View file

@ -9,47 +9,84 @@ DatabaseManager &DatabaseManager::GetInstance() {
return instance; return instance;
} }
DatabaseManager::ConnectionProxy::~ConnectionProxy() {
if (conn_) {
owner_.ReturnConnection(std::move(conn_));
}
}
bool DatabaseManager::Init(boost::asio::io_context &io_context, bool DatabaseManager::Init(boost::asio::io_context &io_context,
const std::string &host, uint16_t port, const std::string &host, uint16_t port,
const std::string &user, const std::string &password, const std::string &user, const std::string &password,
const std::string &db) { const std::string &db) {
try { try {
io_context_ = &io_context;
strand_ = std::make_unique< strand_ = std::make_unique<
boost::asio::strand<boost::asio::io_context::executor_type>>( boost::asio::strand<boost::asio::io_context::executor_type>>(
io_context.get_executor()); io_context.get_executor());
boost::asio::ip::tcp::resolver resolver(io_context.get_executor()); host_ = host;
auto endpoints = resolver.resolve(host, std::to_string(port)); port_ = port;
user_ = user;
password_ = password;
db_ = db;
boost::mysql::handshake_params params(user, password, db); current_total_connections_ = 0;
static boost::asio::ssl::context ssl_ctx( pool_.clear();
boost::asio::ssl::context::tlsv12_client);
conn_ = std::make_unique<boost::mysql::tcp_ssl_connection>( Logger::Log("Database Dynamic Pool 초기화 완료 (Max: ", max_connections_,
io_context.get_executor(), ssl_ctx); "): ", host, ":", port);
conn_->connect(*endpoints.begin(), params);
Logger::Log("Database 연결 성공 (AsyncLock 적용): ", host, ":", port);
return true; return true;
} catch (const std::exception &e) { } catch (const std::exception &e) {
Logger::Log("Database 연결 실패: ", e.what()); Logger::Log("Database Pool 초기화 실패: ", e.what());
return false; return false;
} }
} }
// 비동기 락 획득 (이미 락이 걸려있으면 대기열에 들어감) boost::asio::awaitable<std::unique_ptr<boost::mysql::tcp_ssl_connection>>
boost::asio::awaitable<void> DatabaseManager::Lock() { DatabaseManager::CreateNewConnection() {
auto executor = co_await boost::asio::this_coro::executor; auto conn = std::make_unique<boost::mysql::tcp_ssl_connection>(
io_context_->get_executor(), ssl_ctx_);
boost::asio::ip::tcp::resolver resolver(io_context_->get_executor());
auto endpoints = co_await resolver.async_resolve(host_, std::to_string(port_),
boost::asio::use_awaitable);
boost::mysql::handshake_params params(user_, password_, db_);
co_await conn->async_connect(*endpoints.begin(), params,
boost::asio::use_awaitable);
co_return conn;
}
boost::asio::awaitable<DatabaseManager::ConnectionProxy>
DatabaseManager::GetConnection() {
while (true) {
co_await boost::asio::post( co_await boost::asio::post(
boost::asio::bind_executor(*strand_, boost::asio::use_awaitable)); boost::asio::bind_executor(*strand_, boost::asio::use_awaitable));
if (!is_locked_) { if (!pool_.empty()) {
is_locked_ = true; auto conn = std::move(pool_.front());
co_return; pool_.pop_front();
co_return ConnectionProxy(std::move(conn), *this);
} }
// 이미 잠겨있으면 타이머를 만들어서 대기열에 추가 (비동기 대기) if (current_total_connections_ < max_connections_) {
current_total_connections_++;
try {
Logger::Log("새로운 DB 커넥션 생성 중... (현재: ",
current_total_connections_, ")");
auto conn = co_await CreateNewConnection();
Logger::Log("새로운 DB 커넥션 생성 완료");
co_return ConnectionProxy(std::move(conn), *this);
} catch (const std::exception &e) {
Logger::Log("DB 커넥션 생성 실패: ", e.what());
current_total_connections_--;
throw;
}
}
auto executor = co_await boost::asio::this_coro::executor;
auto timer = std::make_shared<boost::asio::steady_timer>( auto timer = std::make_shared<boost::asio::steady_timer>(
executor, std::chrono::steady_clock::time_point::max()); executor, std::chrono::steady_clock::time_point::max());
waiting_queue_.push(timer); waiting_queue_.push(timer);
@ -57,53 +94,53 @@ boost::asio::awaitable<void> DatabaseManager::Lock() {
try { try {
co_await timer->async_wait(boost::asio::use_awaitable); co_await timer->async_wait(boost::asio::use_awaitable);
} catch (...) { } catch (...) {
// 타이머 취소(Unlock에서 호출함)되면 성공적으로 락을 획득한 것으로 간주
} }
co_return; }
} }
// 비동기 락 해제 (대기열에 다음 사람이 있으면 깨워줌) void DatabaseManager::ReturnConnection(
void DatabaseManager::Unlock() { std::unique_ptr<boost::mysql::tcp_ssl_connection> conn) {
if (!conn) {
boost::asio::dispatch(*strand_, [this]() { boost::asio::dispatch(*strand_, [this]() {
if (waiting_queue_.empty()) { if (current_total_connections_ > 0) {
is_locked_ = false; current_total_connections_--;
} else { }
auto next = waiting_queue_.front(); });
return;
}
boost::asio::dispatch(*strand_, [this, conn = std::move(conn)]() mutable {
pool_.push_back(std::move(conn));
if (!waiting_queue_.empty()) {
auto timer = waiting_queue_.front();
waiting_queue_.pop(); waiting_queue_.pop();
next->cancel(); timer->cancel();
} }
}); });
} }
boost::asio::awaitable<DatabaseManager::UserData> boost::asio::awaitable<DatabaseManager::UserData>
DatabaseManager::LoadUser(std::string nickname) { DatabaseManager::LoadUser(std::string nickname) {
co_await Lock();
try { try {
if (!conn_) { auto conn = co_await GetConnection();
Unlock();
co_return UserData{nickname, 10000, 0};
}
boost::mysql::results result; boost::mysql::results result;
boost::mysql::statement stmt = co_await conn_->async_prepare_statement( boost::mysql::statement stmt = co_await conn->async_prepare_statement(
"SELECT username, gold, sword_level, max_sword_level FROM users WHERE " "SELECT username, gold, sword_level, max_sword_level FROM users WHERE "
"username = ?", "username = ?",
boost::asio::use_awaitable); boost::asio::use_awaitable);
co_await conn_->async_execute(stmt.bind(nickname), result, co_await conn->async_execute(stmt.bind(nickname), result,
boost::asio::use_awaitable); boost::asio::use_awaitable);
if (result.rows().empty()) { if (result.rows().empty()) {
boost::mysql::statement ins_stmt = boost::mysql::statement ins_stmt = co_await conn->async_prepare_statement(
co_await conn_->async_prepare_statement(
"INSERT INTO users (username, gold, sword_level) VALUES (?, " "INSERT INTO users (username, gold, sword_level) VALUES (?, "
"10000, 0)", "10000, 0)",
boost::asio::use_awaitable); boost::asio::use_awaitable);
co_await conn_->async_execute(ins_stmt.bind(nickname), result, co_await conn->async_execute(ins_stmt.bind(nickname), result,
boost::asio::use_awaitable); boost::asio::use_awaitable);
Logger::Log("새로운 유저 탄생 (DB): ", nickname); Logger::Log("새로운 유저 탄생 (DB): ", nickname);
Unlock(); co_return UserData{nickname, 10000, 0, 0};
co_return UserData{nickname, 10000, 0};
} else { } else {
auto row = result.rows().at(0); auto row = result.rows().at(0);
UserData data; UserData data;
@ -111,56 +148,42 @@ DatabaseManager::LoadUser(std::string nickname) {
data.gold = row.at(1).as_uint64(); data.gold = row.at(1).as_uint64();
data.swordLevel = (uint32_t)row.at(2).as_int64(); data.swordLevel = (uint32_t)row.at(2).as_int64();
data.maxSwordLevel = (uint32_t)row.at(3).as_int64(); data.maxSwordLevel = (uint32_t)row.at(3).as_int64();
Unlock();
co_return data; co_return data;
} }
} catch (const std::exception &e) { } catch (const std::exception &e) {
Logger::Log("LoadUser 에러: ", e.what()); Logger::Log("LoadUser 에러: ", e.what());
Unlock(); co_return UserData{nickname, 10000, 0, 0};
co_return UserData{nickname, 10000, 0};
} }
} }
boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname, boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
uint64_t gold, uint64_t gold,
uint32_t swordLevel) { uint32_t swordLevel) {
co_await Lock();
try { try {
if (!conn_) { auto conn = co_await GetConnection();
Unlock();
co_return;
}
boost::mysql::results result; boost::mysql::results result;
boost::mysql::statement stmt = co_await conn_->async_prepare_statement( boost::mysql::statement stmt = co_await conn->async_prepare_statement(
"UPDATE users SET gold = ?, sword_level = ?, max_sword_level = " "UPDATE users SET gold = ?, sword_level = ?, max_sword_level = "
"GREATEST(max_sword_level, ?) WHERE username = ?", "GREATEST(max_sword_level, ?) WHERE username = ?",
boost::asio::use_awaitable); boost::asio::use_awaitable);
co_await conn_->async_execute( co_await conn->async_execute(
stmt.bind(gold, (int64_t)swordLevel, (int64_t)swordLevel, nickname), stmt.bind(gold, (int64_t)swordLevel, (int64_t)swordLevel, nickname),
result, boost::asio::use_awaitable); result, boost::asio::use_awaitable);
} catch (const std::exception &e) { } catch (const std::exception &e) {
Logger::Log("SaveUser 에러: ", e.what()); Logger::Log("SaveUser 에러: ", e.what());
} }
Unlock();
co_return; co_return;
} }
boost::asio::awaitable<DatabaseManager::RankingResult> boost::asio::awaitable<DatabaseManager::RankingResult>
DatabaseManager::GetRanking(int type, std::string nickname) { DatabaseManager::GetRanking(int type, std::string nickname) {
co_await Lock();
RankingResult res; RankingResult res;
res.myRank = -1; res.myRank = -1;
res.myValue = 0; res.myValue = 0;
try { try {
if (!conn_) { auto conn = co_await GetConnection();
Unlock();
co_return res;
}
std::string columnName = (type == 0) ? "max_sword_level" : "gold"; std::string columnName = (type == 0) ? "max_sword_level" : "gold";
@ -168,7 +191,7 @@ DatabaseManager::GetRanking(int type, std::string nickname) {
std::string topQuery = "SELECT username, " + columnName + std::string topQuery = "SELECT username, " + columnName +
" FROM users ORDER BY " + columnName + " FROM users ORDER BY " + columnName +
" DESC LIMIT 10"; " DESC LIMIT 10";
co_await conn_->async_execute(topQuery, topResult, co_await conn->async_execute(topQuery, topResult,
boost::asio::use_awaitable); boost::asio::use_awaitable);
uint32_t rank = 1; uint32_t rank = 1;
@ -176,7 +199,6 @@ DatabaseManager::GetRanking(int type, std::string nickname) {
RankingResult::Entry entry; RankingResult::Entry entry;
entry.rank = rank++; entry.rank = rank++;
entry.nickname = row.at(0).as_string(); entry.nickname = row.at(0).as_string();
// DB 타입이 달라 분기처리..
if (type == 0) { if (type == 0) {
entry.value = (uint64_t)row.at(1).as_int64(); entry.value = (uint64_t)row.at(1).as_int64();
} else { } else {
@ -190,9 +212,9 @@ DatabaseManager::GetRanking(int type, std::string nickname) {
"SELECT " + columnName + ", (SELECT COUNT(*) + 1 FROM users WHERE " + "SELECT " + columnName + ", (SELECT COUNT(*) + 1 FROM users WHERE " +
columnName + " > u." + columnName + ") FROM users u WHERE username = ?"; columnName + " > u." + columnName + ") FROM users u WHERE username = ?";
boost::mysql::statement stmt = co_await conn_->async_prepare_statement( boost::mysql::statement stmt = co_await conn->async_prepare_statement(
myQuery, boost::asio::use_awaitable); myQuery, boost::asio::use_awaitable);
co_await conn_->async_execute(stmt.bind(nickname), myResult, co_await conn->async_execute(stmt.bind(nickname), myResult,
boost::asio::use_awaitable); boost::asio::use_awaitable);
if (!myResult.rows().empty()) { if (!myResult.rows().empty()) {
@ -207,7 +229,5 @@ DatabaseManager::GetRanking(int type, std::string nickname) {
} catch (const std::exception &e) { } catch (const std::exception &e) {
Logger::Log("GetRanking 에러: ", e.what()); Logger::Log("GetRanking 에러: ", e.what());
} }
Unlock();
co_return res; co_return res;
} }