Skip to content

Commit 41cee95

Browse files
committed
refactor: Extract application layer from main.cpp for better separation of concerns
- Create new app/ module with clear responsibilities: - Application: Top-level application lifecycle management - ServerOrchestrator: Server component initialization and lifecycle - SignalManager: Signal handling (SIGINT, SIGTERM, SIGHUP) - CommandLineParser: Command-line argument parsing - ConfigurationManager: Configuration loading and hot reload - Simplify main.cpp to 24 lines (was 656 lines) - ServerLifecycleManager now acts as factory pattern - Creates components and returns InitializedComponents - TcpServer takes ownership via std::move - Clear separation: lifecycle manager creates, server owns - Update all affected modules to use Expected<T, Error>: - cache/invalidation_queue.cpp, query_cache.cpp - index/index.cpp, posting_list.cpp - query/query_parser.cpp, result_sorter.cpp - server/handlers/sync_handler.cpp, tcp_server.cpp - Add comprehensive tests for new components - Update documentation (architecture.md, configuration.md) Benefits: - Clear architectural layers and responsibilities - Improved testability (components can be tested in isolation) - Type-safe error handling throughout - Better code organization and maintainability
1 parent d74c514 commit 41cee95

37 files changed

+7423
-2224
lines changed

docs/en/architecture.md

Lines changed: 1014 additions & 0 deletions
Large diffs are not rendered by default.

docs/en/configuration.md

Lines changed: 729 additions & 683 deletions
Large diffs are not rendered by default.

docs/ja/architecture.md

Lines changed: 1015 additions & 0 deletions
Large diffs are not rendered by default.

docs/ja/configuration.md

Lines changed: 729 additions & 674 deletions
Large diffs are not rendered by default.

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ configure_file(
1111
include_directories(${CMAKE_CURRENT_BINARY_DIR})
1212

1313
# Subdirectories
14+
add_subdirectory(app)
1415
add_subdirectory(config)
1516
add_subdirectory(mysql)
1617
add_subdirectory(index)
@@ -26,6 +27,7 @@ add_subdirectory(client)
2627
add_executable(mygramdb main.cpp)
2728

2829
target_link_libraries(mygramdb PRIVATE
30+
mygramdb_app # Application layer (includes all other modules)
2931
mygramdb_server # Includes config, cache, query, index, storage transitively
3032
)
3133

src/app/CMakeLists.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Application layer library
2+
3+
add_library(mygramdb_app STATIC
4+
application.cpp
5+
command_line_parser.cpp
6+
configuration_manager.cpp
7+
server_orchestrator.cpp
8+
signal_manager.cpp
9+
)
10+
11+
target_include_directories(mygramdb_app PUBLIC
12+
${CMAKE_SOURCE_DIR}/src
13+
)
14+
15+
target_link_libraries(mygramdb_app PUBLIC
16+
mygramdb_config
17+
mygramdb_server
18+
mygramdb_utils
19+
spdlog::spdlog
20+
)
21+
22+
# Link MySQL libraries if enabled
23+
if(USE_MYSQL)
24+
target_link_libraries(mygramdb_app PUBLIC
25+
mygramdb_mysql
26+
)
27+
endif()

src/app/application.cpp

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/**
2+
* @file application.cpp
3+
* @brief Main application class implementation
4+
*/
5+
6+
#include "app/application.h"
7+
8+
#include <spdlog/spdlog.h>
9+
10+
#include <filesystem>
11+
#include <fstream>
12+
#include <iostream>
13+
14+
#include "utils/daemon_utils.h"
15+
#include "version.h"
16+
17+
#ifndef _WIN32
18+
#include <unistd.h> // for getuid(), geteuid()
19+
#endif
20+
21+
namespace mygramdb::app {
22+
23+
namespace {
24+
constexpr int kShutdownCheckIntervalMs = 100; // Shutdown check interval (ms)
25+
} // namespace
26+
27+
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays) - Standard C/C++ main signature
28+
mygram::utils::Expected<std::unique_ptr<Application>, mygram::utils::Error> Application::Create(
29+
int argc, char* argv[]) { // NOLINT(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
30+
// Step 1: Parse command-line arguments
31+
auto args_result = CommandLineParser::Parse(argc, argv);
32+
if (!args_result) {
33+
return mygram::utils::MakeUnexpected(args_result.error());
34+
}
35+
36+
CommandLineArgs args = std::move(*args_result);
37+
38+
// Handle help and version early (before loading config)
39+
if (args.show_help) {
40+
CommandLineParser::PrintHelp(argv[0]); // NOLINT
41+
// Return special "success" application that exits immediately
42+
auto app = std::unique_ptr<Application>(new Application(std::move(args), nullptr));
43+
return app;
44+
}
45+
46+
if (args.show_version) {
47+
CommandLineParser::PrintVersion();
48+
// Return special "success" application that exits immediately
49+
auto app = std::unique_ptr<Application>(new Application(std::move(args), nullptr));
50+
return app;
51+
}
52+
53+
// Step 2: Load configuration
54+
auto config_mgr = ConfigurationManager::Create(args.config_file, args.schema_file);
55+
if (!config_mgr) {
56+
return mygram::utils::MakeUnexpected(config_mgr.error());
57+
}
58+
59+
// Create application instance
60+
auto app = std::unique_ptr<Application>(new Application(std::move(args), std::move(*config_mgr)));
61+
return app;
62+
}
63+
64+
Application::Application(CommandLineArgs args, std::unique_ptr<ConfigurationManager> config_mgr)
65+
: args_(std::move(args)), config_manager_(std::move(config_mgr)) {}
66+
67+
Application::~Application() {
68+
if (started_) {
69+
Stop();
70+
}
71+
}
72+
73+
int Application::Run() {
74+
// Handle special modes (--help, --version, --config-test)
75+
int special_exit_code = HandleSpecialModes();
76+
if (special_exit_code >= 0) {
77+
return special_exit_code; // Early exit
78+
}
79+
80+
// Log startup message
81+
spdlog::info("{} starting...", Version::FullString());
82+
83+
// Check root privilege
84+
auto root_check = CheckRootPrivilege();
85+
if (!root_check) {
86+
spdlog::error("Root privilege check failed: {}", root_check.error().to_string());
87+
return 1;
88+
}
89+
90+
// Apply logging configuration
91+
auto logging_result = config_manager_->ApplyLoggingConfig();
92+
if (!logging_result) {
93+
spdlog::error("Failed to apply logging configuration: {}", logging_result.error().to_string());
94+
return 1;
95+
}
96+
97+
// Daemonize if requested (must be done before opening files/sockets)
98+
auto daemon_result = DaemonizeIfRequested();
99+
if (!daemon_result) {
100+
spdlog::error("Daemonization failed: {}", daemon_result.error().to_string());
101+
return 1;
102+
}
103+
104+
// Verify dump directory permissions
105+
auto dump_check = VerifyDumpDirectory();
106+
if (!dump_check) {
107+
spdlog::error("Dump directory verification failed: {}", dump_check.error().to_string());
108+
return 1;
109+
}
110+
111+
// Initialize components
112+
auto init_result = Initialize();
113+
if (!init_result) {
114+
spdlog::error("Initialization failed: {}", init_result.error().to_string());
115+
return 1;
116+
}
117+
118+
// Start servers
119+
auto start_result = Start();
120+
if (!start_result) {
121+
spdlog::error("Server startup failed: {}", start_result.error().to_string());
122+
return 1;
123+
}
124+
125+
// Run main loop (blocks until shutdown signal)
126+
RunMainLoop();
127+
128+
// Graceful shutdown
129+
Stop();
130+
131+
spdlog::info("MygramDB stopped");
132+
return 0;
133+
}
134+
135+
mygram::utils::Expected<void, mygram::utils::Error> Application::Initialize() {
136+
if (initialized_) {
137+
return mygram::utils::MakeUnexpected(
138+
mygram::utils::MakeError(mygram::utils::ErrorCode::kInternalError, "Application already initialized"));
139+
}
140+
141+
// Setup signal handlers
142+
auto signal_mgr = SignalManager::Create();
143+
if (!signal_mgr) {
144+
return mygram::utils::MakeUnexpected(signal_mgr.error());
145+
}
146+
signal_manager_ = std::move(*signal_mgr);
147+
148+
// Initialize server orchestrator
149+
ServerOrchestrator::Dependencies deps{
150+
.config = config_manager_->GetConfig(),
151+
.signal_manager = *signal_manager_,
152+
.dump_dir = config_manager_->GetConfig().dump.dir,
153+
};
154+
155+
auto orchestrator = ServerOrchestrator::Create(deps);
156+
if (!orchestrator) {
157+
return mygram::utils::MakeUnexpected(orchestrator.error());
158+
}
159+
server_orchestrator_ = std::move(*orchestrator);
160+
161+
// Initialize server components (tables, MySQL, servers)
162+
auto init_result = server_orchestrator_->Initialize();
163+
if (!init_result) {
164+
return mygram::utils::MakeUnexpected(init_result.error());
165+
}
166+
167+
initialized_ = true;
168+
return {};
169+
}
170+
171+
mygram::utils::Expected<void, mygram::utils::Error> Application::Start() {
172+
if (!initialized_) {
173+
return mygram::utils::MakeUnexpected(
174+
mygram::utils::MakeError(mygram::utils::ErrorCode::kInternalError, "Cannot start: not initialized"));
175+
}
176+
177+
if (started_) {
178+
return mygram::utils::MakeUnexpected(
179+
mygram::utils::MakeError(mygram::utils::ErrorCode::kInternalError, "Already started"));
180+
}
181+
182+
// Start servers
183+
auto start_result = server_orchestrator_->Start();
184+
if (!start_result) {
185+
return mygram::utils::MakeUnexpected(start_result.error());
186+
}
187+
188+
started_ = true;
189+
return {};
190+
}
191+
192+
void Application::RunMainLoop() {
193+
spdlog::info("Entering main loop...");
194+
195+
while (!signal_manager_->IsShutdownRequested()) {
196+
std::this_thread::sleep_for(std::chrono::milliseconds(kShutdownCheckIntervalMs));
197+
198+
// Check for config reload
199+
if (signal_manager_->CheckAndResetConfigReload()) {
200+
HandleConfigReload();
201+
}
202+
}
203+
204+
spdlog::info("Shutdown requested, cleaning up...");
205+
}
206+
207+
void Application::Stop() {
208+
if (!started_) {
209+
return; // Nothing to stop
210+
}
211+
212+
// Stop server orchestrator (stops all servers in reverse order)
213+
if (server_orchestrator_) {
214+
server_orchestrator_->Stop();
215+
}
216+
217+
started_ = false;
218+
}
219+
220+
int Application::HandleSpecialModes() {
221+
// Help and version are handled in Create() (print and return immediately)
222+
if (args_.show_help || args_.show_version) {
223+
return 0; // Success exit
224+
}
225+
226+
// Config test mode
227+
if (args_.config_test_mode) {
228+
return HandleConfigTestMode();
229+
}
230+
231+
return -1; // Not a special mode, continue normal execution
232+
}
233+
234+
int Application::HandleConfigTestMode() {
235+
// Print configuration test results
236+
return config_manager_->PrintConfigTest();
237+
}
238+
239+
mygram::utils::Expected<void, mygram::utils::Error> Application::CheckRootPrivilege() { // static
240+
#ifndef _WIN32
241+
if (getuid() == 0 || geteuid() == 0) {
242+
std::cerr << "ERROR: Running MygramDB as root is not allowed for security reasons.\n";
243+
std::cerr << "Please run as a non-privileged user.\n";
244+
std::cerr << "\n";
245+
std::cerr << "Recommended approaches:\n";
246+
std::cerr << " - systemd: Use User= and Group= directives in service file\n";
247+
std::cerr << " - Docker: Use USER directive in Dockerfile (already configured)\n";
248+
std::cerr << " - Manual: Run as a dedicated user (e.g., 'sudo -u mygramdb mygramdb -c config.yaml')\n";
249+
return mygram::utils::MakeUnexpected(
250+
mygram::utils::MakeError(mygram::utils::ErrorCode::kPermissionDenied, "Running as root is not allowed"));
251+
}
252+
#endif
253+
return {};
254+
}
255+
256+
mygram::utils::Expected<void, mygram::utils::Error> Application::VerifyDumpDirectory() {
257+
const std::string& dump_dir = config_manager_->GetConfig().dump.dir;
258+
259+
try {
260+
std::filesystem::path dump_path(dump_dir);
261+
262+
// Create directory if it doesn't exist
263+
if (!std::filesystem::exists(dump_path)) {
264+
spdlog::info("Creating dump directory: {}", dump_dir);
265+
std::filesystem::create_directories(dump_path);
266+
}
267+
268+
// Check if directory is writable by attempting to create a test file
269+
std::filesystem::path test_file = dump_path / ".write_test";
270+
std::ofstream test_stream(test_file);
271+
if (!test_stream.is_open()) {
272+
return mygram::utils::MakeUnexpected(mygram::utils::MakeError(mygram::utils::ErrorCode::kPermissionDenied,
273+
"Dump directory is not writable: " + dump_dir));
274+
}
275+
test_stream.close();
276+
std::filesystem::remove(test_file);
277+
278+
spdlog::info("Dump directory verified: {}", dump_dir);
279+
} catch (const std::exception& e) {
280+
return mygram::utils::MakeUnexpected(mygram::utils::MakeError(
281+
mygram::utils::ErrorCode::kIOError, "Failed to verify dump directory: " + std::string(e.what())));
282+
}
283+
284+
return {};
285+
}
286+
287+
mygram::utils::Expected<void, mygram::utils::Error> Application::DaemonizeIfRequested() const {
288+
if (!args_.daemon_mode) {
289+
return {}; // Not requested, nothing to do
290+
}
291+
292+
spdlog::info("Daemonizing process...");
293+
if (!utils::Daemonize()) {
294+
return mygram::utils::MakeUnexpected(
295+
mygram::utils::MakeError(mygram::utils::ErrorCode::kInternalError, "Failed to daemonize process"));
296+
}
297+
298+
// Note: After daemonization, stdout/stderr are redirected to /dev/null
299+
// All output must go through spdlog to be visible (configure file logging if needed)
300+
return {};
301+
}
302+
303+
void Application::HandleConfigReload() {
304+
spdlog::info("Configuration reload requested (SIGHUP received)");
305+
306+
// Reload configuration
307+
auto reload_result = config_manager_->Reload();
308+
if (!reload_result) {
309+
spdlog::error("Failed to reload configuration: {}", reload_result.error().to_string());
310+
spdlog::warn("Continuing with current configuration");
311+
return;
312+
}
313+
314+
// Apply config changes to server orchestrator
315+
auto apply_result = server_orchestrator_->ReloadConfig(config_manager_->GetConfig());
316+
if (!apply_result) {
317+
spdlog::error("Failed to apply configuration changes: {}", apply_result.error().to_string());
318+
} else {
319+
spdlog::info("Configuration reload completed successfully");
320+
}
321+
}
322+
323+
} // namespace mygramdb::app

0 commit comments

Comments
 (0)