database_service.cpp 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148
  1. #include "database_service.hpp"
  2. #include "json_parse.hpp"
  3. #include "project_addressing.hpp"
  4. #include "storage/document_store.hpp"
  5. #include "storage/document_store_lmdb.hpp"
  6. #include "storage/dual_write_mirror.hpp"
  7. #include "storage/lmdb_env.hpp"
  8. #include "storage/migrate_v1_to_v2.hpp"
  9. #include "storage/project_store.hpp"
  10. #include <grpcpp/grpcpp.h>
  11. #include <grpcpp/resource_quota.h>
  12. #include <spdlog/spdlog.h>
  13. #include <fstream>
  14. #ifdef HAVE_SYSTEMD
  15. #include <systemd/sd-daemon.h>
  16. #endif
  17. #if defined(__GLIBC__) && !defined(__APPLE__)
  18. #include <malloc.h>
  19. #endif
  20. namespace smartbotic::database {
  21. DatabaseService::DatabaseService(Config config)
  22. : config_(std::move(config))
  23. {
  24. }
  25. DatabaseService::~DatabaseService() {
  26. stop();
  27. }
  28. std::string DatabaseService::readOnlyReason() const {
  29. std::lock_guard<std::mutex> lock(reason_mutex_);
  30. return read_only_reason_;
  31. }
  32. void DatabaseService::setReadOnly(bool value, const std::string& reason) {
  33. {
  34. std::lock_guard<std::mutex> lock(reason_mutex_);
  35. read_only_reason_ = value ? reason : "";
  36. }
  37. bool prev = read_only_.exchange(value, std::memory_order_acq_rel);
  38. if (prev != value) {
  39. if (value) {
  40. spdlog::error("Database entered READ-ONLY mode: {}", reason);
  41. } else {
  42. spdlog::info("Database unlocked -- writes accepted");
  43. }
  44. }
  45. }
  46. bool DatabaseService::initialize() {
  47. spdlog::info("Initializing database service (node: {})", config_.nodeId);
  48. try {
  49. // Create data directory if needed
  50. std::error_code ec;
  51. std::filesystem::create_directories(config_.dataDirectory, ec);
  52. if (ec) {
  53. spdlog::error("Failed to create data directory: {}", ec.message());
  54. return false;
  55. }
  56. setupComponents();
  57. // Initialize encryption
  58. if (config_.encryptionEnabled) {
  59. if (!encryption_->initialize()) {
  60. spdlog::error("Failed to initialize encryption");
  61. return false;
  62. }
  63. }
  64. // Recover from persistence
  65. recovery_outcome_ = persistence_->recover(*store_);
  66. #if defined(__GLIBC__) && !defined(__APPLE__)
  67. // v1.9.3 — release freelist pages accumulated during recovery. v1.9.1
  68. // added trim at end of loadSnapshot, but the WAL replay phase that
  69. // runs afterward (`persistence_->recover`'s `replayWal` step) also
  70. // burns through GBs of small Document JSON allocations whose pages
  71. // sit on the per-thread freelist with no subsequent allocation to
  72. // shake them loose. On Zoe (BUG-memory-leak-zoe.md update 15:05)
  73. // this was 5.7 GB at 23 min uptime — fully reclaimable via
  74. // malloc_trim, just nothing called it. Paired with the periodic
  75. // every-5-minute trim in MemoryStore::logMemoryCheck, so any
  76. // post-boot bloat that escapes this trim gets cleaned up shortly.
  77. ::malloc_trim(0);
  78. #endif
  79. if (recovery_outcome_.isFailure()) {
  80. const std::string modeStr =
  81. recoveryModeToString(config_.persistenceConfig.recoveryMode);
  82. spdlog::error("");
  83. spdlog::error("+------------------------------------------------------------------+");
  84. spdlog::error("| RECOVERY REFUSED |");
  85. spdlog::error("| |");
  86. spdlog::error("| Mode: {}", modeStr);
  87. spdlog::error("| Expected snapshot: {}", recovery_outcome_.expectedSnapshot.string());
  88. spdlog::error("| Reason: {}", recovery_outcome_.failureReason);
  89. spdlog::error("| |");
  90. spdlog::error("| Snapshots available: {}", recovery_outcome_.snapshotsAvailable);
  91. spdlog::error("| Snapshots attempted: {}", recovery_outcome_.snapshotsAttempted);
  92. spdlog::error("| |");
  93. spdlog::error("| To escalate, restart with one of: |");
  94. spdlog::error("| --recovery-mode=snapshot_fallback Try older snapshots |");
  95. spdlog::error("| --recovery-mode=wal_only Replay WAL only (slow) |");
  96. spdlog::error("| --recovery-mode=best_effort Try all of the above |");
  97. spdlog::error("| --recovery-mode=force_empty Start empty (LAST RESORT) |");
  98. spdlog::error("| |");
  99. spdlog::error("| Or set in config.json: \"recovery\": {{ \"mode\": \"<mode>\" }} |");
  100. spdlog::error("| |");
  101. spdlog::error("| PRESERVE /var/lib/smartbotic-database/ BEFORE ESCALATING. |");
  102. spdlog::error("+------------------------------------------------------------------+");
  103. throw std::runtime_error("recovery failed; refusing to start");
  104. }
  105. // Auto-readonly mode on non-trivial recovery
  106. if (recovery_outcome_.isNonTrivial() && !force_readwrite_) {
  107. std::string reason;
  108. switch (recovery_outcome_.kind) {
  109. case RecoveryOutcome::Kind::SnapshotFellBack:
  110. reason = "fell back to snapshot " +
  111. recovery_outcome_.snapshotUsed.filename().string() +
  112. " because " +
  113. recovery_outcome_.expectedSnapshot.filename().string() +
  114. " failed: " + recovery_outcome_.failureReason;
  115. break;
  116. case RecoveryOutcome::Kind::WalOnlyReplay:
  117. reason = "WAL-only replay, no snapshot loaded (" +
  118. std::to_string(recovery_outcome_.walEntriesReplayed) +
  119. " entries)";
  120. break;
  121. case RecoveryOutcome::Kind::ForcedEmpty:
  122. reason = "forced empty by operator (--recovery-mode=force_empty)";
  123. break;
  124. default:
  125. reason = "non-trivial recovery";
  126. break;
  127. }
  128. reason += ". Run `smartbotic-db-cli unlock` to accept this state, "
  129. "or restart with --force-readwrite to bypass this check.";
  130. setReadOnly(true, reason);
  131. spdlog::error("");
  132. spdlog::error("+------------------------------------------------------------------+");
  133. spdlog::error("| [ERROR] Database booted in READ-ONLY mode after non-trivial |");
  134. spdlog::error("| recovery. |");
  135. spdlog::error("| |");
  136. spdlog::error("| Reason: {}", reason);
  137. spdlog::error("| |");
  138. spdlog::error("| Writes will be REJECTED until you acknowledge this state: |");
  139. spdlog::error("| smartbotic-db-cli unlock # live, no restart |");
  140. spdlog::error("| smartbotic-database --force-readwrite # on next restart |");
  141. spdlog::error("+------------------------------------------------------------------+");
  142. }
  143. // Set replication sequence after recovery (WAL sequence is now known)
  144. replication_->setSequence(persistence_->currentWalSequence());
  145. // v1.8.0 — start the persistence manager before loadFromStore /
  146. // migrations so any system-collection writes those phases produce
  147. // (view docs, _collection_meta entries, _migrations recordings)
  148. // reach the WAL like normal mutations. Pre-v1.8, persistence_ was
  149. // started later in start(), which silently dropped those writes
  150. // (running_=false → logInsert no-op). That meant migrated views
  151. // only "survived" because the runner re-created them every boot,
  152. // and a manually-created view that landed during the racy startup
  153. // window (before full readiness) could be lost.
  154. if (!persistence_->start()) {
  155. spdlog::error("Failed to start persistence manager before migrations");
  156. return false;
  157. }
  158. // Load view definitions from the _views system collection (which is now
  159. // populated by the persistence recovery above).
  160. view_manager_->loadFromStore();
  161. // Load per-collection configs from the _collection_meta system collection.
  162. // Must happen AFTER persistence recovery and BEFORE migrations so any
  163. // migration-created documents are stamped with the correct precision.
  164. config_manager_->loadFromStore();
  165. // Run migrations if enabled
  166. if (config_.migrations.enabled && !config_.migrations.directory.empty()) {
  167. if (!runMigrations()) {
  168. if (config_.migrations.failOnError) {
  169. spdlog::error("Failed to run migrations");
  170. return false;
  171. }
  172. spdlog::warn("Some migrations failed, continuing anyway");
  173. }
  174. }
  175. // v2.0 Stage 4 — synchronous backfill into doc_store_ before
  176. // serving. Without this, only post-boot writes land in LMDB and
  177. // the read-flip downstream would see an empty mirror.
  178. backfillIntoDocStore();
  179. spdlog::info("Database service initialized successfully");
  180. return true;
  181. } catch (const std::exception& e) {
  182. spdlog::error("Failed to initialize database service: {}", e.what());
  183. return false;
  184. }
  185. }
  186. void DatabaseService::migrateLegacyEnvToDefaultProject() {
  187. const auto legacy = config_.dataDirectory / "env";
  188. const auto target = config_.dataDirectory / "projects" /
  189. smartbotic::database::kDefaultProject / "env";
  190. std::error_code ec;
  191. const bool legacy_exists = std::filesystem::exists(legacy, ec);
  192. const bool target_exists = std::filesystem::exists(target, ec);
  193. if (!legacy_exists) return; // fresh install or already migrated
  194. if (target_exists) {
  195. spdlog::error("v2.3 storage: both legacy '{}' and new '{}' exist — "
  196. "refusing to start. Inspect manually; the safe move is "
  197. "to either delete the legacy dir (if you confirm it's a "
  198. "leftover) or stop and contact ops.",
  199. legacy.string(), target.string());
  200. throw std::runtime_error("v2.3 storage migration: ambiguous layout");
  201. }
  202. std::filesystem::create_directories(target.parent_path(), ec);
  203. if (ec) {
  204. throw std::runtime_error("v2.3 storage migration: cannot create '"
  205. + target.parent_path().string() + "': " + ec.message());
  206. }
  207. std::filesystem::rename(legacy, target, ec);
  208. if (ec) {
  209. throw std::runtime_error("v2.3 storage migration: rename '"
  210. + legacy.string() + "' -> '"
  211. + target.string() + "' failed: " + ec.message());
  212. }
  213. spdlog::info("v2.3 storage: migrated legacy env '{}' -> '{}' (default project)",
  214. legacy.string(), target.string());
  215. }
  216. smartbotic::db::storage::DocumentStore* DatabaseService::docStore() noexcept {
  217. return docStore(smartbotic::database::kDefaultProject);
  218. }
  219. smartbotic::db::storage::DocumentStore*
  220. DatabaseService::docStore(std::string_view project) noexcept {
  221. if (!projects_) return nullptr;
  222. return projects_->get(project);
  223. }
  224. std::vector<std::string> DatabaseService::listProjects() const {
  225. if (!projects_) return {};
  226. return projects_->listOnDisk();
  227. }
  228. bool DatabaseService::createProject(const std::string& name, std::string& error) {
  229. if (!projects_) {
  230. error = "project registry not initialized";
  231. return false;
  232. }
  233. return projects_->create(name, error);
  234. }
  235. bool DatabaseService::dropProject(const std::string& name, std::string& error) {
  236. if (!projects_) {
  237. error = "project registry not initialized";
  238. return false;
  239. }
  240. return projects_->drop(name, error);
  241. }
  242. bool DatabaseService::runMigrations() {
  243. if (!config_.migrations.enabled) {
  244. return true;
  245. }
  246. MigrationRunner::Config migrationConfig;
  247. migrationConfig.directory = config_.migrations.directory;
  248. migrationConfig.autoApply = config_.migrations.autoApply;
  249. migrationConfig.failOnError = config_.migrations.failOnError;
  250. migrationRunner_ = std::make_unique<MigrationRunner>(*store_, *view_manager_, migrationConfig);
  251. return migrationRunner_->runMigrations();
  252. }
  253. bool DatabaseService::backfillIntoDocStore() {
  254. if (!projects_) {
  255. // Registry open failed earlier; nothing to back-fill into.
  256. return true;
  257. }
  258. // v2.3 — MemoryStore stores collection keys in qualified form
  259. // ("project:collection"). For the default project (back-compat
  260. // path) the marker check operates on the default env. Skip when
  261. // already migrated.
  262. auto handle = projects_->getHandle(smartbotic::database::kDefaultProject);
  263. if (handle.env) {
  264. try {
  265. if (smartbotic::db::storage::migration_complete(*handle.env)) {
  266. spdlog::info("v2.3 backfill: default project already migrated, skipping");
  267. return true;
  268. }
  269. } catch (const std::exception& e) {
  270. spdlog::warn("v2.3 backfill: migration_complete probe failed: {} — "
  271. "treating default env as fresh and proceeding",
  272. e.what());
  273. }
  274. }
  275. const auto collections = store_->listCollections();
  276. uint64_t total_docs = 0;
  277. uint64_t total_failures = 0;
  278. auto t0 = std::chrono::steady_clock::now();
  279. for (const auto& qualified : collections) {
  280. if (qualified.empty() || qualified[0] == '_') continue;
  281. // Each qualified key parses to (project, collection); route the
  282. // mirror write to the right project's env.
  283. smartbotic::database::ProjectCollection pc;
  284. try {
  285. pc = smartbotic::database::parseProjectCollection(qualified);
  286. } catch (const std::exception&) {
  287. // Legacy unqualified key — treat as default project.
  288. pc = {std::string(smartbotic::database::kDefaultProject), qualified};
  289. }
  290. auto* ds = projects_->getOrCreate(pc.project);
  291. if (!ds) {
  292. ++total_failures;
  293. continue;
  294. }
  295. const auto docs = store_->getAllDocuments(qualified);
  296. for (const auto& doc : docs) {
  297. std::optional<Document> opt_doc(doc);
  298. const uint64_t drift_before = mirror_drift_count_.load(std::memory_order_relaxed);
  299. smartbotic::db::storage::applyDualWriteMirror(
  300. ds, mirror_healthy_, mirror_drift_count_,
  301. pc.collection, doc.id, opt_doc, EventType::INSERT);
  302. if (mirror_drift_count_.load(std::memory_order_relaxed) > drift_before) {
  303. ++total_failures;
  304. }
  305. ++total_docs;
  306. if (total_docs % 10000 == 0) {
  307. sd_notify(0, "EXTEND_TIMEOUT_USEC=600000000");
  308. const std::string status = "STATUS=v2.3 backfill: " +
  309. std::to_string(total_docs) + " docs mirrored";
  310. sd_notify(0, status.c_str());
  311. spdlog::info("v2.3 backfill: {} docs mirrored ({} failures so far)",
  312. total_docs, total_failures);
  313. }
  314. }
  315. }
  316. const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
  317. std::chrono::steady_clock::now() - t0).count();
  318. if (total_failures == 0) {
  319. // Stamp schema_version=2 on every opened project env. Subsequent
  320. // boots short-circuit at migration_complete().
  321. for (const auto& name : projects_->listOpen()) {
  322. auto h = projects_->getHandle(name);
  323. if (!h.env) continue;
  324. try {
  325. smartbotic::db::storage::mark_migration_complete(*h.env);
  326. } catch (const std::exception& e) {
  327. spdlog::warn("v2.3 backfill: schema_version marker write failed for "
  328. "project '{}': {}", name, e.what());
  329. }
  330. }
  331. spdlog::info("v2.3 backfill: complete — {} docs across {} collections in {} ms",
  332. total_docs, collections.size(), elapsed);
  333. } else {
  334. spdlog::error("v2.3 backfill: completed with {} failures out of {} docs "
  335. "in {} ms — mirror_healthy_=false, reads must NOT flip to LMDB",
  336. total_failures, total_docs, elapsed);
  337. }
  338. return true;
  339. }
  340. void DatabaseService::start() {
  341. if (running_.exchange(true)) {
  342. return;
  343. }
  344. spdlog::info("Starting database service on {}:{}", config_.bindAddress, config_.rpcPort);
  345. // Start components
  346. store_->start();
  347. persistence_->start();
  348. events_->start();
  349. files_->start();
  350. if (config_.replicationEnabled) {
  351. replication_->start();
  352. // Set initial local collections for discovery
  353. replication_->setLocalCollections(store_->listCollections());
  354. }
  355. // Start gRPC server
  356. startGrpcServer();
  357. spdlog::info("Database service started");
  358. }
  359. void DatabaseService::stop() {
  360. if (!running_.exchange(false)) {
  361. return;
  362. }
  363. spdlog::info("Stopping database service...");
  364. // Stop gRPC server first
  365. stopGrpcServer();
  366. // Stop components in reverse order
  367. if (replication_) {
  368. replication_->stop();
  369. }
  370. if (files_) {
  371. files_->stop();
  372. }
  373. if (events_) {
  374. events_->stop();
  375. }
  376. if (persistence_ && store_) {
  377. // v1.8.0 — take a final snapshot on graceful shutdown so the next
  378. // boot's recovery is "trivial" (snapshot-loaded) instead of "WAL-only
  379. // replay" which trips auto-readonly mode. This makes ordinary
  380. // restarts pass through cleanly without needing
  381. // `smartbotic-db-cli unlock`.
  382. //
  383. // v1.8.2 — extend systemd's stop watchdog before doing it. On Zoe
  384. // (1.85M docs / ~5 GB tracked) the serialize takes ~70 s and the
  385. // default TimeoutStopSec=30s SIGKILLed the process mid-write, peak
  386. // RSS hit 11 GB (live state + uncompressed serialize buffer), and
  387. // the new snapshot never made it to disk anyway. EXTEND_TIMEOUT_USEC
  388. // tells systemd we're working — same protocol the recovery path
  389. // uses during startup. Paired with TimeoutStopSec=300 in the unit
  390. // file as the static cap.
  391. #ifdef HAVE_SYSTEMD
  392. sd_notify(0, "EXTEND_TIMEOUT_USEC=600000000");
  393. sd_notify(0, "STATUS=Writing final snapshot");
  394. #endif
  395. try {
  396. persistence_->forceSnapshot(*store_);
  397. spdlog::info("Final snapshot taken on shutdown");
  398. } catch (const std::exception& e) {
  399. spdlog::warn("Final snapshot on shutdown failed: {} — recovery on next "
  400. "boot may be WAL-only and trip auto-readonly mode", e.what());
  401. }
  402. }
  403. if (persistence_) {
  404. persistence_->stop();
  405. }
  406. if (store_) {
  407. store_->stop();
  408. }
  409. spdlog::info("Database service stopped");
  410. }
  411. void DatabaseService::wait() {
  412. if (serverThread_.joinable()) {
  413. serverThread_.join();
  414. }
  415. }
  416. void DatabaseService::signalStop() {
  417. stopRequested_ = true;
  418. stop();
  419. }
  420. nlohmann::json DatabaseService::getStats() const {
  421. nlohmann::json stats;
  422. if (store_) {
  423. auto storeStats = store_->getStats();
  424. stats["documents"] = storeStats.totalDocuments;
  425. stats["collections"] = storeStats.totalCollections;
  426. stats["memory_bytes"] = storeStats.estimatedMemoryBytes;
  427. stats["inserts"] = storeStats.insertCount;
  428. stats["updates"] = storeStats.updateCount;
  429. stats["deletes"] = storeStats.deleteCount;
  430. stats["queries"] = storeStats.queryCount;
  431. }
  432. if (persistence_) {
  433. auto persistStats = persistence_->getStats();
  434. stats["wal_sequence"] = persistStats.walSequence;
  435. stats["wal_size_bytes"] = persistStats.walSizeBytes;
  436. stats["snapshot_count"] = persistStats.snapshotCount;
  437. stats["last_snapshot_sequence"] = persistStats.lastSnapshotSequence;
  438. }
  439. if (events_) {
  440. stats["subscriptions"] = events_->subscriptionCount();
  441. }
  442. return stats;
  443. }
  444. DatabaseService::Config DatabaseService::loadConfig(const std::filesystem::path& configPath) {
  445. std::ifstream file(configPath);
  446. if (!file) {
  447. throw std::runtime_error("Failed to open config file: " + configPath.string());
  448. }
  449. nlohmann::json json;
  450. file >> json;
  451. return parseConfig(json);
  452. }
  453. DatabaseService::Config DatabaseService::parseConfig(const nlohmann::json& json) {
  454. Config config;
  455. // Check for both "storage" and "database" keys for backward compatibility
  456. const nlohmann::json* dbConfig = nullptr;
  457. if (json.contains("database")) {
  458. dbConfig = &json["database"];
  459. } else if (json.contains("storage")) {
  460. dbConfig = &json["storage"];
  461. }
  462. if (dbConfig) {
  463. const auto& db = *dbConfig;
  464. config.nodeId = db.value("node_id", config.nodeId);
  465. config.bindAddress = db.value("bind_address", config.bindAddress);
  466. config.rpcPort = db.value("rpc_port", config.rpcPort);
  467. // v2.4 — parse the per-listener fleet. If "listeners" is set,
  468. // it overrides the legacy bind_address+rpc_port. Otherwise we
  469. // synthesise one listener below using those legacy fields.
  470. if (db.contains("listeners") && db["listeners"].is_array()) {
  471. for (const auto& l : db["listeners"]) {
  472. ListenerConfig lc;
  473. lc.bind = l.value("bind", lc.bind);
  474. lc.port = l.value("port", lc.port);
  475. if (l.contains("tls") && l["tls"].is_object()) {
  476. const auto& t = l["tls"];
  477. lc.tls.enabled = t.value("enabled", false);
  478. lc.tls.cert_path = t.value("cert_path", std::string{});
  479. lc.tls.key_path = t.value("key_path", std::string{});
  480. lc.tls.auto_self_signed_if_missing =
  481. t.value("auto_self_signed_if_missing", true);
  482. }
  483. if (l.contains("auth") && l["auth"].is_object()) {
  484. const auto& a = l["auth"];
  485. lc.auth.required = a.value("required", false);
  486. if (a.contains("keys") && a["keys"].is_array()) {
  487. for (const auto& k : a["keys"]) {
  488. if (k.is_string()) lc.auth.keys.push_back(k.get<std::string>());
  489. }
  490. }
  491. }
  492. config.listeners.push_back(std::move(lc));
  493. }
  494. }
  495. // Expand environment variables in data directory
  496. std::string dataDir = db.value("data_directory", "");
  497. if (dataDir.find("${HOME}") != std::string::npos) {
  498. const char* home = std::getenv("HOME");
  499. if (home) {
  500. size_t pos = dataDir.find("${HOME}");
  501. dataDir.replace(pos, 7, home);
  502. }
  503. }
  504. config.dataDirectory = dataDir;
  505. // Migrations settings
  506. if (db.contains("migrations")) {
  507. auto& migrations = db["migrations"];
  508. config.migrations.enabled = migrations.value("enabled", config.migrations.enabled);
  509. config.migrations.autoApply = migrations.value("auto_apply", config.migrations.autoApply);
  510. config.migrations.failOnError = migrations.value("fail_on_error", config.migrations.failOnError);
  511. std::string migDir = migrations.value("directory", "");
  512. if (migDir.find("${HOME}") != std::string::npos) {
  513. const char* home = std::getenv("HOME");
  514. if (home) {
  515. size_t pos = migDir.find("${HOME}");
  516. migDir.replace(pos, 7, home);
  517. }
  518. }
  519. config.migrations.directory = migDir;
  520. }
  521. // Memory eviction settings
  522. if (db.contains("memory")) {
  523. auto& memory = db["memory"];
  524. // v2.0 Stage 8 — deprecation log. These knobs control MemoryStore
  525. // eviction, which is still active for the MemoryStore-side mirror
  526. // but does NOT bound RSS in v2.0 (LMDB mmap is the dominant RSS
  527. // contributor). The substrate-level equivalent is the LMDB env
  528. // mapsize and OS page cache. v2.1 will rename `max_memory_mb` to
  529. // `buffer_pool_size_mb` per the Phase C plan.
  530. for (const char* deprecated : {"max_memory_mb",
  531. "eviction_threshold_percent",
  532. "eviction_target_percent",
  533. "eviction_check_interval_ms",
  534. "eviction_chunk_size",
  535. "eviction_chunk_pause_ms",
  536. "max_eviction_passes_per_trigger",
  537. "hot_write_floor_ms",
  538. "memory_priority"}) {
  539. if (memory.contains(deprecated)) {
  540. spdlog::warn("v2.0 deprecation: storage.memory.{} is deprecated "
  541. "and will be removed in v2.1. v2.0 RSS is bounded "
  542. "by the LMDB env mapsize + OS page cache, not by "
  543. "MemoryStore eviction. Setting still applied to "
  544. "the MemoryStore mirror for back-compat.",
  545. deprecated);
  546. }
  547. }
  548. config.maxMemoryMb = memory.value("max_memory_mb", config.maxMemoryMb);
  549. config.evictionThresholdPercent = memory.value("eviction_threshold_percent", config.evictionThresholdPercent);
  550. config.evictionTargetPercent = memory.value("eviction_target_percent", config.evictionTargetPercent);
  551. config.evictionCheckIntervalMs = memory.value("eviction_check_interval_ms", config.evictionCheckIntervalMs);
  552. // NEW v1.7.0 eviction tuning (schema-only in T2; runtime use lands in T3/T4)
  553. config.evictionChunkSize = memory.value("eviction_chunk_size", config.evictionChunkSize);
  554. config.evictionChunkPauseMs = memory.value("eviction_chunk_pause_ms", config.evictionChunkPauseMs);
  555. config.maxEvictionPassesPerTrigger = memory.value("max_eviction_passes_per_trigger", config.maxEvictionPassesPerTrigger);
  556. config.hotWriteFloorMs = memory.value("hot_write_floor_ms", config.hotWriteFloorMs);
  557. config.memorySoftPercent = memory.value("memory_soft_percent", config.memorySoftPercent);
  558. config.memoryHardPercent = memory.value("memory_hard_percent", config.memoryHardPercent);
  559. config.memoryEmergencyPercent = memory.value("memory_emergency_percent", config.memoryEmergencyPercent);
  560. // v1.7.0 T10 — eviction burst event threshold
  561. config.evictionBurstThreshold = memory.value("eviction_burst_threshold", config.evictionBurstThreshold);
  562. }
  563. // Persistence settings
  564. if (db.contains("persistence")) {
  565. auto& persistence = db["persistence"];
  566. config.walSyncIntervalMs = persistence.value("wal_sync_interval_ms", config.walSyncIntervalMs);
  567. config.snapshotIntervalSec = persistence.value("snapshot_interval_sec", config.snapshotIntervalSec);
  568. config.compressionEnabled = persistence.value("compression", "lz4") != "none";
  569. // Snapshot durability (NEW in v1.6.1)
  570. if (persistence.contains("snapshots")) {
  571. const auto& snap = persistence["snapshots"];
  572. config.persistenceConfig.validateAfterWrite = snap.value("validate_after_write", config.persistenceConfig.validateAfterWrite);
  573. config.persistenceConfig.cleanupOnlyIfVerified = snap.value("cleanup_only_if_verified", config.persistenceConfig.cleanupOnlyIfVerified);
  574. }
  575. // Recovery (NEW in v1.6.1)
  576. if (persistence.contains("recovery")) {
  577. const auto& rec = persistence["recovery"];
  578. std::string modeStr = rec.value("mode", std::string("normal"));
  579. try {
  580. config.persistenceConfig.recoveryMode = recoveryModeFromString(modeStr);
  581. } catch (const std::exception& e) {
  582. spdlog::warn("Invalid recovery.mode '{}', defaulting to 'normal': {}",
  583. modeStr, e.what());
  584. config.persistenceConfig.recoveryMode = RecoveryMode::Normal;
  585. }
  586. config.persistenceConfig.autoEscalate = rec.value("auto_escalate", config.persistenceConfig.autoEscalate);
  587. config.persistenceConfig.allowEmptyOnFreshInstall = rec.value("allow_empty_on_fresh_install", config.persistenceConfig.allowEmptyOnFreshInstall);
  588. }
  589. }
  590. // File settings
  591. if (db.contains("files")) {
  592. auto& files = db["files"];
  593. config.maxFileSizeMb = files.value("max_file_size_mb", config.maxFileSizeMb);
  594. config.fileCleanupIntervalSec = files.value("cleanup_orphans_interval_sec", config.fileCleanupIntervalSec);
  595. if (files.contains("allowed_types")) {
  596. for (const auto& type : files["allowed_types"]) {
  597. config.allowedFileTypes.push_back(type.get<std::string>());
  598. }
  599. }
  600. }
  601. // Encryption settings
  602. if (db.contains("encryption")) {
  603. auto& encryption = db["encryption"];
  604. config.encryptionEnabled = encryption.value("enabled", config.encryptionEnabled);
  605. config.autoGenerateKey = encryption.value("auto_generate_key", config.autoGenerateKey);
  606. std::string keyFile = encryption.value("key_file", "");
  607. if (keyFile.find("${HOME}") != std::string::npos) {
  608. const char* home = std::getenv("HOME");
  609. if (home) {
  610. size_t pos = keyFile.find("${HOME}");
  611. keyFile.replace(pos, 7, home);
  612. }
  613. }
  614. config.keyFilePath = keyFile;
  615. }
  616. // Replication settings
  617. if (db.contains("replication")) {
  618. auto& replication = db["replication"];
  619. config.replicationEnabled = replication.value("enabled", config.replicationEnabled);
  620. config.conflictResolution = replication.value("conflict_resolution", config.conflictResolution);
  621. if (replication.contains("peers")) {
  622. for (const auto& peer : replication["peers"]) {
  623. config.peerAddresses.push_back(peer.get<std::string>());
  624. }
  625. }
  626. }
  627. // gRPC settings (v1.6.2 — concurrency cap + configurable message sizes)
  628. if (db.contains("grpc")) {
  629. const auto& grpc = db["grpc"];
  630. config.grpc.maxReceiveMessageSizeMb =
  631. grpc.value("max_receive_message_size_mb", config.grpc.maxReceiveMessageSizeMb);
  632. config.grpc.maxSendMessageSizeMb =
  633. grpc.value("max_send_message_size_mb", config.grpc.maxSendMessageSizeMb);
  634. config.grpc.resourceQuotaMemoryMb =
  635. grpc.value("resource_quota_memory_mb", config.grpc.resourceQuotaMemoryMb);
  636. config.grpc.maxConcurrentSubscribeStreams =
  637. grpc.value("max_concurrent_subscribe_streams", config.grpc.maxConcurrentSubscribeStreams);
  638. config.grpc.maxConcurrentFileStreams =
  639. grpc.value("max_concurrent_file_streams", config.grpc.maxConcurrentFileStreams);
  640. }
  641. }
  642. // v2.4 back-compat shim. If no listeners[] was provided, synthesize
  643. // one from the legacy bind_address + rpc_port fields. This is what
  644. // every existing v2.3 deployment will hit on first v2.4 boot —
  645. // their configs don't know about listeners[] yet, and they get
  646. // exactly the same listener they had before.
  647. if (config.listeners.empty()) {
  648. ListenerConfig legacy;
  649. legacy.bind = config.bindAddress;
  650. legacy.port = config.rpcPort;
  651. legacy.tls.enabled = false;
  652. legacy.auth.required = false;
  653. config.listeners.push_back(std::move(legacy));
  654. }
  655. return config;
  656. }
  657. void DatabaseService::setupComponents() {
  658. // v2.3 multi-project storage substrate.
  659. //
  660. // Each project lives at `<dataDir>/projects/<name>/env/`. The default
  661. // project always exists and is what v2.0-v2.2 clients (which don't
  662. // address projects) implicitly use.
  663. //
  664. // Pre-2.3 installs have their data at the legacy `<dataDir>/env/`
  665. // path. migrateLegacyEnvToDefaultProject() detects that layout and
  666. // atomically moves it under `projects/default/env/` before the
  667. // registry opens.
  668. migrateLegacyEnvToDefaultProject();
  669. try {
  670. constexpr size_t kMapSize = 4ULL << 30; // 4 GiB per project; LMDB grows lazily.
  671. constexpr uint32_t kMaxDbs = 1024;
  672. projects_ = std::make_unique<smartbotic::db::storage::ProjectStoreRegistry>(
  673. config_.dataDirectory / "projects", kMapSize, kMaxDbs);
  674. const auto opened = projects_->openExisting();
  675. spdlog::info("v2.3 storage: opened {} project env(s): [{}]",
  676. opened.size(),
  677. [&opened]() {
  678. std::string s;
  679. for (size_t i = 0; i < opened.size(); ++i) {
  680. if (i) s += ", ";
  681. s += opened[i];
  682. }
  683. return s;
  684. }());
  685. } catch (const std::exception& e) {
  686. spdlog::error("v2.3 storage: failed to open project registry at '{}': {} -- "
  687. "service cannot start without LMDB; bailing",
  688. (config_.dataDirectory / "projects").string(), e.what());
  689. projects_.reset();
  690. throw;
  691. }
  692. // Create memory store with eviction config
  693. MemoryStore::Config storeConfig;
  694. storeConfig.nodeId = config_.nodeId;
  695. storeConfig.maxMemoryBytes = config_.maxMemoryMb * 1024ULL * 1024ULL;
  696. storeConfig.evictionThresholdPercent = config_.evictionThresholdPercent;
  697. storeConfig.evictionTargetPercent = config_.evictionTargetPercent;
  698. storeConfig.evictionCheckIntervalMs = config_.evictionCheckIntervalMs;
  699. // NEW v1.7.0 eviction tuning (wired in T2; consumed in T3/T4).
  700. storeConfig.evictionChunkSize = config_.evictionChunkSize;
  701. storeConfig.evictionChunkPauseMs = config_.evictionChunkPauseMs;
  702. storeConfig.maxEvictionPassesPerTrigger = config_.maxEvictionPassesPerTrigger;
  703. storeConfig.hotWriteFloorMs = config_.hotWriteFloorMs;
  704. storeConfig.memorySoftPercent = config_.memorySoftPercent;
  705. storeConfig.memoryHardPercent = config_.memoryHardPercent;
  706. storeConfig.memoryEmergencyPercent = config_.memoryEmergencyPercent;
  707. storeConfig.evictionBurstThreshold = config_.evictionBurstThreshold;
  708. store_ = std::make_unique<MemoryStore>(storeConfig);
  709. // Create view manager (cache loaded in initialize() after persistence recovery)
  710. view_manager_ = std::make_unique<ViewManager>(*store_);
  711. // Create per-collection config manager and attach it to the store so the
  712. // write paths route document timestamp stamps through it.
  713. config_manager_ = std::make_unique<CollectionConfigManager>(*store_);
  714. store_->setConfigManager(config_manager_.get());
  715. // v1.9.0 — disk-resident version history. Replaces the in-heap
  716. // `CollectionData::versionHistory` deques that were the dominant
  717. // source of the Zoe untracked-RSS gap. One file per collection at
  718. // `<dataDir>/history/<collection>.hlog`. Lifecycle: constructed
  719. // here, wired into the store immediately so snapshot deserializer
  720. // can migrate pre-v1.9 in-memory history blocks straight to disk.
  721. HistoryStore::Config histConfig;
  722. histConfig.dataDir = config_.dataDirectory;
  723. history_store_ = std::make_unique<HistoryStore>(histConfig);
  724. store_->setHistoryStore(history_store_.get());
  725. // v2.3 — wire the LMDB mirror INTO MemoryStore with a project
  726. // resolver. Every write that flows through store_ commits to the
  727. // right per-project LMDB env BEFORE the per-collection lock releases.
  728. if (projects_) {
  729. auto* registry = projects_.get();
  730. store_->setDocumentStoreMirror(
  731. [registry](std::string_view project) -> smartbotic::db::storage::DocumentStore* {
  732. return registry->getOrCreate(project);
  733. },
  734. &mirror_healthy_,
  735. &mirror_drift_count_);
  736. }
  737. // Create persistence manager. Start from any persistenceConfig values the
  738. // caller (config loader / CLI parser) has already populated — including
  739. // recoveryMode — and overlay the top-level convenience fields.
  740. PersistenceManager::Config persistConfig = config_.persistenceConfig;
  741. persistConfig.dataDir = config_.dataDirectory;
  742. persistConfig.walSyncIntervalMs = config_.walSyncIntervalMs;
  743. persistConfig.snapshotIntervalSec = config_.snapshotIntervalSec;
  744. persistConfig.compressionEnabled = config_.compressionEnabled;
  745. persistConfig.nodeId = config_.nodeId;
  746. // Mirror the resolved recoveryMode back into our Config so downstream code
  747. // (logging, RPC handlers) can inspect config_.persistenceConfig.recoveryMode
  748. // without having to reach into the PersistenceManager.
  749. config_.persistenceConfig = persistConfig;
  750. persistence_ = std::make_unique<PersistenceManager>(persistConfig);
  751. // Connect store callbacks to persistence - uses setPersistCallback for WAL logging
  752. store_->setPersistCallback([this](const std::string& collection, const std::string& id,
  753. const std::optional<Document>& doc, EventType eventType) {
  754. // Log to WAL based on event type. v1.7.4: capture the assigned WAL
  755. // sequence and record it on the store so eviction stubs can carry
  756. // a real seq (instead of doc.version, which was useless as a
  757. // `fromSequence` hint and forced a full WAL scan on every fault).
  758. uint64_t walSeq = 0;
  759. switch (eventType) {
  760. case EventType::INSERT:
  761. if (doc) walSeq = persistence_->logInsert(collection, *doc);
  762. break;
  763. case EventType::UPDATE:
  764. if (doc) walSeq = persistence_->logUpdate(collection, *doc);
  765. break;
  766. case EventType::DELETE:
  767. persistence_->logDelete(collection, id);
  768. break;
  769. default:
  770. break;
  771. }
  772. if (walSeq > 0 && (eventType == EventType::INSERT || eventType == EventType::UPDATE)) {
  773. store_->recordWalSequence(collection, id, walSeq);
  774. }
  775. // v2.0 dual-write — moved INTO MemoryStore::mirrorWriteToDocStore so
  776. // that it runs UNDER the per-collection write lock. That closes the
  777. // race window where readers could see a doc in MemoryStore but not
  778. // yet in LMDB. The callback now does only WAL + replication + events.
  779. // Queue for replication broadcast
  780. if (replication_ && config_.replicationEnabled) {
  781. databasepb::ReplicationEntry entry;
  782. entry.set_collection(collection);
  783. entry.set_document_id(id);
  784. // v1.8.0 — stamp our nodeId so peers can attribute the write to
  785. // its actual origin. Pre-v1.8 broadcasts left this empty, which
  786. // is now treated by receivers as "legacy / unknown origin".
  787. entry.set_node_id(config_.nodeId);
  788. entry.set_global_timestamp(
  789. std::chrono::duration_cast<std::chrono::milliseconds>(
  790. std::chrono::system_clock::now().time_since_epoch()
  791. ).count());
  792. switch (eventType) {
  793. case EventType::INSERT:
  794. entry.set_op(databasepb::OP_INSERT);
  795. // Send the full Document envelope (id, collection, data,
  796. // version, timestamps, encryption state). The follower's
  797. // applyReplicatedEntry uses Document::fromJson() which
  798. // expects this envelope — sending only doc->data silently
  799. // dropped every user field on the other side.
  800. if (doc) entry.set_data(doc->toJson().dump());
  801. break;
  802. case EventType::UPDATE:
  803. entry.set_op(databasepb::OP_UPDATE);
  804. if (doc) entry.set_data(doc->toJson().dump());
  805. break;
  806. case EventType::DELETE:
  807. entry.set_op(databasepb::OP_DELETE);
  808. break;
  809. default:
  810. break;
  811. }
  812. replication_->queueForReplication(entry);
  813. }
  814. // Publish event
  815. if (events_) {
  816. DatabaseEvent event;
  817. event.type = eventType;
  818. event.collection = collection;
  819. event.documentId = id;
  820. event.timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
  821. std::chrono::system_clock::now().time_since_epoch()
  822. ).count();
  823. event.nodeId = config_.nodeId;
  824. if (doc) {
  825. event.data = doc->data();
  826. }
  827. events_->publish(event);
  828. }
  829. });
  830. // Set up document load callback for LRU eviction recovery.
  831. //
  832. // v1.7.4: walSequence is now a real WAL sequence (set by the persist
  833. // callback below at append time, not the per-doc version counter).
  834. // Pass `walSequence - 1` as the exclusive `fromSequence` so replay
  835. // returns entries with seq >= walSequence — the doc's last write is
  836. // exactly at walSequence, so the very first hit is the one we want.
  837. // For docs whose seq is unknown (0 — e.g. loaded from a snapshot
  838. // that predates v1.7.4), fall back to the full-WAL scan from 0.
  839. store_->setDocumentLoadCallback([this](const std::string& collection,
  840. const std::string& id,
  841. uint64_t walSequence) -> std::optional<Document> {
  842. uint64_t fromSequence = walSequence > 0 ? walSequence - 1 : 0;
  843. return persistence_->loadDocument(collection, id, fromSequence);
  844. });
  845. // Create encryption manager
  846. EncryptionManager::Config encryptConfig;
  847. encryptConfig.enabled = config_.encryptionEnabled;
  848. encryptConfig.keyFilePath = config_.keyFilePath;
  849. encryptConfig.autoGenerateKey = config_.autoGenerateKey;
  850. encryption_ = std::make_unique<EncryptionManager>(encryptConfig);
  851. // Create event manager
  852. EventManager::Config eventConfig;
  853. eventConfig.nodeId = config_.nodeId;
  854. events_ = std::make_unique<EventManager>(eventConfig);
  855. // Create file manager
  856. FileManager::Config fileConfig;
  857. fileConfig.filesDir = config_.dataDirectory / "files";
  858. fileConfig.maxFileSizeMb = config_.maxFileSizeMb;
  859. fileConfig.allowedMimeTypes = config_.allowedFileTypes;
  860. fileConfig.cleanupIntervalSec = config_.fileCleanupIntervalSec;
  861. files_ = std::make_unique<FileManager>(fileConfig);
  862. // Create replication manager
  863. ReplicationManager::Config replConfig;
  864. replConfig.nodeId = config_.nodeId;
  865. replConfig.peerAddresses = config_.peerAddresses;
  866. replConfig.conflictResolution = config_.conflictResolution;
  867. replication_ = std::make_unique<ReplicationManager>(replConfig);
  868. // Wire replication entry apply callback
  869. replication_->setEntryApplyCallback([this](const databasepb::ReplicationEntry& entry) {
  870. applyReplicatedEntry(entry);
  871. });
  872. // Create gRPC implementations
  873. storageImpl_ = std::make_unique<DatabaseGrpcImpl>(
  874. *this, *store_, *persistence_, *events_, *files_, *encryption_, *view_manager_, *config_manager_
  875. );
  876. // v1.6.2 — wire the streaming-RPC concurrency limits from GrpcConfig.
  877. storageImpl_->setStreamLimits(
  878. config_.grpc.maxConcurrentSubscribeStreams,
  879. config_.grpc.maxConcurrentFileStreams);
  880. replicationImpl_ = std::make_unique<DatabaseReplicationGrpcImpl>(
  881. *store_, *replication_, *persistence_
  882. );
  883. }
  884. void DatabaseService::applyReplicatedEntry(const databasepb::ReplicationEntry& entry) {
  885. try {
  886. // v1.8.0 — also append the replicated entry to the local WAL, tagged
  887. // with the originating node's id. This is what makes follower-side
  888. // recovery (eviction → WAL fallback, restart → WAL replay) preserve
  889. // replicated documents. Echo amplification is prevented by:
  890. // - GetEntriesSince emitting walEntry.nodeId (not local node id)
  891. // - SyncProtocol skipping entries whose origin is the peer itself
  892. // both implemented in the same v1.8.0 cycle.
  893. const std::string origin = entry.node_id().empty()
  894. ? config_.nodeId // legacy peer (pre-1.8) — best-guess local
  895. : entry.node_id();
  896. switch (entry.op()) {
  897. case databasepb::OP_INSERT:
  898. case databasepb::OP_UPDATE:
  899. case databasepb::OP_UPSERT: {
  900. if (entry.data().empty()) {
  901. spdlog::warn("Replication entry has no data for op {}", static_cast<int>(entry.op()));
  902. return;
  903. }
  904. auto json = smartbotic::db::parse_to_nlohmann(entry.data());
  905. Document doc = Document::fromJson(json);
  906. doc.id = entry.document_id();
  907. doc.nodeId = entry.node_id();
  908. // loadDocument bypasses the persist-and-broadcast callback to avoid
  909. // re-replicating; we explicitly write to the WAL right after with
  910. // the origin-aware overload so eviction/WAL recovery still works.
  911. store_->loadDocument(entry.collection(), doc);
  912. // v2.3.1 — also mirror to LMDB. loadDocument bypasses the
  913. // MemoryStore persist callback that normally drives the
  914. // dual-write mirror; in v2.0-v2.2 the mirror was a side
  915. // channel and reads still hit MemoryStore, so the bypass
  916. // was harmless. In v2.3 reads are LMDB-first, so a
  917. // follower that only filled MemoryStore would return
  918. // empty Find/Get for every replicated doc. Route the
  919. // entry through the same project-aware mirror the write
  920. // handlers use.
  921. if (projects_) {
  922. auto pc = smartbotic::database::parseProjectCollection(
  923. entry.collection());
  924. if (auto* ds = projects_->getOrCreate(pc.project)) {
  925. std::optional<Document> opt_doc(doc);
  926. smartbotic::db::storage::applyDualWriteMirror(
  927. ds, mirror_healthy_, mirror_drift_count_,
  928. pc.collection, doc.id, opt_doc, EventType::INSERT);
  929. }
  930. }
  931. if (persistence_) {
  932. uint64_t walSeq = (entry.op() == databasepb::OP_INSERT)
  933. ? persistence_->logInsert(entry.collection(), doc, origin)
  934. : persistence_->logUpdate(entry.collection(), doc, origin);
  935. if (walSeq > 0) {
  936. store_->recordWalSequence(entry.collection(), doc.id, walSeq);
  937. }
  938. }
  939. spdlog::trace("Applied replicated {} to {}/{} from {}",
  940. entry.op() == databasepb::OP_INSERT ? "insert" :
  941. entry.op() == databasepb::OP_UPDATE ? "update" : "upsert",
  942. entry.collection(), entry.document_id(), entry.node_id());
  943. break;
  944. }
  945. case databasepb::OP_DELETE: {
  946. store_->remove(entry.collection(), entry.document_id());
  947. if (persistence_) {
  948. persistence_->logDelete(entry.collection(), entry.document_id(), origin);
  949. }
  950. spdlog::trace("Applied replicated delete to {}/{} from {}",
  951. entry.collection(), entry.document_id(), entry.node_id());
  952. break;
  953. }
  954. case databasepb::OP_CREATE_COLLECTION: {
  955. CollectionOptions options;
  956. store_->createCollection(entry.collection(), options);
  957. if (persistence_) {
  958. persistence_->logCreateCollection(entry.collection(), options, origin);
  959. }
  960. spdlog::trace("Applied replicated create collection {} from {}",
  961. entry.collection(), entry.node_id());
  962. break;
  963. }
  964. case databasepb::OP_DROP_COLLECTION: {
  965. store_->dropCollection(entry.collection());
  966. if (persistence_) {
  967. persistence_->logDropCollection(entry.collection(), origin);
  968. }
  969. spdlog::trace("Applied replicated drop collection {} from {}",
  970. entry.collection(), entry.node_id());
  971. break;
  972. }
  973. default:
  974. spdlog::warn("Unknown replication operation type: {}", static_cast<int>(entry.op()));
  975. break;
  976. }
  977. } catch (const std::exception& e) {
  978. spdlog::error("Failed to apply replicated entry: {}", e.what());
  979. }
  980. }
  981. void DatabaseService::startGrpcServer() {
  982. std::string serverAddress = config_.bindAddress + ":" + std::to_string(config_.rpcPort);
  983. grpc::ServerBuilder builder;
  984. builder.AddListeningPort(serverAddress, grpc::InsecureServerCredentials());
  985. builder.RegisterService(storageImpl_.get());
  986. builder.RegisterService(replicationImpl_.get());
  987. // Configurable message sizes (default 100 MB for file uploads)
  988. const int64_t mb = 1024 * 1024;
  989. builder.SetMaxReceiveMessageSize(
  990. static_cast<int>(config_.grpc.maxReceiveMessageSizeMb * mb));
  991. builder.SetMaxSendMessageSize(
  992. static_cast<int>(config_.grpc.maxSendMessageSizeMb * mb));
  993. // ResourceQuota bounds total inbound buffer memory across all RPCs
  994. grpc::ResourceQuota quota("smartbotic-db");
  995. quota.Resize(config_.grpc.resourceQuotaMemoryMb * mb);
  996. builder.SetResourceQuota(quota);
  997. spdlog::info("gRPC config: recv={}MB send={}MB quota={}MB subs={} files={}",
  998. config_.grpc.maxReceiveMessageSizeMb,
  999. config_.grpc.maxSendMessageSizeMb,
  1000. config_.grpc.resourceQuotaMemoryMb,
  1001. config_.grpc.maxConcurrentSubscribeStreams,
  1002. config_.grpc.maxConcurrentFileStreams);
  1003. grpcServer_ = builder.BuildAndStart();
  1004. if (!grpcServer_) {
  1005. throw std::runtime_error("Failed to start gRPC server on " + serverAddress);
  1006. }
  1007. spdlog::info("gRPC server listening on {}", serverAddress);
  1008. }
  1009. void DatabaseService::stopGrpcServer() {
  1010. if (grpcServer_) {
  1011. grpcServer_->Shutdown();
  1012. grpcServer_.reset();
  1013. }
  1014. }
  1015. } // namespace smartbotic::database