|
|
@@ -56,8 +56,13 @@ namespace {
|
|
|
constexpr const char* kMetaSubDb = "_meta";
|
|
|
constexpr const char* kKeySchemaVersion = "schema_version";
|
|
|
constexpr const char* kKeyMigratedAt = "migration_completed_at";
|
|
|
+constexpr const char* kKeyInProgress = "migration_in_progress";
|
|
|
constexpr const char* kSchemaVersionV2 = "2";
|
|
|
|
|
|
+// Backup path advertised in error messages (v1.9.5 backup-before-upgrade
|
|
|
+// hook stages the v1.x data dir here, per debian/preinst).
|
|
|
+constexpr const char* kBackupRoot = "/var/backups/smartbotic-database/";
|
|
|
+
|
|
|
// -------------------------------------------------------------------------
|
|
|
// LMDB helpers — local subset.
|
|
|
// -------------------------------------------------------------------------
|
|
|
@@ -96,6 +101,26 @@ std::optional<std::string> read_meta_key(LmdbEnv& env, const char* key) {
|
|
|
return std::string(static_cast<const char*>(v.mv_data), v.mv_size);
|
|
|
}
|
|
|
|
|
|
+// Set the migration_in_progress flag in a short write txn. Called at the
|
|
|
+// start of every migration run so that a crash mid-flight leaves the flag
|
|
|
+// set; the next attempt clears it as part of mark_complete()'s atomic
|
|
|
+// completion txn.
|
|
|
+void mark_in_progress(LmdbEnv& env) {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ MDB_dbi dbi = 0;
|
|
|
+ mdb_check(mdb_dbi_open(wtxn.raw(), kMetaSubDb, MDB_CREATE, &dbi),
|
|
|
+ "dbi_open (_meta create)");
|
|
|
+ std::string val = "1";
|
|
|
+ MDB_val k = to_val(std::string_view(kKeyInProgress));
|
|
|
+ MDB_val v = to_val(val);
|
|
|
+ mdb_check(mdb_put(wtxn.raw(), dbi, &k, &v, 0), "put (_meta in_progress)");
|
|
|
+ wtxn.commit();
|
|
|
+}
|
|
|
+
|
|
|
+// Atomic completion: writes the schema_version + completed_at markers AND
|
|
|
+// deletes the in_progress flag in a single write txn. Either all three
|
|
|
+// changes land or none of them do; on any failure before this txn the
|
|
|
+// in_progress flag stays set and the next boot sees it.
|
|
|
void mark_complete(LmdbEnv& env) {
|
|
|
WriteTxn wtxn(env);
|
|
|
MDB_dbi dbi = 0;
|
|
|
@@ -119,9 +144,35 @@ void mark_complete(LmdbEnv& env) {
|
|
|
mdb_check(mdb_put(wtxn.raw(), dbi, &k, &v, 0),
|
|
|
"put (_meta migration_completed_at)");
|
|
|
}
|
|
|
+ // Clear in_progress flag — delete the key outright so a future
|
|
|
+ // `migration_in_progress()` returns false cleanly.
|
|
|
+ {
|
|
|
+ MDB_val k = to_val(std::string_view(kKeyInProgress));
|
|
|
+ int rc = mdb_del(wtxn.raw(), dbi, &k, nullptr);
|
|
|
+ if (rc != MDB_SUCCESS && rc != MDB_NOTFOUND) {
|
|
|
+ throw_mdb(rc, "_meta del in_progress");
|
|
|
+ }
|
|
|
+ }
|
|
|
wtxn.commit();
|
|
|
}
|
|
|
|
|
|
+// Build an operator-actionable error message. Includes the collection
|
|
|
+// that failed (if known), the underlying cause, and the v1.9.5 backup
|
|
|
+// path so operators don't have to grep `dpkg` to find their rollback.
|
|
|
+std::string format_error(const std::string& where_collection,
|
|
|
+ const std::string& cause) {
|
|
|
+ std::string msg = "v2.0 migration failed";
|
|
|
+ if (!where_collection.empty()) {
|
|
|
+ msg += " (collection '" + where_collection + "')";
|
|
|
+ }
|
|
|
+ msg += ": ";
|
|
|
+ msg += cause;
|
|
|
+ msg += ". Rollback: stop smartbotic-database and restore from ";
|
|
|
+ msg += kBackupRoot;
|
|
|
+ msg += "pre-upgrade-<ts>-from-1.x/.";
|
|
|
+ return msg;
|
|
|
+}
|
|
|
+
|
|
|
// -------------------------------------------------------------------------
|
|
|
// v1.x recovery driver — drives SnapshotManager + WriteAheadLog directly
|
|
|
// rather than instantiating a PersistenceManager (which spins up background
|
|
|
@@ -305,6 +356,16 @@ bool migration_complete(LmdbEnv& env) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+bool migration_in_progress(LmdbEnv& env) {
|
|
|
+ try {
|
|
|
+ auto v = read_meta_key(env, kKeyInProgress);
|
|
|
+ return v.has_value() && !v->empty();
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ spdlog::warn("migration_in_progress: read failed: {}", e.what());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
AutoMigrateDecision should_run_migration(const std::string& v1_data_dir,
|
|
|
LmdbEnv& env,
|
|
|
const AutoMigrateOptions& opts) {
|
|
|
@@ -363,6 +424,7 @@ MigrationResult migrate_v1_to_v2(
|
|
|
std::function<void(const MigrationProgress&)> progress_cb) {
|
|
|
|
|
|
MigrationResult result;
|
|
|
+ std::string current_collection; // tracked so format_error() can name it
|
|
|
try {
|
|
|
// 1. Pre-flight: clean install case.
|
|
|
fs::path snap_dir = fs::path(v1_data_dir) / "snapshots";
|
|
|
@@ -386,7 +448,19 @@ MigrationResult migrate_v1_to_v2(
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
- // 3. Drive v1.x recovery into a temporary MemoryStore.
|
|
|
+ // 3. In-progress detection (Task 3.4). If the previous attempt
|
|
|
+ // died mid-flight, proceed as a retry — LMDB put-overwrite
|
|
|
+ // semantics make this safe.
|
|
|
+ if (migration_in_progress(env)) {
|
|
|
+ spdlog::warn("migration: previous attempt did not finish (in_progress "
|
|
|
+ "flag set) — retrying. Partial state will be overwritten.");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. Mark in-progress so a crash before mark_complete() leaves a
|
|
|
+ // detectable breadcrumb.
|
|
|
+ mark_in_progress(env);
|
|
|
+
|
|
|
+ // 5. Drive v1.x recovery into a temporary MemoryStore.
|
|
|
smartbotic::database::MemoryStore::Config storeCfg;
|
|
|
// 8 GB ceiling — migration is bounded by source size and the
|
|
|
// ceiling is just to keep the in-RAM mirror from triggering
|
|
|
@@ -419,6 +493,7 @@ MigrationResult migrate_v1_to_v2(
|
|
|
// 5. Copy phase. ONE PUT PER DOC — latency-tolerant.
|
|
|
uint64_t docs_migrated = 0;
|
|
|
for (const auto& collection : collections) {
|
|
|
+ current_collection = collection;
|
|
|
auto docs = mstore.getAllDocuments(collection);
|
|
|
|
|
|
// Some v1.x docs ship without `collection` set; round-trip
|
|
|
@@ -465,8 +540,11 @@ MigrationResult migrate_v1_to_v2(
|
|
|
}
|
|
|
|
|
|
result.docs_migrated = docs_migrated;
|
|
|
+ current_collection.clear();
|
|
|
|
|
|
- // 6. Final marker write.
|
|
|
+ // 6. Final marker write. Single write txn atomically sets the
|
|
|
+ // schema_version=2 + completed_at markers and clears the
|
|
|
+ // in_progress flag.
|
|
|
mark_complete(env);
|
|
|
|
|
|
if (progress_cb) {
|
|
|
@@ -486,12 +564,15 @@ MigrationResult migrate_v1_to_v2(
|
|
|
return result;
|
|
|
} catch (const std::exception& e) {
|
|
|
result.success = false;
|
|
|
- result.error = std::string("v2.0 migration failed: ") + e.what();
|
|
|
+ // format_error embeds the failed-collection name (if known), the
|
|
|
+ // underlying cause, and the v1.9.5 backup path for rollback. The
|
|
|
+ // in_progress flag stays set — next boot's auto-detect retries.
|
|
|
+ result.error = format_error(current_collection, e.what());
|
|
|
spdlog::error("migration: FAILED — {}", result.error);
|
|
|
return result;
|
|
|
} catch (...) {
|
|
|
result.success = false;
|
|
|
- result.error = "v2.0 migration failed: unknown exception";
|
|
|
+ result.error = format_error(current_collection, "unknown exception");
|
|
|
spdlog::error("migration: FAILED — {}", result.error);
|
|
|
return result;
|
|
|
}
|