database_service.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. #include "database_service.hpp"
  2. #include "json_parse.hpp"
  3. #include <grpcpp/grpcpp.h>
  4. #include <grpcpp/resource_quota.h>
  5. #include <spdlog/spdlog.h>
  6. #include <fstream>
  7. #ifdef HAVE_SYSTEMD
  8. #include <systemd/sd-daemon.h>
  9. #endif
  10. #if defined(__GLIBC__) && !defined(__APPLE__)
  11. #include <malloc.h>
  12. #endif
  13. namespace smartbotic::database {
  14. DatabaseService::DatabaseService(Config config)
  15. : config_(std::move(config))
  16. {
  17. }
  18. DatabaseService::~DatabaseService() {
  19. stop();
  20. }
  21. std::string DatabaseService::readOnlyReason() const {
  22. std::lock_guard<std::mutex> lock(reason_mutex_);
  23. return read_only_reason_;
  24. }
  25. void DatabaseService::setReadOnly(bool value, const std::string& reason) {
  26. {
  27. std::lock_guard<std::mutex> lock(reason_mutex_);
  28. read_only_reason_ = value ? reason : "";
  29. }
  30. bool prev = read_only_.exchange(value, std::memory_order_acq_rel);
  31. if (prev != value) {
  32. if (value) {
  33. spdlog::error("Database entered READ-ONLY mode: {}", reason);
  34. } else {
  35. spdlog::info("Database unlocked -- writes accepted");
  36. }
  37. }
  38. }
  39. bool DatabaseService::initialize() {
  40. spdlog::info("Initializing database service (node: {})", config_.nodeId);
  41. try {
  42. // Create data directory if needed
  43. std::error_code ec;
  44. std::filesystem::create_directories(config_.dataDirectory, ec);
  45. if (ec) {
  46. spdlog::error("Failed to create data directory: {}", ec.message());
  47. return false;
  48. }
  49. setupComponents();
  50. // Initialize encryption
  51. if (config_.encryptionEnabled) {
  52. if (!encryption_->initialize()) {
  53. spdlog::error("Failed to initialize encryption");
  54. return false;
  55. }
  56. }
  57. // Recover from persistence
  58. recovery_outcome_ = persistence_->recover(*store_);
  59. #if defined(__GLIBC__) && !defined(__APPLE__)
  60. // v1.9.3 — release freelist pages accumulated during recovery. v1.9.1
  61. // added trim at end of loadSnapshot, but the WAL replay phase that
  62. // runs afterward (`persistence_->recover`'s `replayWal` step) also
  63. // burns through GBs of small Document JSON allocations whose pages
  64. // sit on the per-thread freelist with no subsequent allocation to
  65. // shake them loose. On Zoe (BUG-memory-leak-zoe.md update 15:05)
  66. // this was 5.7 GB at 23 min uptime — fully reclaimable via
  67. // malloc_trim, just nothing called it. Paired with the periodic
  68. // every-5-minute trim in MemoryStore::logMemoryCheck, so any
  69. // post-boot bloat that escapes this trim gets cleaned up shortly.
  70. ::malloc_trim(0);
  71. #endif
  72. if (recovery_outcome_.isFailure()) {
  73. const std::string modeStr =
  74. recoveryModeToString(config_.persistenceConfig.recoveryMode);
  75. spdlog::error("");
  76. spdlog::error("+------------------------------------------------------------------+");
  77. spdlog::error("| RECOVERY REFUSED |");
  78. spdlog::error("| |");
  79. spdlog::error("| Mode: {}", modeStr);
  80. spdlog::error("| Expected snapshot: {}", recovery_outcome_.expectedSnapshot.string());
  81. spdlog::error("| Reason: {}", recovery_outcome_.failureReason);
  82. spdlog::error("| |");
  83. spdlog::error("| Snapshots available: {}", recovery_outcome_.snapshotsAvailable);
  84. spdlog::error("| Snapshots attempted: {}", recovery_outcome_.snapshotsAttempted);
  85. spdlog::error("| |");
  86. spdlog::error("| To escalate, restart with one of: |");
  87. spdlog::error("| --recovery-mode=snapshot_fallback Try older snapshots |");
  88. spdlog::error("| --recovery-mode=wal_only Replay WAL only (slow) |");
  89. spdlog::error("| --recovery-mode=best_effort Try all of the above |");
  90. spdlog::error("| --recovery-mode=force_empty Start empty (LAST RESORT) |");
  91. spdlog::error("| |");
  92. spdlog::error("| Or set in config.json: \"recovery\": {{ \"mode\": \"<mode>\" }} |");
  93. spdlog::error("| |");
  94. spdlog::error("| PRESERVE /var/lib/smartbotic-database/ BEFORE ESCALATING. |");
  95. spdlog::error("+------------------------------------------------------------------+");
  96. throw std::runtime_error("recovery failed; refusing to start");
  97. }
  98. // Auto-readonly mode on non-trivial recovery
  99. if (recovery_outcome_.isNonTrivial() && !force_readwrite_) {
  100. std::string reason;
  101. switch (recovery_outcome_.kind) {
  102. case RecoveryOutcome::Kind::SnapshotFellBack:
  103. reason = "fell back to snapshot " +
  104. recovery_outcome_.snapshotUsed.filename().string() +
  105. " because " +
  106. recovery_outcome_.expectedSnapshot.filename().string() +
  107. " failed: " + recovery_outcome_.failureReason;
  108. break;
  109. case RecoveryOutcome::Kind::WalOnlyReplay:
  110. reason = "WAL-only replay, no snapshot loaded (" +
  111. std::to_string(recovery_outcome_.walEntriesReplayed) +
  112. " entries)";
  113. break;
  114. case RecoveryOutcome::Kind::ForcedEmpty:
  115. reason = "forced empty by operator (--recovery-mode=force_empty)";
  116. break;
  117. default:
  118. reason = "non-trivial recovery";
  119. break;
  120. }
  121. reason += ". Run `smartbotic-db-cli unlock` to accept this state, "
  122. "or restart with --force-readwrite to bypass this check.";
  123. setReadOnly(true, reason);
  124. spdlog::error("");
  125. spdlog::error("+------------------------------------------------------------------+");
  126. spdlog::error("| [ERROR] Database booted in READ-ONLY mode after non-trivial |");
  127. spdlog::error("| recovery. |");
  128. spdlog::error("| |");
  129. spdlog::error("| Reason: {}", reason);
  130. spdlog::error("| |");
  131. spdlog::error("| Writes will be REJECTED until you acknowledge this state: |");
  132. spdlog::error("| smartbotic-db-cli unlock # live, no restart |");
  133. spdlog::error("| smartbotic-database --force-readwrite # on next restart |");
  134. spdlog::error("+------------------------------------------------------------------+");
  135. }
  136. // Set replication sequence after recovery (WAL sequence is now known)
  137. replication_->setSequence(persistence_->currentWalSequence());
  138. // v1.8.0 — start the persistence manager before loadFromStore /
  139. // migrations so any system-collection writes those phases produce
  140. // (view docs, _collection_meta entries, _migrations recordings)
  141. // reach the WAL like normal mutations. Pre-v1.8, persistence_ was
  142. // started later in start(), which silently dropped those writes
  143. // (running_=false → logInsert no-op). That meant migrated views
  144. // only "survived" because the runner re-created them every boot,
  145. // and a manually-created view that landed during the racy startup
  146. // window (before full readiness) could be lost.
  147. if (!persistence_->start()) {
  148. spdlog::error("Failed to start persistence manager before migrations");
  149. return false;
  150. }
  151. // Load view definitions from the _views system collection (which is now
  152. // populated by the persistence recovery above).
  153. view_manager_->loadFromStore();
  154. // Load per-collection configs from the _collection_meta system collection.
  155. // Must happen AFTER persistence recovery and BEFORE migrations so any
  156. // migration-created documents are stamped with the correct precision.
  157. config_manager_->loadFromStore();
  158. // Run migrations if enabled
  159. if (config_.migrations.enabled && !config_.migrations.directory.empty()) {
  160. if (!runMigrations()) {
  161. if (config_.migrations.failOnError) {
  162. spdlog::error("Failed to run migrations");
  163. return false;
  164. }
  165. spdlog::warn("Some migrations failed, continuing anyway");
  166. }
  167. }
  168. spdlog::info("Database service initialized successfully");
  169. return true;
  170. } catch (const std::exception& e) {
  171. spdlog::error("Failed to initialize database service: {}", e.what());
  172. return false;
  173. }
  174. }
  175. bool DatabaseService::runMigrations() {
  176. if (!config_.migrations.enabled) {
  177. return true;
  178. }
  179. MigrationRunner::Config migrationConfig;
  180. migrationConfig.directory = config_.migrations.directory;
  181. migrationConfig.autoApply = config_.migrations.autoApply;
  182. migrationConfig.failOnError = config_.migrations.failOnError;
  183. migrationRunner_ = std::make_unique<MigrationRunner>(*store_, *view_manager_, migrationConfig);
  184. return migrationRunner_->runMigrations();
  185. }
  186. void DatabaseService::start() {
  187. if (running_.exchange(true)) {
  188. return;
  189. }
  190. spdlog::info("Starting database service on {}:{}", config_.bindAddress, config_.rpcPort);
  191. // Start components
  192. store_->start();
  193. persistence_->start();
  194. events_->start();
  195. files_->start();
  196. if (config_.replicationEnabled) {
  197. replication_->start();
  198. // Set initial local collections for discovery
  199. replication_->setLocalCollections(store_->listCollections());
  200. }
  201. // Start gRPC server
  202. startGrpcServer();
  203. spdlog::info("Database service started");
  204. }
  205. void DatabaseService::stop() {
  206. if (!running_.exchange(false)) {
  207. return;
  208. }
  209. spdlog::info("Stopping database service...");
  210. // Stop gRPC server first
  211. stopGrpcServer();
  212. // Stop components in reverse order
  213. if (replication_) {
  214. replication_->stop();
  215. }
  216. if (files_) {
  217. files_->stop();
  218. }
  219. if (events_) {
  220. events_->stop();
  221. }
  222. if (persistence_ && store_) {
  223. // v1.8.0 — take a final snapshot on graceful shutdown so the next
  224. // boot's recovery is "trivial" (snapshot-loaded) instead of "WAL-only
  225. // replay" which trips auto-readonly mode. This makes ordinary
  226. // restarts pass through cleanly without needing
  227. // `smartbotic-db-cli unlock`.
  228. //
  229. // v1.8.2 — extend systemd's stop watchdog before doing it. On Zoe
  230. // (1.85M docs / ~5 GB tracked) the serialize takes ~70 s and the
  231. // default TimeoutStopSec=30s SIGKILLed the process mid-write, peak
  232. // RSS hit 11 GB (live state + uncompressed serialize buffer), and
  233. // the new snapshot never made it to disk anyway. EXTEND_TIMEOUT_USEC
  234. // tells systemd we're working — same protocol the recovery path
  235. // uses during startup. Paired with TimeoutStopSec=300 in the unit
  236. // file as the static cap.
  237. #ifdef HAVE_SYSTEMD
  238. sd_notify(0, "EXTEND_TIMEOUT_USEC=600000000");
  239. sd_notify(0, "STATUS=Writing final snapshot");
  240. #endif
  241. try {
  242. persistence_->forceSnapshot(*store_);
  243. spdlog::info("Final snapshot taken on shutdown");
  244. } catch (const std::exception& e) {
  245. spdlog::warn("Final snapshot on shutdown failed: {} — recovery on next "
  246. "boot may be WAL-only and trip auto-readonly mode", e.what());
  247. }
  248. }
  249. if (persistence_) {
  250. persistence_->stop();
  251. }
  252. if (store_) {
  253. store_->stop();
  254. }
  255. spdlog::info("Database service stopped");
  256. }
  257. void DatabaseService::wait() {
  258. if (serverThread_.joinable()) {
  259. serverThread_.join();
  260. }
  261. }
  262. void DatabaseService::signalStop() {
  263. stopRequested_ = true;
  264. stop();
  265. }
  266. nlohmann::json DatabaseService::getStats() const {
  267. nlohmann::json stats;
  268. if (store_) {
  269. auto storeStats = store_->getStats();
  270. stats["documents"] = storeStats.totalDocuments;
  271. stats["collections"] = storeStats.totalCollections;
  272. stats["memory_bytes"] = storeStats.estimatedMemoryBytes;
  273. stats["inserts"] = storeStats.insertCount;
  274. stats["updates"] = storeStats.updateCount;
  275. stats["deletes"] = storeStats.deleteCount;
  276. stats["queries"] = storeStats.queryCount;
  277. }
  278. if (persistence_) {
  279. auto persistStats = persistence_->getStats();
  280. stats["wal_sequence"] = persistStats.walSequence;
  281. stats["wal_size_bytes"] = persistStats.walSizeBytes;
  282. stats["snapshot_count"] = persistStats.snapshotCount;
  283. stats["last_snapshot_sequence"] = persistStats.lastSnapshotSequence;
  284. }
  285. if (events_) {
  286. stats["subscriptions"] = events_->subscriptionCount();
  287. }
  288. return stats;
  289. }
  290. DatabaseService::Config DatabaseService::loadConfig(const std::filesystem::path& configPath) {
  291. std::ifstream file(configPath);
  292. if (!file) {
  293. throw std::runtime_error("Failed to open config file: " + configPath.string());
  294. }
  295. nlohmann::json json;
  296. file >> json;
  297. return parseConfig(json);
  298. }
  299. DatabaseService::Config DatabaseService::parseConfig(const nlohmann::json& json) {
  300. Config config;
  301. // Check for both "storage" and "database" keys for backward compatibility
  302. const nlohmann::json* dbConfig = nullptr;
  303. if (json.contains("database")) {
  304. dbConfig = &json["database"];
  305. } else if (json.contains("storage")) {
  306. dbConfig = &json["storage"];
  307. }
  308. if (dbConfig) {
  309. const auto& db = *dbConfig;
  310. config.nodeId = db.value("node_id", config.nodeId);
  311. config.bindAddress = db.value("bind_address", config.bindAddress);
  312. config.rpcPort = db.value("rpc_port", config.rpcPort);
  313. // Expand environment variables in data directory
  314. std::string dataDir = db.value("data_directory", "");
  315. if (dataDir.find("${HOME}") != std::string::npos) {
  316. const char* home = std::getenv("HOME");
  317. if (home) {
  318. size_t pos = dataDir.find("${HOME}");
  319. dataDir.replace(pos, 7, home);
  320. }
  321. }
  322. config.dataDirectory = dataDir;
  323. // Migrations settings
  324. if (db.contains("migrations")) {
  325. auto& migrations = db["migrations"];
  326. config.migrations.enabled = migrations.value("enabled", config.migrations.enabled);
  327. config.migrations.autoApply = migrations.value("auto_apply", config.migrations.autoApply);
  328. config.migrations.failOnError = migrations.value("fail_on_error", config.migrations.failOnError);
  329. std::string migDir = migrations.value("directory", "");
  330. if (migDir.find("${HOME}") != std::string::npos) {
  331. const char* home = std::getenv("HOME");
  332. if (home) {
  333. size_t pos = migDir.find("${HOME}");
  334. migDir.replace(pos, 7, home);
  335. }
  336. }
  337. config.migrations.directory = migDir;
  338. }
  339. // Memory eviction settings
  340. if (db.contains("memory")) {
  341. auto& memory = db["memory"];
  342. config.maxMemoryMb = memory.value("max_memory_mb", config.maxMemoryMb);
  343. config.evictionThresholdPercent = memory.value("eviction_threshold_percent", config.evictionThresholdPercent);
  344. config.evictionTargetPercent = memory.value("eviction_target_percent", config.evictionTargetPercent);
  345. config.evictionCheckIntervalMs = memory.value("eviction_check_interval_ms", config.evictionCheckIntervalMs);
  346. // NEW v1.7.0 eviction tuning (schema-only in T2; runtime use lands in T3/T4)
  347. config.evictionChunkSize = memory.value("eviction_chunk_size", config.evictionChunkSize);
  348. config.evictionChunkPauseMs = memory.value("eviction_chunk_pause_ms", config.evictionChunkPauseMs);
  349. config.maxEvictionPassesPerTrigger = memory.value("max_eviction_passes_per_trigger", config.maxEvictionPassesPerTrigger);
  350. config.hotWriteFloorMs = memory.value("hot_write_floor_ms", config.hotWriteFloorMs);
  351. config.memorySoftPercent = memory.value("memory_soft_percent", config.memorySoftPercent);
  352. config.memoryHardPercent = memory.value("memory_hard_percent", config.memoryHardPercent);
  353. config.memoryEmergencyPercent = memory.value("memory_emergency_percent", config.memoryEmergencyPercent);
  354. // v1.7.0 T10 — eviction burst event threshold
  355. config.evictionBurstThreshold = memory.value("eviction_burst_threshold", config.evictionBurstThreshold);
  356. }
  357. // Persistence settings
  358. if (db.contains("persistence")) {
  359. auto& persistence = db["persistence"];
  360. config.walSyncIntervalMs = persistence.value("wal_sync_interval_ms", config.walSyncIntervalMs);
  361. config.snapshotIntervalSec = persistence.value("snapshot_interval_sec", config.snapshotIntervalSec);
  362. config.compressionEnabled = persistence.value("compression", "lz4") != "none";
  363. // Snapshot durability (NEW in v1.6.1)
  364. if (persistence.contains("snapshots")) {
  365. const auto& snap = persistence["snapshots"];
  366. config.persistenceConfig.validateAfterWrite = snap.value("validate_after_write", config.persistenceConfig.validateAfterWrite);
  367. config.persistenceConfig.cleanupOnlyIfVerified = snap.value("cleanup_only_if_verified", config.persistenceConfig.cleanupOnlyIfVerified);
  368. }
  369. // Recovery (NEW in v1.6.1)
  370. if (persistence.contains("recovery")) {
  371. const auto& rec = persistence["recovery"];
  372. std::string modeStr = rec.value("mode", std::string("normal"));
  373. try {
  374. config.persistenceConfig.recoveryMode = recoveryModeFromString(modeStr);
  375. } catch (const std::exception& e) {
  376. spdlog::warn("Invalid recovery.mode '{}', defaulting to 'normal': {}",
  377. modeStr, e.what());
  378. config.persistenceConfig.recoveryMode = RecoveryMode::Normal;
  379. }
  380. config.persistenceConfig.autoEscalate = rec.value("auto_escalate", config.persistenceConfig.autoEscalate);
  381. config.persistenceConfig.allowEmptyOnFreshInstall = rec.value("allow_empty_on_fresh_install", config.persistenceConfig.allowEmptyOnFreshInstall);
  382. }
  383. }
  384. // File settings
  385. if (db.contains("files")) {
  386. auto& files = db["files"];
  387. config.maxFileSizeMb = files.value("max_file_size_mb", config.maxFileSizeMb);
  388. config.fileCleanupIntervalSec = files.value("cleanup_orphans_interval_sec", config.fileCleanupIntervalSec);
  389. if (files.contains("allowed_types")) {
  390. for (const auto& type : files["allowed_types"]) {
  391. config.allowedFileTypes.push_back(type.get<std::string>());
  392. }
  393. }
  394. }
  395. // Encryption settings
  396. if (db.contains("encryption")) {
  397. auto& encryption = db["encryption"];
  398. config.encryptionEnabled = encryption.value("enabled", config.encryptionEnabled);
  399. config.autoGenerateKey = encryption.value("auto_generate_key", config.autoGenerateKey);
  400. std::string keyFile = encryption.value("key_file", "");
  401. if (keyFile.find("${HOME}") != std::string::npos) {
  402. const char* home = std::getenv("HOME");
  403. if (home) {
  404. size_t pos = keyFile.find("${HOME}");
  405. keyFile.replace(pos, 7, home);
  406. }
  407. }
  408. config.keyFilePath = keyFile;
  409. }
  410. // Replication settings
  411. if (db.contains("replication")) {
  412. auto& replication = db["replication"];
  413. config.replicationEnabled = replication.value("enabled", config.replicationEnabled);
  414. config.conflictResolution = replication.value("conflict_resolution", config.conflictResolution);
  415. if (replication.contains("peers")) {
  416. for (const auto& peer : replication["peers"]) {
  417. config.peerAddresses.push_back(peer.get<std::string>());
  418. }
  419. }
  420. }
  421. // gRPC settings (v1.6.2 — concurrency cap + configurable message sizes)
  422. if (db.contains("grpc")) {
  423. const auto& grpc = db["grpc"];
  424. config.grpc.maxReceiveMessageSizeMb =
  425. grpc.value("max_receive_message_size_mb", config.grpc.maxReceiveMessageSizeMb);
  426. config.grpc.maxSendMessageSizeMb =
  427. grpc.value("max_send_message_size_mb", config.grpc.maxSendMessageSizeMb);
  428. config.grpc.resourceQuotaMemoryMb =
  429. grpc.value("resource_quota_memory_mb", config.grpc.resourceQuotaMemoryMb);
  430. config.grpc.maxConcurrentSubscribeStreams =
  431. grpc.value("max_concurrent_subscribe_streams", config.grpc.maxConcurrentSubscribeStreams);
  432. config.grpc.maxConcurrentFileStreams =
  433. grpc.value("max_concurrent_file_streams", config.grpc.maxConcurrentFileStreams);
  434. }
  435. }
  436. return config;
  437. }
  438. void DatabaseService::setupComponents() {
  439. // Create memory store with eviction config
  440. MemoryStore::Config storeConfig;
  441. storeConfig.nodeId = config_.nodeId;
  442. storeConfig.maxMemoryBytes = config_.maxMemoryMb * 1024ULL * 1024ULL;
  443. storeConfig.evictionThresholdPercent = config_.evictionThresholdPercent;
  444. storeConfig.evictionTargetPercent = config_.evictionTargetPercent;
  445. storeConfig.evictionCheckIntervalMs = config_.evictionCheckIntervalMs;
  446. // NEW v1.7.0 eviction tuning (wired in T2; consumed in T3/T4).
  447. storeConfig.evictionChunkSize = config_.evictionChunkSize;
  448. storeConfig.evictionChunkPauseMs = config_.evictionChunkPauseMs;
  449. storeConfig.maxEvictionPassesPerTrigger = config_.maxEvictionPassesPerTrigger;
  450. storeConfig.hotWriteFloorMs = config_.hotWriteFloorMs;
  451. storeConfig.memorySoftPercent = config_.memorySoftPercent;
  452. storeConfig.memoryHardPercent = config_.memoryHardPercent;
  453. storeConfig.memoryEmergencyPercent = config_.memoryEmergencyPercent;
  454. storeConfig.evictionBurstThreshold = config_.evictionBurstThreshold;
  455. store_ = std::make_unique<MemoryStore>(storeConfig);
  456. // Create view manager (cache loaded in initialize() after persistence recovery)
  457. view_manager_ = std::make_unique<ViewManager>(*store_);
  458. // Create per-collection config manager and attach it to the store so the
  459. // write paths route document timestamp stamps through it.
  460. config_manager_ = std::make_unique<CollectionConfigManager>(*store_);
  461. store_->setConfigManager(config_manager_.get());
  462. // v1.9.0 — disk-resident version history. Replaces the in-heap
  463. // `CollectionData::versionHistory` deques that were the dominant
  464. // source of the Zoe untracked-RSS gap. One file per collection at
  465. // `<dataDir>/history/<collection>.hlog`. Lifecycle: constructed
  466. // here, wired into the store immediately so snapshot deserializer
  467. // can migrate pre-v1.9 in-memory history blocks straight to disk.
  468. HistoryStore::Config histConfig;
  469. histConfig.dataDir = config_.dataDirectory;
  470. history_store_ = std::make_unique<HistoryStore>(histConfig);
  471. store_->setHistoryStore(history_store_.get());
  472. // Create persistence manager. Start from any persistenceConfig values the
  473. // caller (config loader / CLI parser) has already populated — including
  474. // recoveryMode — and overlay the top-level convenience fields.
  475. PersistenceManager::Config persistConfig = config_.persistenceConfig;
  476. persistConfig.dataDir = config_.dataDirectory;
  477. persistConfig.walSyncIntervalMs = config_.walSyncIntervalMs;
  478. persistConfig.snapshotIntervalSec = config_.snapshotIntervalSec;
  479. persistConfig.compressionEnabled = config_.compressionEnabled;
  480. persistConfig.nodeId = config_.nodeId;
  481. // Mirror the resolved recoveryMode back into our Config so downstream code
  482. // (logging, RPC handlers) can inspect config_.persistenceConfig.recoveryMode
  483. // without having to reach into the PersistenceManager.
  484. config_.persistenceConfig = persistConfig;
  485. persistence_ = std::make_unique<PersistenceManager>(persistConfig);
  486. // Connect store callbacks to persistence - uses setPersistCallback for WAL logging
  487. store_->setPersistCallback([this](const std::string& collection, const std::string& id,
  488. const std::optional<Document>& doc, EventType eventType) {
  489. // Log to WAL based on event type. v1.7.4: capture the assigned WAL
  490. // sequence and record it on the store so eviction stubs can carry
  491. // a real seq (instead of doc.version, which was useless as a
  492. // `fromSequence` hint and forced a full WAL scan on every fault).
  493. uint64_t walSeq = 0;
  494. switch (eventType) {
  495. case EventType::INSERT:
  496. if (doc) walSeq = persistence_->logInsert(collection, *doc);
  497. break;
  498. case EventType::UPDATE:
  499. if (doc) walSeq = persistence_->logUpdate(collection, *doc);
  500. break;
  501. case EventType::DELETE:
  502. persistence_->logDelete(collection, id);
  503. break;
  504. default:
  505. break;
  506. }
  507. if (walSeq > 0 && (eventType == EventType::INSERT || eventType == EventType::UPDATE)) {
  508. store_->recordWalSequence(collection, id, walSeq);
  509. }
  510. // Queue for replication broadcast
  511. if (replication_ && config_.replicationEnabled) {
  512. databasepb::ReplicationEntry entry;
  513. entry.set_collection(collection);
  514. entry.set_document_id(id);
  515. // v1.8.0 — stamp our nodeId so peers can attribute the write to
  516. // its actual origin. Pre-v1.8 broadcasts left this empty, which
  517. // is now treated by receivers as "legacy / unknown origin".
  518. entry.set_node_id(config_.nodeId);
  519. entry.set_global_timestamp(
  520. std::chrono::duration_cast<std::chrono::milliseconds>(
  521. std::chrono::system_clock::now().time_since_epoch()
  522. ).count());
  523. switch (eventType) {
  524. case EventType::INSERT:
  525. entry.set_op(databasepb::OP_INSERT);
  526. // Send the full Document envelope (id, collection, data,
  527. // version, timestamps, encryption state). The follower's
  528. // applyReplicatedEntry uses Document::fromJson() which
  529. // expects this envelope — sending only doc->data silently
  530. // dropped every user field on the other side.
  531. if (doc) entry.set_data(doc->toJson().dump());
  532. break;
  533. case EventType::UPDATE:
  534. entry.set_op(databasepb::OP_UPDATE);
  535. if (doc) entry.set_data(doc->toJson().dump());
  536. break;
  537. case EventType::DELETE:
  538. entry.set_op(databasepb::OP_DELETE);
  539. break;
  540. default:
  541. break;
  542. }
  543. replication_->queueForReplication(entry);
  544. }
  545. // Publish event
  546. if (events_) {
  547. DatabaseEvent event;
  548. event.type = eventType;
  549. event.collection = collection;
  550. event.documentId = id;
  551. event.timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
  552. std::chrono::system_clock::now().time_since_epoch()
  553. ).count();
  554. event.nodeId = config_.nodeId;
  555. if (doc) {
  556. event.data = doc->data;
  557. }
  558. events_->publish(event);
  559. }
  560. });
  561. // Set up document load callback for LRU eviction recovery.
  562. //
  563. // v1.7.4: walSequence is now a real WAL sequence (set by the persist
  564. // callback below at append time, not the per-doc version counter).
  565. // Pass `walSequence - 1` as the exclusive `fromSequence` so replay
  566. // returns entries with seq >= walSequence — the doc's last write is
  567. // exactly at walSequence, so the very first hit is the one we want.
  568. // For docs whose seq is unknown (0 — e.g. loaded from a snapshot
  569. // that predates v1.7.4), fall back to the full-WAL scan from 0.
  570. store_->setDocumentLoadCallback([this](const std::string& collection,
  571. const std::string& id,
  572. uint64_t walSequence) -> std::optional<Document> {
  573. uint64_t fromSequence = walSequence > 0 ? walSequence - 1 : 0;
  574. return persistence_->loadDocument(collection, id, fromSequence);
  575. });
  576. // Create encryption manager
  577. EncryptionManager::Config encryptConfig;
  578. encryptConfig.enabled = config_.encryptionEnabled;
  579. encryptConfig.keyFilePath = config_.keyFilePath;
  580. encryptConfig.autoGenerateKey = config_.autoGenerateKey;
  581. encryption_ = std::make_unique<EncryptionManager>(encryptConfig);
  582. // Create event manager
  583. EventManager::Config eventConfig;
  584. eventConfig.nodeId = config_.nodeId;
  585. events_ = std::make_unique<EventManager>(eventConfig);
  586. // Create file manager
  587. FileManager::Config fileConfig;
  588. fileConfig.filesDir = config_.dataDirectory / "files";
  589. fileConfig.maxFileSizeMb = config_.maxFileSizeMb;
  590. fileConfig.allowedMimeTypes = config_.allowedFileTypes;
  591. fileConfig.cleanupIntervalSec = config_.fileCleanupIntervalSec;
  592. files_ = std::make_unique<FileManager>(fileConfig);
  593. // Create replication manager
  594. ReplicationManager::Config replConfig;
  595. replConfig.nodeId = config_.nodeId;
  596. replConfig.peerAddresses = config_.peerAddresses;
  597. replConfig.conflictResolution = config_.conflictResolution;
  598. replication_ = std::make_unique<ReplicationManager>(replConfig);
  599. // Wire replication entry apply callback
  600. replication_->setEntryApplyCallback([this](const databasepb::ReplicationEntry& entry) {
  601. applyReplicatedEntry(entry);
  602. });
  603. // Create gRPC implementations
  604. storageImpl_ = std::make_unique<DatabaseGrpcImpl>(
  605. *this, *store_, *persistence_, *events_, *files_, *encryption_, *view_manager_, *config_manager_
  606. );
  607. // v1.6.2 — wire the streaming-RPC concurrency limits from GrpcConfig.
  608. storageImpl_->setStreamLimits(
  609. config_.grpc.maxConcurrentSubscribeStreams,
  610. config_.grpc.maxConcurrentFileStreams);
  611. replicationImpl_ = std::make_unique<DatabaseReplicationGrpcImpl>(
  612. *store_, *replication_, *persistence_
  613. );
  614. }
  615. void DatabaseService::applyReplicatedEntry(const databasepb::ReplicationEntry& entry) {
  616. try {
  617. // v1.8.0 — also append the replicated entry to the local WAL, tagged
  618. // with the originating node's id. This is what makes follower-side
  619. // recovery (eviction → WAL fallback, restart → WAL replay) preserve
  620. // replicated documents. Echo amplification is prevented by:
  621. // - GetEntriesSince emitting walEntry.nodeId (not local node id)
  622. // - SyncProtocol skipping entries whose origin is the peer itself
  623. // both implemented in the same v1.8.0 cycle.
  624. const std::string origin = entry.node_id().empty()
  625. ? config_.nodeId // legacy peer (pre-1.8) — best-guess local
  626. : entry.node_id();
  627. switch (entry.op()) {
  628. case databasepb::OP_INSERT:
  629. case databasepb::OP_UPDATE:
  630. case databasepb::OP_UPSERT: {
  631. if (entry.data().empty()) {
  632. spdlog::warn("Replication entry has no data for op {}", static_cast<int>(entry.op()));
  633. return;
  634. }
  635. auto json = smartbotic::db::parse_to_nlohmann(entry.data());
  636. Document doc = Document::fromJson(json);
  637. doc.id = entry.document_id();
  638. doc.nodeId = entry.node_id();
  639. // loadDocument bypasses the persist-and-broadcast callback to avoid
  640. // re-replicating; we explicitly write to the WAL right after with
  641. // the origin-aware overload so eviction/WAL recovery still works.
  642. store_->loadDocument(entry.collection(), doc);
  643. if (persistence_) {
  644. uint64_t walSeq = (entry.op() == databasepb::OP_INSERT)
  645. ? persistence_->logInsert(entry.collection(), doc, origin)
  646. : persistence_->logUpdate(entry.collection(), doc, origin);
  647. if (walSeq > 0) {
  648. store_->recordWalSequence(entry.collection(), doc.id, walSeq);
  649. }
  650. }
  651. spdlog::trace("Applied replicated {} to {}/{} from {}",
  652. entry.op() == databasepb::OP_INSERT ? "insert" :
  653. entry.op() == databasepb::OP_UPDATE ? "update" : "upsert",
  654. entry.collection(), entry.document_id(), entry.node_id());
  655. break;
  656. }
  657. case databasepb::OP_DELETE: {
  658. store_->remove(entry.collection(), entry.document_id());
  659. if (persistence_) {
  660. persistence_->logDelete(entry.collection(), entry.document_id(), origin);
  661. }
  662. spdlog::trace("Applied replicated delete to {}/{} from {}",
  663. entry.collection(), entry.document_id(), entry.node_id());
  664. break;
  665. }
  666. case databasepb::OP_CREATE_COLLECTION: {
  667. CollectionOptions options;
  668. store_->createCollection(entry.collection(), options);
  669. if (persistence_) {
  670. persistence_->logCreateCollection(entry.collection(), options, origin);
  671. }
  672. spdlog::trace("Applied replicated create collection {} from {}",
  673. entry.collection(), entry.node_id());
  674. break;
  675. }
  676. case databasepb::OP_DROP_COLLECTION: {
  677. store_->dropCollection(entry.collection());
  678. if (persistence_) {
  679. persistence_->logDropCollection(entry.collection(), origin);
  680. }
  681. spdlog::trace("Applied replicated drop collection {} from {}",
  682. entry.collection(), entry.node_id());
  683. break;
  684. }
  685. default:
  686. spdlog::warn("Unknown replication operation type: {}", static_cast<int>(entry.op()));
  687. break;
  688. }
  689. } catch (const std::exception& e) {
  690. spdlog::error("Failed to apply replicated entry: {}", e.what());
  691. }
  692. }
  693. void DatabaseService::startGrpcServer() {
  694. std::string serverAddress = config_.bindAddress + ":" + std::to_string(config_.rpcPort);
  695. grpc::ServerBuilder builder;
  696. builder.AddListeningPort(serverAddress, grpc::InsecureServerCredentials());
  697. builder.RegisterService(storageImpl_.get());
  698. builder.RegisterService(replicationImpl_.get());
  699. // Configurable message sizes (default 100 MB for file uploads)
  700. const int64_t mb = 1024 * 1024;
  701. builder.SetMaxReceiveMessageSize(
  702. static_cast<int>(config_.grpc.maxReceiveMessageSizeMb * mb));
  703. builder.SetMaxSendMessageSize(
  704. static_cast<int>(config_.grpc.maxSendMessageSizeMb * mb));
  705. // ResourceQuota bounds total inbound buffer memory across all RPCs
  706. grpc::ResourceQuota quota("smartbotic-db");
  707. quota.Resize(config_.grpc.resourceQuotaMemoryMb * mb);
  708. builder.SetResourceQuota(quota);
  709. spdlog::info("gRPC config: recv={}MB send={}MB quota={}MB subs={} files={}",
  710. config_.grpc.maxReceiveMessageSizeMb,
  711. config_.grpc.maxSendMessageSizeMb,
  712. config_.grpc.resourceQuotaMemoryMb,
  713. config_.grpc.maxConcurrentSubscribeStreams,
  714. config_.grpc.maxConcurrentFileStreams);
  715. grpcServer_ = builder.BuildAndStart();
  716. if (!grpcServer_) {
  717. throw std::runtime_error("Failed to start gRPC server on " + serverAddress);
  718. }
  719. spdlog::info("gRPC server listening on {}", serverAddress);
  720. }
  721. void DatabaseService::stopGrpcServer() {
  722. if (grpcServer_) {
  723. grpcServer_->Shutdown();
  724. grpcServer_.reset();
  725. }
  726. }
  727. } // namespace smartbotic::database