Skip to content

Commit 366bc00

Browse files
committed
* Create dabase backup on shutdown
Thanks Canary team This update introduces a refined automatic database backup feature during the server shutdown process. The main improvements include: 1. Automatic Compression: The database backup is now always compressed using gzip, reducing disk space usage. 2. Backup Management: The system organizes backup files into folders named by date and automatically deletes backups older than 7 days. This ensures that the backup storage remains manageable over time without manual intervention. The motivation behind these changes is to create a more efficient and reliable way of managing database backups, ensuring data safety while optimizing storage space usage. The feature can be highly useful for production servers, as it creates backups during shutdown and maintains them efficiently by automatically removing old backups.
1 parent f9ec966 commit 366bc00

8 files changed

Lines changed: 114 additions & 4 deletions

File tree

.github/workflows/build-ubuntu.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,14 @@ jobs:
5252
run: >
5353
sudo apt-get update && sudo apt-get install ccache linux-headers-"$(uname -r)"
5454
55-
- name: Switch to gcc-12 on Ubuntu 22.04
55+
- name: Switch to gcc-13 on Ubuntu 22.04
5656
if: matrix.os == 'ubuntu-22.04'
5757
run: |
58-
sudo apt install gcc-12 g++-12
59-
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
60-
sudo update-alternatives --set gcc /usr/bin/gcc-12
58+
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
59+
sudo apt-get update
60+
sudo apt install gcc-13 g++-13 -y
61+
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
62+
sudo update-alternatives --set gcc /usr/bin/gcc-13
6163
6264
- name: Switch to gcc-14 on Ubuntu 24.04
6365
if: matrix.os == 'ubuntu-24.04'

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,5 +390,8 @@ crystalserver.old
390390
# VCPKG
391391
vcpkg_installed
392392

393+
# DB Backups
394+
database_backup
395+
393396
# CLION
394397
cmake-build-*

config.lua.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ mysqlDatabase = "crystalserver"
403403
mysqlPort = 3306
404404
mysqlSock = ""
405405
passwordType = "sha1"
406+
mysqlDatabaseBackup = false
406407

407408
-- NOTE: memoryConst: This is the memory cost for the Argon2 hash algorithm. It specifies the amount of memory that the algorithm will use when calculating a hash.
408409
--The memory cost is measured in units of KiB (1024 bytes). A higher memory cost makes the algorithm more resistant to brute-force and hash-table attacks, but also consumes more memory.

src/config/config_enums.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ enum ConfigKey_t : uint16_t {
176176
MYSQL_PASS,
177177
MYSQL_SOCK,
178178
MYSQL_USER,
179+
MYSQL_DB_BACKUP,
179180
OLD_PROTOCOL,
180181
ONE_PLAYER_ON_ACCOUNT,
181182
ONLY_INVITED_CAN_MOVE_HOUSE_ITEMS,

src/config/configmanager.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ bool ConfigManager::load() {
5656
loadBoolConfig(L, RESET_SESSIONS_ON_STARTUP, "resetSessionsOnStartup", false);
5757
loadBoolConfig(L, TOGGLE_MAINTAIN_MODE, "toggleMaintainMode", false);
5858
loadBoolConfig(L, TOGGLE_MAP_CUSTOM, "toggleMapCustom", true);
59+
loadBoolConfig(L, MYSQL_DB_BACKUP, "mysqlDatabaseBackup", false);
5960

6061
loadFloatConfig(L, HOUSE_PRICE_RENT_MULTIPLIER, "housePriceRentMultiplier", 1.0);
6162
loadFloatConfig(L, HOUSE_RENT_RATE, "houseRentRate", 1.0);

src/crystalserver.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ void CrystalServer::modulesLoadHelper(bool loaded, std::string moduleName) {
385385
}
386386

387387
void CrystalServer::shutdown() {
388+
g_database().createDatabaseBackup(true);
388389
g_dispatcher().shutdown();
389390
g_metrics().shutdown();
390391
inject<ThreadPool>().shutdown();

src/database/database.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "config/configmanager.hpp"
2121
#include "lib/di/container.hpp"
2222
#include "lib/metrics/metrics.hpp"
23+
#include "utils/tools.hpp"
2324

2425
Database::~Database() {
2526
if (handle != nullptr) {
@@ -68,6 +69,89 @@ bool Database::connect(const std::string* host, const std::string* user, const s
6869
return true;
6970
}
7071

72+
void Database::createDatabaseBackup(bool compress) const {
73+
if (!g_configManager().getBoolean(MYSQL_DB_BACKUP)) {
74+
return;
75+
}
76+
// Get current time for formatting
77+
auto now = std::chrono::system_clock::now();
78+
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
79+
std::string formattedDate = fmt::format("{:%Y-%m-%d}", fmt::localtime(now_c));
80+
std::string formattedTime = fmt::format("{:%H-%M-%S}", fmt::localtime(now_c));
81+
// Create a backup directory based on the current date
82+
std::string backupDir = fmt::format("database_backup/{}/", formattedDate);
83+
std::filesystem::create_directories(backupDir);
84+
std::string backupFileName = fmt::format("{}backup_{}.sql", backupDir, formattedTime);
85+
// Create a temporary configuration file for MySQL credentials
86+
std::string tempConfigFile = "database_backup.cnf";
87+
std::ofstream configFile(tempConfigFile);
88+
if (configFile.is_open()) {
89+
configFile << "[client]\n";
90+
configFile << "user=" << g_configManager().getString(MYSQL_USER) << "\n";
91+
configFile << "password=" << g_configManager().getString(MYSQL_PASS) << "\n";
92+
configFile << "host=" << g_configManager().getString(MYSQL_HOST) << "\n";
93+
configFile << "port=" << g_configManager().getNumber(SQL_PORT) << "\n";
94+
configFile.close();
95+
} else {
96+
g_logger().error("Failed to create temporary MySQL configuration file.");
97+
return;
98+
}
99+
// Execute mysqldump command to create backup file
100+
std::string command = fmt::format(
101+
"mysqldump --defaults-extra-file={} {} > {}",
102+
tempConfigFile, g_configManager().getString(MYSQL_DB), backupFileName
103+
);
104+
int result = std::system(command.c_str());
105+
std::filesystem::remove(tempConfigFile);
106+
if (result != 0) {
107+
g_logger().error("Failed to create database backup using mysqldump.");
108+
return;
109+
}
110+
// Compress the backup file if requested
111+
std::string compressedFileName;
112+
compressedFileName = backupFileName + ".gz";
113+
gzFile gzFile = gzopen(compressedFileName.c_str(), "wb9");
114+
if (!gzFile) {
115+
g_logger().error("Failed to open gzip file for compression.");
116+
return;
117+
}
118+
std::ifstream backupFile(backupFileName, std::ios::binary);
119+
if (!backupFile.is_open()) {
120+
g_logger().error("Failed to open backup file for compression: {}", backupFileName);
121+
gzclose(gzFile);
122+
return;
123+
}
124+
std::string buffer(8192, '\0');
125+
while (backupFile.read(&buffer[0], buffer.size()) || backupFile.gcount() > 0) {
126+
gzwrite(gzFile, buffer.data(), backupFile.gcount());
127+
}
128+
backupFile.close();
129+
gzclose(gzFile);
130+
std::filesystem::remove(backupFileName);
131+
g_logger().info("Database backup successfully compressed to: {}", compressedFileName);
132+
// Delete backups older than 7 days
133+
auto nowTime = std::chrono::system_clock::now();
134+
auto sevenDaysAgo = nowTime - std::chrono::hours(7 * 24); // 7 days in hours
135+
for (const auto &entry : std::filesystem::directory_iterator("database_backup")) {
136+
if (entry.is_directory()) {
137+
try {
138+
for (const auto &file : std::filesystem::directory_iterator(entry)) {
139+
if (file.path().extension() == ".gz") {
140+
auto fileTime = std::filesystem::last_write_time(file);
141+
auto fileTimeSystemClock = std::chrono::clock_cast<std::chrono::system_clock>(fileTime);
142+
if (fileTimeSystemClock < sevenDaysAgo) {
143+
std::filesystem::remove(file);
144+
g_logger().info("Deleted old backup file: {}", file.path().string());
145+
}
146+
}
147+
}
148+
} catch (const std::filesystem::filesystem_error &e) {
149+
g_logger().error("Failed to check or delete files in backup directory: {}. Error: {}", entry.path().string(), e.what());
150+
}
151+
}
152+
}
153+
}
154+
71155
bool Database::beginTransaction() {
72156
if (!executeQuery("BEGIN")) {
73157
return false;

src/database/database.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@ class Database {
4545

4646
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);
4747

48+
/**
49+
* @brief Creates a backup of the database.
50+
*
51+
* This function generates a backup of the database, with options for compression.
52+
* The backup can be triggered periodically or during specific events like server loading.
53+
*
54+
* The backup operation will only execute if the configuration option `MYSQL_DB_BACKUP`
55+
* is set to true in the `config.lua` file. If this configuration is disabled, the function
56+
* will return without performing any action.
57+
*
58+
* @param compress Indicates whether the backup should be compressed.
59+
* - If `compress` is true, the backup is created during an interval-based save, which occurs every 2 hours.
60+
* This helps prevent excessive growth in the number of backup files.
61+
* - If `compress` is false, the backup is created during the global save, which is triggered once a day when the server loads.
62+
*/
63+
void createDatabaseBackup(bool compress) const;
64+
4865
bool retryQuery(std::string_view query, int retries);
4966
bool executeQuery(std::string_view query);
5067

0 commit comments

Comments
 (0)