Parcourir la source

feat(cli): --read-only/--force-readwrite/--recovery-mode + lock/unlock/status

Server (smartbotic-database):
- --read-only: boot in read-only (rejects writes until unlocked)
- --force-readwrite: bypass auto-readonly from non-trivial recovery
- --recovery-mode=X: override recovery mode for this boot
- Exit code 10: recovery refused (banner printed by DatabaseService)
- Exit code 11: invalid --recovery-mode value

CLI (smartbotic-db-cli):
- lock    -> SetReadOnly(true)
- unlock  -> SetReadOnly(false)
- status  -> GetReadOnlyStatus + print full recovery outcome
fszontagh il y a 3 mois
Parent
commit
a7f1bf1fe1
2 fichiers modifiés avec 102 ajouts et 1 suppressions
  1. 42 0
      cli/main.cpp
  2. 60 1
      service/src/main.cpp

+ 42 - 0
cli/main.cpp

@@ -46,6 +46,9 @@ void printUsage() {
               << "  " << C_CYAN << "upsert" << C_RESET << " <collection> <id> '<json>'    Insert or update\n"
               << "  " << C_CYAN << "remove" << C_RESET << " <collection> <id>             Delete a document\n"
               << "  " << C_CYAN << "count" << C_RESET << " <collection>                   Count documents\n"
+              << "  " << C_CYAN << "lock" << C_RESET << "                                 Lock database (read-only)\n"
+              << "  " << C_CYAN << "unlock" << C_RESET << "                               Unlock database (accept writes)\n"
+              << "  " << C_CYAN << "status" << C_RESET << "                               Show read-only + recovery status\n"
               << "  " << C_CYAN << "help" << C_RESET << "                                 Show this help\n\n"
               << C_BOLD << "Options:" << C_RESET << "\n"
               << "  --address HOST:PORT    Database address (default: localhost:9004)\n";
@@ -145,6 +148,45 @@ bool execCommand(smartbotic::database::Client& client,
             return true;
         }
 
+        if (cmd == "lock") {
+            if (client.setReadOnly(true)) {
+                std::cout << C_GREEN << "ok" << C_RESET << " Database locked (read-only)\n";
+                return true;
+            }
+            printError("failed to lock database");
+            return false;
+        }
+
+        if (cmd == "unlock") {
+            if (client.setReadOnly(false)) {
+                std::cout << C_GREEN << "ok" << C_RESET << " Database unlocked (writes accepted)\n";
+                return true;
+            }
+            printError("failed to unlock database");
+            return false;
+        }
+
+        if (cmd == "status") {
+            auto s = client.getReadOnlyStatus();
+            std::cout << "Read-only:          " << (s.readOnly ? (std::string(C_RED) + "YES" + C_RESET) : "no") << "\n";
+            if (s.readOnly) {
+                std::cout << "Reason:             " << s.reason << "\n";
+            }
+            std::cout << "Recovery outcome:   " << s.recoveryOutcome << "\n";
+            if (!s.expectedSnapshot.empty()) {
+                std::cout << "Expected snapshot:  " << s.expectedSnapshot << "\n";
+            }
+            if (!s.snapshotUsed.empty()) {
+                std::cout << "Snapshot used:      " << s.snapshotUsed << "\n";
+            }
+            if (!s.failureReason.empty()) {
+                std::cout << "Failure reason:     " << s.failureReason << "\n";
+            }
+            std::cout << "WAL replayed:       " << s.walEntriesReplayed << " entries\n";
+            std::cout << "Snapshots tried:    " << s.snapshotsAttempted << "\n";
+            return true;
+        }
+
         if (cmd == "help" || cmd == "?") {
             printUsage();
             return true;

+ 60 - 1
service/src/main.cpp

@@ -1,4 +1,5 @@
 #include "database_service.hpp"
+#include "persistence/persistence_manager.hpp"
 
 #include <spdlog/spdlog.h>
 #include <spdlog/sinks/stdout_color_sinks.h>
@@ -7,6 +8,8 @@
 #include <cstdlib>
 #include <filesystem>
 #include <iostream>
+#include <optional>
+#include <string>
 
 #ifdef HAVE_SYSTEMD
 #include <systemd/sd-daemon.h>
@@ -30,6 +33,11 @@ void printUsage(const char* programName) {
               << "  --config, -c PATH    Configuration file path\n"
               << "  --help, -h           Show this help message\n"
               << "  --version, -v        Show version information\n"
+              << "  --read-only                Start in read-only mode (reject all writes)\n"
+              << "  --force-readwrite          Accept non-trivial recovery state; start writable\n"
+              << "  --recovery-mode=MODE       Override recovery mode for this boot\n"
+              << "                             Values: normal, snapshot_fallback, wal_only,\n"
+              << "                                     best_effort, force_empty\n"
               << "\n"
               << "Environment variables:\n"
               << "  DATABASE_NODE_ID     Override node ID from config\n"
@@ -71,6 +79,33 @@ void initLogging(const std::string& serviceName) {
     }
 }
 
+struct CliOverrides {
+    bool readOnly = false;
+    bool forceReadwrite = false;
+    std::optional<smartbotic::database::RecoveryMode> recoveryMode;
+};
+
+CliOverrides parseCliOverrides(int argc, char* argv[]) {
+    CliOverrides ov;
+    for (int i = 1; i < argc; i++) {
+        std::string arg = argv[i];
+        if (arg == "--read-only") {
+            ov.readOnly = true;
+        } else if (arg == "--force-readwrite") {
+            ov.forceReadwrite = true;
+        } else if (arg.rfind("--recovery-mode=", 0) == 0) {
+            std::string mode = arg.substr(std::string("--recovery-mode=").size());
+            try {
+                ov.recoveryMode = smartbotic::database::recoveryModeFromString(mode);
+            } catch (const std::exception& e) {
+                std::cerr << "ERROR: " << e.what() << std::endl;
+                std::exit(11);
+            }
+        }
+    }
+    return ov;
+}
+
 std::filesystem::path findConfigFile(int argc, char* argv[]) {
     // Check command line arguments
     for (int i = 1; i < argc; ++i) {
@@ -149,20 +184,44 @@ int main(int argc, char* argv[]) {
             config.nodeId = nodeId;
         }
 
+        // Parse CLI overrides (--read-only, --force-readwrite, --recovery-mode=...)
+        auto cli = parseCliOverrides(argc, argv);
+
+        // Apply --recovery-mode to persistence config before service construction
+        if (cli.recoveryMode) {
+            config.persistenceConfig.recoveryMode = *cli.recoveryMode;
+        }
+
         // Create service
         smartbotic::database::DatabaseService service(config);
         g_service = &service;
 
+        // Apply --force-readwrite BEFORE initialize() so auto-readonly check respects it
+        if (cli.forceReadwrite) {
+            service.setForceReadwrite(true);
+        }
+
         // Set up signal handlers
         std::signal(SIGINT, signalHandler);
         std::signal(SIGTERM, signalHandler);
 
-        // Initialize service
+        // Initialize service. Recovery-refused is reported via the
+        // recovery_outcome_ state (banner printed by DatabaseService) and
+        // maps to exit code 10.
         if (!service.initialize()) {
+            if (service.recoveryOutcome().isFailure()) {
+                // Banner was already printed by DatabaseService; just exit with code 10.
+                return 10;
+            }
             spdlog::error("Failed to initialize database service");
             return 1;
         }
 
+        // Apply --read-only after initialize() succeeds, before start() accepts connections
+        if (cli.readOnly) {
+            service.setReadOnly(true, "started with --read-only flag");
+        }
+
         // Start service
         service.start();