Skip to content

Commit e5c7800

Browse files
boomballaromange
andauthored
feat(config): Implement CONFIG REWRITE command (#5471)
* feat(config): implement CONFIG REWRITE command --------- Signed-off-by: boomballa <[email protected]> Co-authored-by: Roman Gershman <[email protected]>
1 parent 807b0aa commit e5c7800

File tree

1 file changed

+152
-1
lines changed

1 file changed

+152
-1
lines changed

src/server/server_family.cc

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@
1111
#include <absl/strings/str_replace.h>
1212
#include <absl/strings/strip.h>
1313
#include <croncpp.h> // cron::cronexpr
14+
#include <fcntl.h> // for mkstemp
1415
#include <sys/resource.h>
16+
#include <sys/stat.h> // for fchmod
1517
#include <sys/utsname.h>
16-
#include <unistd.h> // for getpid()
18+
#include <unistd.h> // for getpid(), write(), close(), unlink(), fsync()
1719

1820
#include <algorithm>
1921
#include <chrono>
22+
#include <filesystem>
23+
#include <fstream>
2024
#include <optional>
25+
#include <unordered_map>
26+
#include <unordered_set>
2127

2228
#include "absl/strings/ascii.h"
2329
#include "facade/error.h"
@@ -732,6 +738,142 @@ bool ReadProcStats(io::StatusData* sdata) {
732738
return true;
733739
}
734740

741+
// Rewrite the configuration file with runtime modified settings
742+
GenericError RewriteConfigFile() {
743+
absl::CommandLineFlag* flagfile_flag = absl::FindCommandLineFlag("flagfile");
744+
if (!flagfile_flag || flagfile_flag->CurrentValue().empty()) {
745+
return GenericError("The server is running without a config file");
746+
}
747+
748+
std::string config_file_path = flagfile_flag->CurrentValue();
749+
750+
// Read original config file
751+
std::ifstream file(config_file_path);
752+
if (!file.is_open()) {
753+
return GenericError("Cannot read config file");
754+
}
755+
756+
std::string original_content;
757+
std::string line;
758+
std::unordered_set<std::string> existing_flags;
759+
std::vector<std::string> updated_lines;
760+
bool in_generated_section = false;
761+
bool had_generated_section = false;
762+
763+
// Get only runtime modified flag values (not startup config)
764+
std::unordered_map<std::string, std::string> current_flags;
765+
auto all_flags = absl::GetAllFlags();
766+
for (const auto& [flag_name, flag_ptr] : all_flags) {
767+
// Only include flags that were modified at runtime via CONFIG SET
768+
// We exclude 'flagfile' and other startup-only configs
769+
if (flag_ptr->CurrentValue() != flag_ptr->DefaultValue() && flag_name != "flagfile") {
770+
// Additional check: only include if the config is known to ConfigRegistry
771+
// This ensures we only write configs that can be modified at runtime
772+
auto config_names = config_registry.List(flag_name);
773+
if (!config_names.empty()) {
774+
current_flags[std::string(flag_name)] = flag_ptr->CurrentValue();
775+
}
776+
}
777+
}
778+
779+
// Process original file line by line
780+
while (std::getline(file, line)) {
781+
std::string trimmed = line;
782+
trimmed.erase(0, trimmed.find_first_not_of(" \t"));
783+
784+
// Skip generated section from previous rewrites
785+
if (trimmed == "# Generated by CONFIG REWRITE") {
786+
in_generated_section = true;
787+
had_generated_section = true;
788+
break;
789+
}
790+
791+
if (!in_generated_section) {
792+
// Check if this line is a flag definition
793+
if (!trimmed.empty() && trimmed[0] == '-' && trimmed[1] == '-') {
794+
size_t eq_pos = trimmed.find('=');
795+
if (eq_pos != std::string::npos) {
796+
std::string flag_name = trimmed.substr(2, eq_pos - 2);
797+
if (current_flags.count(flag_name)) {
798+
// Update existing flag with current value
799+
updated_lines.push_back(absl::StrCat("--", flag_name, "=", current_flags[flag_name]));
800+
existing_flags.insert(flag_name);
801+
} else {
802+
// Keep original line if flag is not in current active flags
803+
updated_lines.push_back(line);
804+
}
805+
} else {
806+
// Keep original line as-is
807+
updated_lines.push_back(line);
808+
}
809+
} else {
810+
// Keep comments and other lines as-is
811+
updated_lines.push_back(line);
812+
}
813+
}
814+
}
815+
file.close();
816+
817+
// Collect new flags that weren't in original config
818+
std::vector<std::string> new_flags;
819+
for (const auto& [flag_name, flag_value] : current_flags) {
820+
if (existing_flags.find(flag_name) == existing_flags.end()) {
821+
new_flags.push_back(absl::StrCat("--", flag_name, "=", flag_value));
822+
}
823+
}
824+
825+
// Build final content
826+
std::string final_content;
827+
for (const auto& line : updated_lines) {
828+
final_content += line + "\n";
829+
}
830+
831+
// Add new flags section if there are any
832+
if (!new_flags.empty()) {
833+
if (!final_content.empty() && final_content.back() != '\n') {
834+
final_content += "\n";
835+
}
836+
// Only add extra spacing if this is the first time adding generated section
837+
if (!had_generated_section) {
838+
final_content += "\n# Generated by CONFIG REWRITE\n";
839+
} else {
840+
final_content += "# Generated by CONFIG REWRITE\n";
841+
}
842+
for (const auto& new_flag : new_flags) {
843+
final_content += new_flag + "\n";
844+
}
845+
}
846+
847+
// Atomic write using mkstemp + rename
848+
std::string tmp_template = config_file_path + ".tmpXXXXXX";
849+
int fd = mkstemp(tmp_template.data());
850+
if (fd == -1) {
851+
return GenericError("Failed to create temporary file");
852+
}
853+
854+
size_t off = 0;
855+
while (off < final_content.size()) {
856+
ssize_t n = write(fd, final_content.c_str() + off, final_content.size() - off);
857+
if (n <= 0) {
858+
close(fd);
859+
unlink(tmp_template.data());
860+
return GenericError("Failed to write config file");
861+
}
862+
off += n;
863+
}
864+
865+
fsync(fd);
866+
fchmod(fd, 0644);
867+
close(fd);
868+
869+
if (rename(tmp_template.data(), config_file_path.c_str()) == -1) {
870+
unlink(tmp_template.data());
871+
return GenericError("Failed to rewrite config file");
872+
}
873+
874+
return {};
875+
}
876+
735877
} // namespace
736878

737879
void SlowLogGet(dfly::CmdArgList args, std::string_view sub_cmd, util::ProactorPool* pp,
@@ -2195,6 +2337,8 @@ void ServerFamily::Config(CmdArgList args, const CommandContext& cmd_cntx) {
21952337
" Set the configuration <directive> to <value>.",
21962338
"RESETSTAT",
21972339
" Reset statistics reported by the INFO command.",
2340+
"REWRITE",
2341+
" Rewrite the configuration file with the current configuration.",
21982342
"HELP",
21992343
" Prints this help.",
22002344
};
@@ -2255,6 +2399,13 @@ void ServerFamily::Config(CmdArgList args, const CommandContext& cmd_cntx) {
22552399
return rb->SendBulkStrArr(res, RedisReplyBuilder::MAP);
22562400
}
22572401

2402+
if (sub_cmd == "REWRITE") {
2403+
if (auto ec = RewriteConfigFile(); ec) {
2404+
return builder->SendError(ec.Format());
2405+
}
2406+
return builder->SendOk();
2407+
}
2408+
22582409
if (sub_cmd == "RESETSTAT") {
22592410
ResetStat(cmd_cntx.conn_cntx->ns);
22602411
return builder->SendOk();

0 commit comments

Comments
 (0)