diff --git a/README.md b/README.md index 978033e..0d70007 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ cmake --build build - Session: 개별 클라이언트 비동기 수신/송신 처리 - PacketHandler: 패킷 ID별 로직 디스패칭 - SessionManager: 글로벌 세션 관리 및 브로드캐스팅 -- DatabaseManager: 비동기 데이터베이스 작업 및 락 제어 +- DatabaseManager: 커넥션 풀을 통한 동기/비동기 데이터베이스 작업 관리 - Packet: Protobuf 기반 직렬화 프로토콜 (proto/Protocol.proto) ## Python GUI 클라이언트 diff --git a/include/DatabaseManager.h b/include/DatabaseManager.h index 2d61505..b74d7c2 100644 --- a/include/DatabaseManager.h +++ b/include/DatabaseManager.h @@ -2,10 +2,13 @@ #include #include +#include #include +#include #include #include #include +#include class DatabaseManager { public: @@ -33,24 +36,60 @@ public: uint64_t myValue; }; + class ConnectionProxy { + public: + ConnectionProxy(std::unique_ptr 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 conn_; + DatabaseManager &owner_; + }; + boost::asio::awaitable LoadUser(std::string nickname); boost::asio::awaitable SaveUser(std::string nickname, uint64_t gold, uint32_t swordLevel); boost::asio::awaitable GetRanking(int type, std::string nickname); + boost::asio::awaitable GetConnection(); + private: DatabaseManager() = default; - // 비동기 Mutex 역할을 할 함수들 - boost::asio::awaitable Lock(); - void Unlock(); + void ReturnConnection(std::unique_ptr conn); + boost::asio::awaitable> + CreateNewConnection(); - std::unique_ptr conn_; + std::deque> pool_; + std::queue> waiting_queue_; + + boost::asio::io_context *io_context_ = nullptr; std::unique_ptr> strand_; - // 비동기 락 시스템을 위한 상태 변수 - bool is_locked_ = false; - std::queue> waiting_queue_; + boost::asio::ssl::context ssl_ctx_{boost::asio::ssl::context::tlsv12_client}; + + std::string host_, user_, password_, db_; + uint16_t port_; + + uint32_t max_connections_ = 10; + uint32_t current_total_connections_ = 0; }; diff --git a/server/DatabaseManager.cpp b/server/DatabaseManager.cpp index 174b4ef..526e7aa 100644 --- a/server/DatabaseManager.cpp +++ b/server/DatabaseManager.cpp @@ -9,101 +9,138 @@ DatabaseManager &DatabaseManager::GetInstance() { return instance; } +DatabaseManager::ConnectionProxy::~ConnectionProxy() { + if (conn_) { + owner_.ReturnConnection(std::move(conn_)); + } +} + bool DatabaseManager::Init(boost::asio::io_context &io_context, const std::string &host, uint16_t port, const std::string &user, const std::string &password, const std::string &db) { try { + io_context_ = &io_context; strand_ = std::make_unique< boost::asio::strand>( io_context.get_executor()); - boost::asio::ip::tcp::resolver resolver(io_context.get_executor()); - auto endpoints = resolver.resolve(host, std::to_string(port)); + host_ = host; + port_ = port; + user_ = user; + password_ = password; + db_ = db; - boost::mysql::handshake_params params(user, password, db); - static boost::asio::ssl::context ssl_ctx( - boost::asio::ssl::context::tlsv12_client); + current_total_connections_ = 0; + pool_.clear(); - conn_ = std::make_unique( - io_context.get_executor(), ssl_ctx); - conn_->connect(*endpoints.begin(), params); - - Logger::Log("Database 연결 성공 (AsyncLock 적용): ", host, ":", port); + Logger::Log("Database Dynamic Pool 초기화 완료 (Max: ", max_connections_, + "): ", host, ":", port); return true; } catch (const std::exception &e) { - Logger::Log("Database 연결 실패: ", e.what()); + Logger::Log("Database Pool 초기화 실패: ", e.what()); return false; } } -// 비동기 락 획득 (이미 락이 걸려있으면 대기열에 들어감) -boost::asio::awaitable DatabaseManager::Lock() { - auto executor = co_await boost::asio::this_coro::executor; +boost::asio::awaitable> +DatabaseManager::CreateNewConnection() { + auto conn = std::make_unique( + io_context_->get_executor(), ssl_ctx_); - co_await boost::asio::post( - boost::asio::bind_executor(*strand_, boost::asio::use_awaitable)); + 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); - if (!is_locked_) { - is_locked_ = true; - co_return; - } + boost::mysql::handshake_params params(user_, password_, db_); + co_await conn->async_connect(*endpoints.begin(), params, + boost::asio::use_awaitable); - // 이미 잠겨있으면 타이머를 만들어서 대기열에 추가 (비동기 대기) - auto timer = std::make_shared( - executor, std::chrono::steady_clock::time_point::max()); - waiting_queue_.push(timer); - - try { - co_await timer->async_wait(boost::asio::use_awaitable); - } catch (...) { - // 타이머 취소(Unlock에서 호출함)되면 성공적으로 락을 획득한 것으로 간주 - } - co_return; + co_return conn; } -// 비동기 락 해제 (대기열에 다음 사람이 있으면 깨워줌) -void DatabaseManager::Unlock() { - boost::asio::dispatch(*strand_, [this]() { - if (waiting_queue_.empty()) { - is_locked_ = false; - } else { - auto next = waiting_queue_.front(); +boost::asio::awaitable +DatabaseManager::GetConnection() { + while (true) { + co_await boost::asio::post( + boost::asio::bind_executor(*strand_, boost::asio::use_awaitable)); + + if (!pool_.empty()) { + auto conn = std::move(pool_.front()); + 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( + executor, std::chrono::steady_clock::time_point::max()); + waiting_queue_.push(timer); + + try { + co_await timer->async_wait(boost::asio::use_awaitable); + } catch (...) { + } + } +} + +void DatabaseManager::ReturnConnection( + std::unique_ptr conn) { + if (!conn) { + boost::asio::dispatch(*strand_, [this]() { + if (current_total_connections_ > 0) { + current_total_connections_--; + } + }); + 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(); - next->cancel(); + timer->cancel(); } }); } boost::asio::awaitable DatabaseManager::LoadUser(std::string nickname) { - co_await Lock(); - try { - if (!conn_) { - Unlock(); - co_return UserData{nickname, 10000, 0}; - } + auto conn = co_await GetConnection(); 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 " "username = ?", boost::asio::use_awaitable); - co_await conn_->async_execute(stmt.bind(nickname), result, - boost::asio::use_awaitable); + co_await conn->async_execute(stmt.bind(nickname), result, + boost::asio::use_awaitable); if (result.rows().empty()) { - boost::mysql::statement ins_stmt = - co_await conn_->async_prepare_statement( - "INSERT INTO users (username, gold, sword_level) VALUES (?, " - "10000, 0)", - boost::asio::use_awaitable); - co_await conn_->async_execute(ins_stmt.bind(nickname), result, - boost::asio::use_awaitable); + boost::mysql::statement ins_stmt = co_await conn->async_prepare_statement( + "INSERT INTO users (username, gold, sword_level) VALUES (?, " + "10000, 0)", + boost::asio::use_awaitable); + co_await conn->async_execute(ins_stmt.bind(nickname), result, + boost::asio::use_awaitable); Logger::Log("새로운 유저 탄생 (DB): ", nickname); - Unlock(); - co_return UserData{nickname, 10000, 0}; + co_return UserData{nickname, 10000, 0, 0}; } else { auto row = result.rows().at(0); UserData data; @@ -111,56 +148,42 @@ DatabaseManager::LoadUser(std::string nickname) { data.gold = row.at(1).as_uint64(); data.swordLevel = (uint32_t)row.at(2).as_int64(); data.maxSwordLevel = (uint32_t)row.at(3).as_int64(); - Unlock(); co_return data; } } catch (const std::exception &e) { Logger::Log("LoadUser 에러: ", e.what()); - Unlock(); - co_return UserData{nickname, 10000, 0}; + co_return UserData{nickname, 10000, 0, 0}; } } boost::asio::awaitable DatabaseManager::SaveUser(std::string nickname, uint64_t gold, uint32_t swordLevel) { - co_await Lock(); - try { - if (!conn_) { - Unlock(); - co_return; - } + auto conn = co_await GetConnection(); 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 = " "GREATEST(max_sword_level, ?) WHERE username = ?", boost::asio::use_awaitable); - co_await conn_->async_execute( + co_await conn->async_execute( stmt.bind(gold, (int64_t)swordLevel, (int64_t)swordLevel, nickname), result, boost::asio::use_awaitable); } catch (const std::exception &e) { Logger::Log("SaveUser 에러: ", e.what()); } - - Unlock(); co_return; } boost::asio::awaitable DatabaseManager::GetRanking(int type, std::string nickname) { - co_await Lock(); - RankingResult res; res.myRank = -1; res.myValue = 0; try { - if (!conn_) { - Unlock(); - co_return res; - } + auto conn = co_await GetConnection(); std::string columnName = (type == 0) ? "max_sword_level" : "gold"; @@ -168,15 +191,14 @@ DatabaseManager::GetRanking(int type, std::string nickname) { std::string topQuery = "SELECT username, " + columnName + " FROM users ORDER BY " + columnName + " DESC LIMIT 10"; - co_await conn_->async_execute(topQuery, topResult, - boost::asio::use_awaitable); + co_await conn->async_execute(topQuery, topResult, + boost::asio::use_awaitable); uint32_t rank = 1; for (auto row : topResult.rows()) { RankingResult::Entry entry; entry.rank = rank++; entry.nickname = row.at(0).as_string(); - // DB 타입이 달라 분기처리.. if (type == 0) { entry.value = (uint64_t)row.at(1).as_int64(); } else { @@ -190,10 +212,10 @@ DatabaseManager::GetRanking(int type, std::string nickname) { "SELECT " + columnName + ", (SELECT COUNT(*) + 1 FROM users WHERE " + 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); - co_await conn_->async_execute(stmt.bind(nickname), myResult, - boost::asio::use_awaitable); + co_await conn->async_execute(stmt.bind(nickname), myResult, + boost::asio::use_awaitable); if (!myResult.rows().empty()) { auto row = myResult.rows().at(0); @@ -207,7 +229,5 @@ DatabaseManager::GetRanking(int type, std::string nickname) { } catch (const std::exception &e) { Logger::Log("GetRanking 에러: ", e.what()); } - - Unlock(); co_return res; }