sword_game/server/DatabaseManager.cpp
2026-02-08 18:04:12 +09:00

233 lines
7.7 KiB
C++

#include "DatabaseManager.h"
#include "Logger.h"
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/mysql.hpp>
DatabaseManager &DatabaseManager::GetInstance() {
static DatabaseManager instance;
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<boost::asio::io_context::executor_type>>(
io_context.get_executor());
host_ = host;
port_ = port;
user_ = user;
password_ = password;
db_ = db;
current_total_connections_ = 0;
pool_.clear();
Logger::Log("Database Dynamic Pool 초기화 완료 (Max: ", max_connections_,
"): ", host, ":", port);
return true;
} catch (const std::exception &e) {
Logger::Log("Database Pool 초기화 실패: ", e.what());
return false;
}
}
boost::asio::awaitable<std::unique_ptr<boost::mysql::tcp_ssl_connection>>
DatabaseManager::CreateNewConnection() {
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(
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<boost::asio::steady_timer>(
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<boost::mysql::tcp_ssl_connection> 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();
timer->cancel();
}
});
}
boost::asio::awaitable<DatabaseManager::UserData>
DatabaseManager::LoadUser(std::string nickname) {
try {
auto conn = co_await GetConnection();
boost::mysql::results result;
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);
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);
Logger::Log("새로운 유저 탄생 (DB): ", nickname);
co_return UserData{nickname, 10000, 0, 0};
} else {
auto row = result.rows().at(0);
UserData data;
data.nickname = row.at(0).as_string();
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();
co_return data;
}
} catch (const std::exception &e) {
Logger::Log("LoadUser 에러: ", e.what());
co_return UserData{nickname, 10000, 0, 0};
}
}
boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
uint64_t gold,
uint32_t swordLevel) {
try {
auto conn = co_await GetConnection();
boost::mysql::results result;
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(
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());
}
co_return;
}
boost::asio::awaitable<DatabaseManager::RankingResult>
DatabaseManager::GetRanking(int type, std::string nickname) {
RankingResult res;
res.myRank = -1;
res.myValue = 0;
try {
auto conn = co_await GetConnection();
std::string columnName = (type == 0) ? "max_sword_level" : "gold";
boost::mysql::results topResult;
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);
uint32_t rank = 1;
for (auto row : topResult.rows()) {
RankingResult::Entry entry;
entry.rank = rank++;
entry.nickname = row.at(0).as_string();
if (type == 0) {
entry.value = (uint64_t)row.at(1).as_int64();
} else {
entry.value = row.at(1).as_uint64();
}
res.topEntries.push_back(entry);
}
boost::mysql::results myResult;
std::string myQuery =
"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(
myQuery, 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);
if (type == 0) {
res.myValue = (uint64_t)row.at(0).as_int64();
} else {
res.myValue = row.at(0).as_uint64();
}
res.myRank = (int32_t)row.at(1).as_int64();
}
} catch (const std::exception &e) {
Logger::Log("GetRanking 에러: ", e.what());
}
co_return res;
}