|
|
@@ -1,6 +1,9 @@
|
|
|
#include "database_service.hpp"
|
|
|
#include "config/config_loader.hpp"
|
|
|
#include "persistence/persistence_manager.hpp"
|
|
|
+#include "storage/document_store_lmdb.hpp"
|
|
|
+#include "storage/lmdb_env.hpp"
|
|
|
+#include "storage/migrate_v1_to_v2.hpp"
|
|
|
|
|
|
#include <spdlog/spdlog.h>
|
|
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
|
|
@@ -95,6 +98,10 @@ void printUsage(const char* programName) {
|
|
|
<< " --recovery-mode=MODE Override recovery mode for this boot\n"
|
|
|
<< " Values: normal, snapshot_fallback, wal_only,\n"
|
|
|
<< " best_effort, force_empty\n"
|
|
|
+ << " --migrate-from-v1 Force one-shot v1.x → v2.0 migration even\n"
|
|
|
+ << " if heuristics don't fire (operator escape hatch)\n"
|
|
|
+ << " --no-auto-migrate Refuse to auto-migrate. If v1.x data is detected\n"
|
|
|
+ << " at the data directory, exit non-zero.\n"
|
|
|
<< "\n"
|
|
|
<< "Environment variables:\n"
|
|
|
<< " DATABASE_NODE_ID Override node ID from config\n"
|
|
|
@@ -140,6 +147,9 @@ struct CliOverrides {
|
|
|
bool readOnly = false;
|
|
|
bool forceReadwrite = false;
|
|
|
std::optional<smartbotic::database::RecoveryMode> recoveryMode;
|
|
|
+ // v2.0 storage Phase C Stage 3 — one-shot v1.x → v2.0 migration toggles.
|
|
|
+ bool migrateFromV1 = false; // --migrate-from-v1 (force, escape hatch)
|
|
|
+ bool noAutoMigrate = false; // --no-auto-migrate (refuse auto-detect)
|
|
|
};
|
|
|
|
|
|
CliOverrides parseCliOverrides(int argc, char* argv[]) {
|
|
|
@@ -150,6 +160,10 @@ CliOverrides parseCliOverrides(int argc, char* argv[]) {
|
|
|
ov.readOnly = true;
|
|
|
} else if (arg == "--force-readwrite") {
|
|
|
ov.forceReadwrite = true;
|
|
|
+ } else if (arg == "--migrate-from-v1") {
|
|
|
+ ov.migrateFromV1 = true;
|
|
|
+ } else if (arg == "--no-auto-migrate") {
|
|
|
+ ov.noAutoMigrate = true;
|
|
|
} else if (arg.rfind("--recovery-mode=", 0) == 0) {
|
|
|
std::string mode = arg.substr(std::string("--recovery-mode=").size());
|
|
|
try {
|
|
|
@@ -163,6 +177,113 @@ CliOverrides parseCliOverrides(int argc, char* argv[]) {
|
|
|
return ov;
|
|
|
}
|
|
|
|
|
|
+// Run the v1.x → v2.0 auto-detect-and-migrate phase. Called from main()
|
|
|
+// before service.initialize() so the heavy v1.x recovery sequence doesn't
|
|
|
+// run twice. Returns 0 on success or on a clean "no migration needed"
|
|
|
+// decision; non-zero on a refused migration (operator must intervene).
|
|
|
+//
|
|
|
+// On success, the v2.0 schema_version=2 marker is set in the env's _meta
|
|
|
+// sub-db, so subsequent boots short-circuit on the migration_complete()
|
|
|
+// check inside should_run_migration().
|
|
|
+int runV1MigrationIfNeeded(const std::filesystem::path& dataDir,
|
|
|
+ bool forceMigrate, bool noAutoMigrate) {
|
|
|
+ using namespace smartbotic::db::storage;
|
|
|
+
|
|
|
+ // v2.0 env lives at <dataDir>/env/ — separate from v1.x's snapshots/
|
|
|
+ // and wal/. Open via the Stage 1 RAII wrapper.
|
|
|
+ LmdbEnvOpts envOpts;
|
|
|
+ envOpts.path = (dataDir / "env").string();
|
|
|
+ envOpts.map_size_bytes = 4ULL << 30; // 4 GiB initial; LMDB grows lazily.
|
|
|
+ envOpts.max_dbs = 1024;
|
|
|
+
|
|
|
+ std::error_code ec;
|
|
|
+ std::filesystem::create_directories(envOpts.path, ec);
|
|
|
+
|
|
|
+ std::unique_ptr<LmdbEnv> env;
|
|
|
+ try {
|
|
|
+ env = std::make_unique<LmdbEnv>(envOpts);
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ spdlog::error("Failed to open v2.0 LMDB env at {}: {}", envOpts.path, e.what());
|
|
|
+ // Pre-Stage-4: this isn't a hard failure path yet because the
|
|
|
+ // service still boots from v1.x. Treat as "skip migration, log it"
|
|
|
+ // and let the rest of boot proceed. Once Stage 4 lands, this
|
|
|
+ // becomes a hard exit.
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ AutoMigrateOptions opts;
|
|
|
+ opts.force = forceMigrate;
|
|
|
+ opts.disabled = noAutoMigrate;
|
|
|
+ AutoMigrateDecision decision =
|
|
|
+ should_run_migration(dataDir.string(), *env, opts);
|
|
|
+
|
|
|
+ switch (decision.action) {
|
|
|
+ case AutoMigrateDecision::Action::SkipNoV1Data:
|
|
|
+ spdlog::info("v2.0 migration: {}", decision.message);
|
|
|
+ return 0;
|
|
|
+ case AutoMigrateDecision::Action::SkipAlreadyMigrated:
|
|
|
+ spdlog::info("v2.0 migration: {}", decision.message);
|
|
|
+ return 0;
|
|
|
+ case AutoMigrateDecision::Action::BlockedByFlag:
|
|
|
+ spdlog::error("v2.0 migration BLOCKED: {}", decision.message);
|
|
|
+ std::cerr << "ERROR: " << decision.message << std::endl;
|
|
|
+ return 12; // exit code 12 = migration refused by operator
|
|
|
+ case AutoMigrateDecision::Action::RunMigration:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Loud banner — operators reading the journal need to know exactly
|
|
|
+ // what's about to happen.
|
|
|
+ spdlog::info("================================================================");
|
|
|
+ spdlog::info("v2.0 storage migration starting");
|
|
|
+ spdlog::info(" Source v1.x data dir: {}", dataDir.string());
|
|
|
+ spdlog::info(" Target v2.0 LMDB env: {}", envOpts.path);
|
|
|
+ spdlog::info(" This is a one-shot operation. The v1.x data dir is");
|
|
|
+ spdlog::info(" read-only and will not be modified. Rollback path:");
|
|
|
+ spdlog::info(" restore /var/backups/smartbotic-database/pre-upgrade-*/.");
|
|
|
+ spdlog::info("================================================================");
|
|
|
+
|
|
|
+#ifdef HAVE_SYSTEMD
|
|
|
+ sd_notify(0, "EXTEND_TIMEOUT_USEC=600000000");
|
|
|
+ sd_notify(0, "STATUS=Migrating from v1.x");
|
|
|
+#endif
|
|
|
+
|
|
|
+ LmdbDocumentStore target(*env);
|
|
|
+
|
|
|
+ auto progressCb = [](const MigrationProgress& p) {
|
|
|
+#ifdef HAVE_SYSTEMD
|
|
|
+ std::string status = "STATUS=Migrating from v1.x: " +
|
|
|
+ std::to_string(p.docs_migrated) + "/" +
|
|
|
+ std::to_string(p.docs_total);
|
|
|
+ if (!p.current_collection.empty()) {
|
|
|
+ status += " in '" + p.current_collection + "'";
|
|
|
+ }
|
|
|
+ sd_notify(0, status.c_str());
|
|
|
+ sd_notify(0, "EXTEND_TIMEOUT_USEC=600000000");
|
|
|
+#endif
|
|
|
+ if (p.docs_migrated > 0 && (p.docs_migrated % 5000) == 0) {
|
|
|
+ spdlog::info("v2.0 migration progress: {}/{} docs (collection '{}')",
|
|
|
+ p.docs_migrated, p.docs_total, p.current_collection);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ MigrationResult result =
|
|
|
+ migrate_v1_to_v2(dataDir.string(), target, *env, progressCb);
|
|
|
+
|
|
|
+ if (!result.success) {
|
|
|
+#ifdef HAVE_SYSTEMD
|
|
|
+ sd_notify(0, "STATUS=Migration failed");
|
|
|
+#endif
|
|
|
+ spdlog::error("v2.0 migration FAILED: {}", result.error);
|
|
|
+ std::cerr << "ERROR: v2.0 migration failed:\n " << result.error << std::endl;
|
|
|
+ return 13; // exit code 13 = migration failure
|
|
|
+ }
|
|
|
+
|
|
|
+ spdlog::info("v2.0 migration complete: {} docs across {} collections",
|
|
|
+ result.docs_migrated, result.collections_migrated);
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
std::filesystem::path findConfigFile(int argc, char* argv[]) {
|
|
|
// Check command line arguments
|
|
|
for (int i = 1; i < argc; ++i) {
|
|
|
@@ -280,7 +401,8 @@ int main(int argc, char* argv[]) {
|
|
|
config.nodeId = nodeId;
|
|
|
}
|
|
|
|
|
|
- // Parse CLI overrides (--read-only, --force-readwrite, --recovery-mode=...)
|
|
|
+ // Parse CLI overrides (--read-only, --force-readwrite, --recovery-mode=...,
|
|
|
+ // --migrate-from-v1, --no-auto-migrate)
|
|
|
auto cli = parseCliOverrides(argc, argv);
|
|
|
|
|
|
// Apply --recovery-mode to persistence config before service construction
|
|
|
@@ -288,6 +410,19 @@ int main(int argc, char* argv[]) {
|
|
|
config.persistenceConfig.recoveryMode = *cli.recoveryMode;
|
|
|
}
|
|
|
|
|
|
+ // v2.0 storage Phase C Stage 3 — auto-detect-and-migrate before the
|
|
|
+ // service initialises. Runs against <dataDir>/env/ (the v2.0 LMDB
|
|
|
+ // env). On success, sets the schema_version=2 marker so subsequent
|
|
|
+ // boots short-circuit. On a refused migration (--no-auto-migrate +
|
|
|
+ // v1.x data present, or migration failure) we exit non-zero before
|
|
|
+ // the service starts.
|
|
|
+ if (int rc = runV1MigrationIfNeeded(config.dataDirectory,
|
|
|
+ cli.migrateFromV1,
|
|
|
+ cli.noAutoMigrate);
|
|
|
+ rc != 0) {
|
|
|
+ return rc;
|
|
|
+ }
|
|
|
+
|
|
|
// Create service
|
|
|
smartbotic::database::DatabaseService service(config);
|
|
|
g_service = &service;
|