|
@@ -16,6 +16,26 @@ DatabaseService::~DatabaseService() {
|
|
|
stop();
|
|
stop();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+std::string DatabaseService::readOnlyReason() const {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(reason_mutex_);
|
|
|
|
|
+ return read_only_reason_;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void DatabaseService::setReadOnly(bool value, const std::string& reason) {
|
|
|
|
|
+ {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(reason_mutex_);
|
|
|
|
|
+ read_only_reason_ = value ? reason : "";
|
|
|
|
|
+ }
|
|
|
|
|
+ bool prev = read_only_.exchange(value, std::memory_order_acq_rel);
|
|
|
|
|
+ if (prev != value) {
|
|
|
|
|
+ if (value) {
|
|
|
|
|
+ spdlog::error("Database entered READ-ONLY mode: {}", reason);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ spdlog::info("Database unlocked -- writes accepted");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
bool DatabaseService::initialize() {
|
|
bool DatabaseService::initialize() {
|
|
|
spdlog::info("Initializing database service (node: {})", config_.nodeId);
|
|
spdlog::info("Initializing database service (node: {})", config_.nodeId);
|
|
|
|
|
|
|
@@ -39,7 +59,75 @@ bool DatabaseService::initialize() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Recover from persistence
|
|
// Recover from persistence
|
|
|
- persistence_->recover(*store_);
|
|
|
|
|
|
|
+ recovery_outcome_ = persistence_->recover(*store_);
|
|
|
|
|
+
|
|
|
|
|
+ if (recovery_outcome_.isFailure()) {
|
|
|
|
|
+ const std::string modeStr =
|
|
|
|
|
+ recoveryModeToString(config_.persistenceConfig.recoveryMode);
|
|
|
|
|
+ spdlog::error("");
|
|
|
|
|
+ spdlog::error("+------------------------------------------------------------------+");
|
|
|
|
|
+ spdlog::error("| RECOVERY REFUSED |");
|
|
|
|
|
+ spdlog::error("| |");
|
|
|
|
|
+ spdlog::error("| Mode: {}", modeStr);
|
|
|
|
|
+ spdlog::error("| Expected snapshot: {}", recovery_outcome_.expectedSnapshot.string());
|
|
|
|
|
+ spdlog::error("| Reason: {}", recovery_outcome_.failureReason);
|
|
|
|
|
+ spdlog::error("| |");
|
|
|
|
|
+ spdlog::error("| Snapshots available: {}", recovery_outcome_.snapshotsAvailable);
|
|
|
|
|
+ spdlog::error("| Snapshots attempted: {}", recovery_outcome_.snapshotsAttempted);
|
|
|
|
|
+ spdlog::error("| |");
|
|
|
|
|
+ spdlog::error("| To escalate, restart with one of: |");
|
|
|
|
|
+ spdlog::error("| --recovery-mode=snapshot_fallback Try older snapshots |");
|
|
|
|
|
+ spdlog::error("| --recovery-mode=wal_only Replay WAL only (slow) |");
|
|
|
|
|
+ spdlog::error("| --recovery-mode=best_effort Try all of the above |");
|
|
|
|
|
+ spdlog::error("| --recovery-mode=force_empty Start empty (LAST RESORT) |");
|
|
|
|
|
+ spdlog::error("| |");
|
|
|
|
|
+ spdlog::error("| Or set in config.json: \"recovery\": {{ \"mode\": \"<mode>\" }} |");
|
|
|
|
|
+ spdlog::error("| |");
|
|
|
|
|
+ spdlog::error("| PRESERVE /var/lib/smartbotic-database/ BEFORE ESCALATING. |");
|
|
|
|
|
+ spdlog::error("+------------------------------------------------------------------+");
|
|
|
|
|
+ throw std::runtime_error("recovery failed; refusing to start");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Auto-readonly mode on non-trivial recovery
|
|
|
|
|
+ if (recovery_outcome_.isNonTrivial() && !force_readwrite_) {
|
|
|
|
|
+ std::string reason;
|
|
|
|
|
+ switch (recovery_outcome_.kind) {
|
|
|
|
|
+ case RecoveryOutcome::Kind::SnapshotFellBack:
|
|
|
|
|
+ reason = "fell back to snapshot " +
|
|
|
|
|
+ recovery_outcome_.snapshotUsed.filename().string() +
|
|
|
|
|
+ " because " +
|
|
|
|
|
+ recovery_outcome_.expectedSnapshot.filename().string() +
|
|
|
|
|
+ " failed: " + recovery_outcome_.failureReason;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RecoveryOutcome::Kind::WalOnlyReplay:
|
|
|
|
|
+ reason = "WAL-only replay, no snapshot loaded (" +
|
|
|
|
|
+ std::to_string(recovery_outcome_.walEntriesReplayed) +
|
|
|
|
|
+ " entries)";
|
|
|
|
|
+ break;
|
|
|
|
|
+ case RecoveryOutcome::Kind::ForcedEmpty:
|
|
|
|
|
+ reason = "forced empty by operator (--recovery-mode=force_empty)";
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ reason = "non-trivial recovery";
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ reason += ". Run `smartbotic-db-cli unlock` to accept this state, "
|
|
|
|
|
+ "or restart with --force-readwrite to bypass this check.";
|
|
|
|
|
+
|
|
|
|
|
+ setReadOnly(true, reason);
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::error("");
|
|
|
|
|
+ spdlog::error("+------------------------------------------------------------------+");
|
|
|
|
|
+ spdlog::error("| [ERROR] Database booted in READ-ONLY mode after non-trivial |");
|
|
|
|
|
+ spdlog::error("| recovery. |");
|
|
|
|
|
+ spdlog::error("| |");
|
|
|
|
|
+ spdlog::error("| Reason: {}", reason);
|
|
|
|
|
+ spdlog::error("| |");
|
|
|
|
|
+ spdlog::error("| Writes will be REJECTED until you acknowledge this state: |");
|
|
|
|
|
+ spdlog::error("| smartbotic-db-cli unlock # live, no restart |");
|
|
|
|
|
+ spdlog::error("| smartbotic-database --force-readwrite # on next restart |");
|
|
|
|
|
+ spdlog::error("+------------------------------------------------------------------+");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// Set replication sequence after recovery (WAL sequence is now known)
|
|
// Set replication sequence after recovery (WAL sequence is now known)
|
|
|
replication_->setSequence(persistence_->currentWalSequence());
|
|
replication_->setSequence(persistence_->currentWalSequence());
|
|
@@ -317,12 +405,18 @@ void DatabaseService::setupComponents() {
|
|
|
config_manager_ = std::make_unique<CollectionConfigManager>(*store_);
|
|
config_manager_ = std::make_unique<CollectionConfigManager>(*store_);
|
|
|
store_->setConfigManager(config_manager_.get());
|
|
store_->setConfigManager(config_manager_.get());
|
|
|
|
|
|
|
|
- // Create persistence manager
|
|
|
|
|
- PersistenceManager::Config persistConfig;
|
|
|
|
|
|
|
+ // Create persistence manager. Start from any persistenceConfig values the
|
|
|
|
|
+ // caller (config loader / CLI parser) has already populated — including
|
|
|
|
|
+ // recoveryMode — and overlay the top-level convenience fields.
|
|
|
|
|
+ PersistenceManager::Config persistConfig = config_.persistenceConfig;
|
|
|
persistConfig.dataDir = config_.dataDirectory;
|
|
persistConfig.dataDir = config_.dataDirectory;
|
|
|
persistConfig.walSyncIntervalMs = config_.walSyncIntervalMs;
|
|
persistConfig.walSyncIntervalMs = config_.walSyncIntervalMs;
|
|
|
persistConfig.snapshotIntervalSec = config_.snapshotIntervalSec;
|
|
persistConfig.snapshotIntervalSec = config_.snapshotIntervalSec;
|
|
|
persistConfig.compressionEnabled = config_.compressionEnabled;
|
|
persistConfig.compressionEnabled = config_.compressionEnabled;
|
|
|
|
|
+ // Mirror the resolved recoveryMode back into our Config so downstream code
|
|
|
|
|
+ // (logging, RPC handlers) can inspect config_.persistenceConfig.recoveryMode
|
|
|
|
|
+ // without having to reach into the PersistenceManager.
|
|
|
|
|
+ config_.persistenceConfig = persistConfig;
|
|
|
persistence_ = std::make_unique<PersistenceManager>(persistConfig);
|
|
persistence_ = std::make_unique<PersistenceManager>(persistConfig);
|
|
|
|
|
|
|
|
// Connect store callbacks to persistence - uses setPersistCallback for WAL logging
|
|
// Connect store callbacks to persistence - uses setPersistCallback for WAL logging
|