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 shutdown #3069

Open
wants to merge 16 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
10 changes: 6 additions & 4 deletions .github/workflows/build-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ jobs:
run: >
sudo apt-get update && sudo apt-get install ccache linux-headers-"$(uname -r)"

- name: Switch to gcc-12 on Ubuntu 22.04
- name: Switch to gcc-13 on Ubuntu 22.04
if: matrix.os == 'ubuntu-22.04'
run: |
sudo apt install gcc-12 g++-12
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 100 --slave /usr/bin/g++ g++ /usr/bin/g++-12 --slave /usr/bin/gcov gcov /usr/bin/gcov-12
sudo update-alternatives --set gcc /usr/bin/gcc-12
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt-get update
sudo apt install gcc-13 g++-13 -y
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ /usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-12
sudo update-alternatives --set gcc /usr/bin/gcc-13

- name: Switch to gcc-14 on Ubuntu 24.04
if: matrix.os == 'ubuntu-24.04'
Expand Down
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
1 change: 1 addition & 0 deletions src/canary_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ void CanaryServer::modulesLoadHelper(bool loaded, std::string moduleName) {
}

void CanaryServer::shutdown() {
g_database().createDatabaseBackup(true);
g_dispatcher().shutdown();
g_metrics().shutdown();
inject<ThreadPool>().shutdown();
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
97 changes: 97 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,102 @@ bool Database::connect(const std::string* host, const std::string* user, const s
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;
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);

// Delete backups older than 7 days
auto nowTime = std::chrono::system_clock::now();
auto sevenDaysAgo = nowTime - std::chrono::hours(7 * 24); // 7 days in hours
for (const auto &entry : std::filesystem::directory_iterator("database_backup")) {
if (entry.is_directory()) {
try {
for (const auto &file : std::filesystem::directory_iterator(entry)) {
if (file.path().extension() == ".gz") {
auto fileTime = std::filesystem::last_write_time(file);
auto fileTimeSystemClock = std::chrono::clock_cast<std::chrono::system_clock>(fileTime);

if (fileTimeSystemClock < sevenDaysAgo) {
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
Loading