Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create database backup on server startup #3069

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -395,5 +395,8 @@ canary.old
# VCPKG
vcpkg_installed

# DB Backups
database_backup

# CLION
cmake-build-*
1 change: 1 addition & 0 deletions config.lua.dist
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ mysqlHost = "127.0.0.1"
mysqlUser = "root"
mysqlPass = "root"
mysqlDatabase = "otservbr-global"
mysqlDatabaseBackup = false
mysqlPort = 3306
mysqlSock = ""
passwordType = "sha1"
Expand Down
2 changes: 2 additions & 0 deletions src/canary_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ void CanaryServer::initializeDatabase() {
));
}

g_database().createDatabaseBackup(false);

DatabaseManager::updateDatabase();

if (g_configManager().getBoolean(OPTIMIZE_DATABASE)
Expand Down
1 change: 1 addition & 0 deletions src/config/config_enums.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ enum ConfigKey_t : uint16_t {
MONTH_KILLS_TO_RED,
MULTIPLIER_ATTACKONFIST,
MYSQL_DB,
MYSQL_DB_BACKUP,
MYSQL_HOST,
MYSQL_PASS,
MYSQL_SOCK,
Expand Down
1 change: 1 addition & 0 deletions src/config/configmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ bool ConfigManager::load() {
loadStringConfig(L, MAP_DOWNLOAD_URL, "mapDownloadUrl", "");
loadStringConfig(L, MAP_NAME, "mapName", "canary");
loadStringConfig(L, MYSQL_DB, "mysqlDatabase", "canary");
loadBoolConfig(L, MYSQL_DB_BACKUP, "mysqlDatabaseBackup", false);
loadStringConfig(L, MYSQL_HOST, "mysqlHost", "127.0.0.1");
loadStringConfig(L, MYSQL_PASS, "mysqlPass", "");
loadStringConfig(L, MYSQL_SOCK, "mysqlSock", "");
Expand Down
100 changes: 100 additions & 0 deletions src/database/database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "config/configmanager.hpp"
#include "lib/di/container.hpp"
#include "lib/metrics/metrics.hpp"
#include "utils/tools.hpp"

Database::~Database() {
if (handle != nullptr) {
Expand Down Expand Up @@ -60,6 +61,105 @@
return true;
}

void Database::createDatabaseBackup(bool compress) const {
if (!g_configManager().getBoolean(MYSQL_DB_BACKUP)) {
return;
}

// Get current time for formatting
auto now = std::chrono::system_clock::now();
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
std::string formattedDate = fmt::format("{:%Y-%m-%d}", fmt::localtime(now_c));
std::string formattedTime = fmt::format("{:%H-%M-%S}", fmt::localtime(now_c));

// Create a backup directory based on the current date
std::string backupDir = fmt::format("database_backup/{}/", formattedDate);
std::filesystem::create_directories(backupDir);
std::string backupFileName = fmt::format("{}backup_{}.sql", backupDir, formattedTime);

// Create a temporary configuration file for MySQL credentials
std::string tempConfigFile = "database_backup.cnf";
std::ofstream configFile(tempConfigFile);
if (configFile.is_open()) {
configFile << "[client]\n";
configFile << "user=" << g_configManager().getString(MYSQL_USER) << "\n";
configFile << "password=" << g_configManager().getString(MYSQL_PASS) << "\n";
configFile << "host=" << g_configManager().getString(MYSQL_HOST) << "\n";
configFile << "port=" << g_configManager().getNumber(SQL_PORT) << "\n";
configFile.close();
} else {
g_logger().error("Failed to create temporary MySQL configuration file.");
return;
}

// Execute mysqldump command to create backup file
std::string command = fmt::format(
"mysqldump --defaults-extra-file={} {} > {}",
tempConfigFile, g_configManager().getString(MYSQL_DB), backupFileName
);

int result = std::system(command.c_str());
std::filesystem::remove(tempConfigFile);

if (result != 0) {
g_logger().error("Failed to create database backup using mysqldump.");
return;
}

// Compress the backup file if requested
std::string compressedFileName;
if (compress) {
compressedFileName = backupFileName + ".gz";
gzFile gzFile = gzopen(compressedFileName.c_str(), "wb9");
if (!gzFile) {
g_logger().error("Failed to open gzip file for compression.");
return;
}

std::ifstream backupFile(backupFileName, std::ios::binary);
if (!backupFile.is_open()) {
g_logger().error("Failed to open backup file for compression: {}", backupFileName);
gzclose(gzFile);
return;
}

std::string buffer(8192, '\0');
while (backupFile.read(&buffer[0], buffer.size()) || backupFile.gcount() > 0) {
gzwrite(gzFile, buffer.data(), backupFile.gcount());
}

backupFile.close();
gzclose(gzFile);
std::filesystem::remove(backupFileName);

g_logger().info("Database backup successfully compressed to: {}", compressedFileName);
} else {
g_logger().info("Database backup successfully created at: {}", backupFileName);
}

// Delete old backups
auto twentyFourHoursAgo = std::chrono::system_clock::now() - std::chrono::hours(24);

Check warning on line 141 in src/database/database.cpp

View workflow job for this annotation

GitHub Actions / ubuntu-22.04-linux-debug

variable ‘twentyFourHoursAgo’ set but not used [-Wunused-but-set-variable]

Check warning on line 141 in src/database/database.cpp

View workflow job for this annotation

GitHub Actions / ubuntu-24.04-linux-debug

variable ‘twentyFourHoursAgo’ set but not used [-Wunused-but-set-variable]
auto sevenDaysAgo = std::chrono::system_clock::now() - std::chrono::hours(24 * 7);
for (const auto &entry : std::filesystem::directory_iterator("database_backup")) {
if (entry.is_directory()) {
try {
auto dirTime = std::filesystem::last_write_time(entry);
if (dirTime.time_since_epoch() < sevenDaysAgo.time_since_epoch()) {
// Instead of deleting the entire directory, delete only specific files
for (const auto &file : std::filesystem::directory_iterator(entry)) {
if (file.path().extension() == ".gz" || file.path().extension() == ".sql") {
std::filesystem::remove(file);
g_logger().info("Deleted old backup file: {}", file.path().string());
}
}
}
} catch (const std::filesystem::filesystem_error &e) {
g_logger().error("Failed to check or delete files in backup directory: {}. Error: {}", entry.path().string(), e.what());
}
}
}
}

bool Database::beginTransaction() {
if (!executeQuery("BEGIN")) {
return false;
Expand Down
17 changes: 17 additions & 0 deletions src/database/database.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ class Database {

bool connect(const std::string* host, const std::string* user, const std::string* password, const std::string* database, uint32_t port, const std::string* sock);

/**
* @brief Creates a backup of the database.
*
* This function generates a backup of the database, with options for compression.
* The backup can be triggered periodically or during specific events like server loading.
*
* The backup operation will only execute if the configuration option `MYSQL_DB_BACKUP`
* is set to true in the `config.lua` file. If this configuration is disabled, the function
* will return without performing any action.
*
* @param compress Indicates whether the backup should be compressed.
* - If `compress` is true, the backup is created during an interval-based save, which occurs every 2 hours.
* This helps prevent excessive growth in the number of backup files.
* - If `compress` is false, the backup is created during the global save, which is triggered once a day when the server loads.
*/
void createDatabaseBackup(bool compress) const;

bool retryQuery(std::string_view query, int retries);
bool executeQuery(std::string_view query);

Expand Down
2 changes: 2 additions & 0 deletions src/game/scheduling/save_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ SaveManager &SaveManager::getInstance() {
}

void SaveManager::saveAll() {
g_database().createDatabaseBackup(true);

Benchmark bm_saveAll;
logger.info("Saving server...");
const auto players = game.getPlayers();
Expand Down
Loading