Auto-migrate legacy LevelDB smsgDB to RocksDB on startup
Build All Platforms / test-linux-unit (push) Waiting to run
Build All Platforms / test-linux-sanitizers (push) Waiting to run
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-linux-daemon (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Blocked by required conditions

Pre-v5.10 the secure-messaging store was backed by LevelDB at
<datadir>/smsgDB/. Phase 3a switched it to RocksDB; existing nodes
upgrading to v5.10 would otherwise lose their pubkey cache and
inbox/outbox because RocksDB can't open a LevelDB tree.

Detection: presence of CURRENT without IDENTITY in smsgDB/. RocksDB
writes IDENTITY on first open; LevelDB never does.

Migration path:
  1. Atomic rename smsgDB/ → smsgDB.leveldb-backup/
  2. Open backup with leveldb::DB (read-only)
  3. Open smsgDB/ with rocksdb::DB (create_if_missing)
  4. Iterate every key, copy in 5000-entry batches
  5. Leave the backup in place — never deleted by the migration code,
     so the user can roll back manually if needed

Triggered lazily inside SecMsgDB::Open so no separate flag or RPC is
needed. Already-migrated nodes (IDENTITY present) skip the path. Once
all users are on v5.10+ the helper and the leveldb headers it pulls
in can be dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 19:19:38 -07:00
parent 68a86c38b5
commit 4fa30abb4b
+137 -3
View File
@@ -60,6 +60,12 @@ Notes:
#include "lz4/lz4.h"
// LevelDB headers retained solely for the one-shot smsgDB leveldb→rocksdb
// migration in MigrateSmsgDBLevelDbToRocksDb. Once all users have upgraded
// past v5.10 the migration helper (and these includes) can be dropped.
#include <leveldb/db.h>
#include <leveldb/iterator.h>
#include "xxhash/xxhash.h"
#include "xxhash/xxhash.c"
@@ -134,6 +140,123 @@ inline rocksdb::Status OpenSmsgDB(const rocksdb::Options& opts,
return OpenSmsgDBImpl(opts, path, dbptr, 0);
}
// ── smsgDB leveldb → rocksdb migration ──────────────────────────────────────
//
// Pre-v5.10 the smessage store was backed by LevelDB at <datadir>/smsgDB/.
// Phase 3a switched it to RocksDB. Existing installations need their pubkey
// cache and inbox/outbox to carry across. The migration is one-shot and
// runs lazily inside SecMsgDB::Open: detect the legacy format, rename the
// directory aside as a backup, copy every key into a fresh rocksdb tree,
// then proceed with the normal open path. The leveldb backup is preserved
// (never deleted) so the user can roll back by deleting smsgDB/ and
// renaming smsgDB.leveldb-backup/ back.
// LevelDB and RocksDB share several filenames (CURRENT, MANIFEST-*, LOG).
// RocksDB additionally writes IDENTITY and OPTIONS-* on first open;
// presence of CURRENT *without* IDENTITY indicates a legacy LevelDB tree.
inline bool IsLegacyLevelDbSmsgDir(const fs::path& dir)
{
if (!fs::exists(dir / "CURRENT"))
return false;
if (fs::exists(dir / "IDENTITY"))
return false;
return true;
}
inline bool MigrateSmsgDBLevelDbToRocksDb(std::string& strError)
{
fs::path smsgDir = GetDataDir() / "smsgDB";
fs::path backupDir = GetDataDir() / "smsgDB.leveldb-backup";
if (fs::exists(backupDir)) {
strError = "smsgDB.leveldb-backup/ already present at " + backupDir.string()
+ " — manual cleanup required before retrying migration.";
return false;
}
printf("smessage: migrating LevelDB-format smsgDB to RocksDB...\n");
// Atomic rename so the legacy data is never deleted by this routine —
// worst case we leave the backup and bail. Filesystem rename within the
// same datadir is atomic on every supported platform.
std::error_code ec;
fs::rename(smsgDir, backupDir, ec);
if (ec) {
strError = "Failed to rename smsgDB to backup: " + ec.message();
return false;
}
// Open backup as leveldb (read-only) — create_if_missing left at default
// false so a malformed dir errors out cleanly.
leveldb::Options leveldbOpts;
leveldb::DB* oldDb = nullptr;
leveldb::Status ls = leveldb::DB::Open(leveldbOpts, backupDir.string(), &oldDb);
if (!ls.ok()) {
strError = "Failed to open legacy LevelDB smsgDB at "
+ backupDir.string() + ": " + ls.ToString();
return false;
}
// Open destination as fresh rocksdb.
rocksdb::Options rdbOpts;
rdbOpts.create_if_missing = true;
rocksdb::DB* newDb = nullptr;
rocksdb::Status rs = OpenSmsgDB(rdbOpts, smsgDir.string(), &newDb);
if (!rs.ok()) {
delete oldDb;
strError = "Failed to create new RocksDB smsgDB: " + rs.ToString();
return false;
}
// Copy every key in batches of 5000.
leveldb::Iterator* it = oldDb->NewIterator(leveldb::ReadOptions());
rocksdb::WriteBatch batch;
int nMigrated = 0;
int nBatch = 0;
bool fOk = true;
for (it->SeekToFirst(); it->Valid(); it->Next()) {
batch.Put(it->key().ToString(), it->value().ToString());
nBatch++;
nMigrated++;
if (nBatch >= 5000) {
rocksdb::Status ws = newDb->Write(rocksdb::WriteOptions(), &batch);
if (!ws.ok()) {
strError = "RocksDB batch write failed during migration: " + ws.ToString();
fOk = false;
break;
}
batch.Clear();
nBatch = 0;
}
}
if (fOk && nBatch > 0) {
rocksdb::Status ws = newDb->Write(rocksdb::WriteOptions(), &batch);
if (!ws.ok()) {
strError = "RocksDB final batch write failed: " + ws.ToString();
fOk = false;
}
}
if (fOk && !it->status().ok()) {
strError = "LevelDB iterator failed mid-migration: " + it->status().ToString();
fOk = false;
}
delete it;
delete oldDb;
delete newDb;
if (!fOk) {
// Leave smsgDB/ in a partially-written state but the backup is
// intact. The user can recover by removing smsgDB/ and renaming
// smsgDB.leveldb-backup/ → smsgDB/.
return false;
}
printf("smessage: migrated %d entries from LevelDB to RocksDB. "
"Original data preserved at %s\n",
nMigrated, backupDir.string().c_str());
return true;
}
const long int SMSG_BUCKET_FILE_SIZE_LIMIT = 0x70000000L;
const int64_t SMSG_THREAD_SHUTDOWN_WAIT_MS = 5000;
const int64_t SMSG_THREAD_SHUTDOWN_POLL_MS = 50;
@@ -423,9 +546,9 @@ bool SecMsgDB::Open(const char* pszMode)
};
bool fCreate = strchr(pszMode, 'c');
fs::path fullpath = GetDataDir() / "smsgDB";
if (!fCreate
&& (!fs::exists(fullpath)
|| !fs::is_directory(fullpath)))
@@ -433,7 +556,18 @@ bool SecMsgDB::Open(const char* pszMode)
printf("SecMsgDB::open() - DB does not exist.\n");
return false;
};
// One-shot migration: pre-v5.10 nodes have a LevelDB tree under smsgDB/.
// Detect that and convert to RocksDB before opening. The legacy data is
// renamed to smsgDB.leveldb-backup/ as a recovery option.
if (fs::is_directory(fullpath) && IsLegacyLevelDbSmsgDir(fullpath)) {
std::string migrateError;
if (!MigrateSmsgDBLevelDbToRocksDb(migrateError)) {
printf("SecMsgDB::open() - migration failed: %s\n", migrateError.c_str());
return false;
}
}
rocksdb::Options options;
options.create_if_missing = fCreate;
rocksdb::Status s = OpenSmsgDB(options, fullpath.string(), &smsgDB);