test_lmdb_env.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. // v2.0 storage engine — unit tests for LmdbEnv (Task 1.2) plus WriteTxn /
  2. // ReadTxn / LmdbDbi primitives (Task 1.3).
  3. #include <atomic>
  4. #include <cassert>
  5. #include <cstdlib>
  6. #include <filesystem>
  7. #include <iostream>
  8. #include <optional>
  9. #include <stdexcept>
  10. #include <string>
  11. #include <string_view>
  12. #include <unistd.h>
  13. #include <lmdb.h>
  14. #include "storage/lmdb_dbi.hpp"
  15. #include "storage/lmdb_env.hpp"
  16. #include "storage/lmdb_txn.hpp"
  17. namespace fs = std::filesystem;
  18. using smartbotic::db::storage::LmdbDbi;
  19. using smartbotic::db::storage::LmdbEnv;
  20. using smartbotic::db::storage::LmdbEnvOpts;
  21. using smartbotic::db::storage::ReadTxn;
  22. using smartbotic::db::storage::WriteTxn;
  23. namespace {
  24. void check(bool cond, const char* msg) {
  25. if (!cond) {
  26. std::cerr << "FAIL: " << msg << "\n";
  27. std::abort();
  28. }
  29. }
  30. // Per-process, unique tmpdir for test isolation.
  31. std::string make_tmpdir(const char* tag) {
  32. static std::atomic<int> counter{0};
  33. int n = counter.fetch_add(1);
  34. std::string path = "/tmp/lmdb-env-test-" + std::to_string(::getpid()) +
  35. "-" + std::to_string(n) + "-" + tag;
  36. std::error_code ec;
  37. fs::remove_all(path, ec);
  38. return path;
  39. }
  40. struct TmpDir {
  41. std::string path;
  42. explicit TmpDir(const char* tag) : path(make_tmpdir(tag)) {}
  43. ~TmpDir() {
  44. std::error_code ec;
  45. fs::remove_all(path, ec);
  46. }
  47. TmpDir(const TmpDir&) = delete;
  48. TmpDir& operator=(const TmpDir&) = delete;
  49. };
  50. // Helper: populate an LMDB env via raw mdb_* calls. Used by the env-only
  51. // tests below; once Task 1.3 adds WriteTxn / LmdbDbi, the txn-level tests
  52. // will use those wrappers instead.
  53. void raw_populate(const std::string& path, size_t map_size,
  54. int n_entries, size_t entry_size) {
  55. std::error_code ec;
  56. fs::create_directories(path, ec);
  57. if (ec) {
  58. throw std::runtime_error(
  59. "raw_populate: create_directories: " + ec.message());
  60. }
  61. MDB_env* env = nullptr;
  62. if (mdb_env_create(&env) != MDB_SUCCESS) {
  63. throw std::runtime_error("raw_populate: env_create");
  64. }
  65. if (mdb_env_set_mapsize(env, map_size) != MDB_SUCCESS) {
  66. mdb_env_close(env);
  67. throw std::runtime_error("raw_populate: set_mapsize");
  68. }
  69. if (mdb_env_open(env, path.c_str(), 0, 0644) != MDB_SUCCESS) {
  70. mdb_env_close(env);
  71. throw std::runtime_error("raw_populate: env_open");
  72. }
  73. MDB_txn* txn = nullptr;
  74. if (mdb_txn_begin(env, nullptr, 0, &txn) != MDB_SUCCESS) {
  75. mdb_env_close(env);
  76. throw std::runtime_error("raw_populate: txn_begin");
  77. }
  78. MDB_dbi dbi = 0;
  79. if (mdb_dbi_open(txn, nullptr, MDB_CREATE, &dbi) != MDB_SUCCESS) {
  80. mdb_txn_abort(txn);
  81. mdb_env_close(env);
  82. throw std::runtime_error("raw_populate: dbi_open");
  83. }
  84. std::string blob(entry_size, 'x');
  85. for (int i = 0; i < n_entries; ++i) {
  86. std::string k = "k" + std::to_string(i);
  87. MDB_val kv{k.size(), const_cast<char*>(k.data())};
  88. MDB_val vv{blob.size(), const_cast<char*>(blob.data())};
  89. int rc = mdb_put(txn, dbi, &kv, &vv, 0);
  90. if (rc != MDB_SUCCESS) {
  91. mdb_txn_abort(txn);
  92. mdb_env_close(env);
  93. throw std::runtime_error("raw_populate: put");
  94. }
  95. }
  96. if (mdb_txn_commit(txn) != MDB_SUCCESS) {
  97. mdb_env_close(env);
  98. throw std::runtime_error("raw_populate: txn_commit");
  99. }
  100. mdb_env_close(env);
  101. }
  102. void raw_get(const std::string& path, const std::string& key,
  103. std::string& out_value, bool& found) {
  104. found = false;
  105. MDB_env* env = nullptr;
  106. if (mdb_env_create(&env) != MDB_SUCCESS) {
  107. throw std::runtime_error("raw_get: env_create");
  108. }
  109. if (mdb_env_set_mapsize(env, 4ULL << 20) != MDB_SUCCESS) {
  110. mdb_env_close(env);
  111. throw std::runtime_error("raw_get: set_mapsize");
  112. }
  113. if (mdb_env_open(env, path.c_str(), 0, 0644) != MDB_SUCCESS) {
  114. mdb_env_close(env);
  115. throw std::runtime_error("raw_get: env_open");
  116. }
  117. MDB_txn* txn = nullptr;
  118. if (mdb_txn_begin(env, nullptr, MDB_RDONLY, &txn) != MDB_SUCCESS) {
  119. mdb_env_close(env);
  120. throw std::runtime_error("raw_get: txn_begin");
  121. }
  122. MDB_dbi dbi = 0;
  123. int rc = mdb_dbi_open(txn, nullptr, 0, &dbi);
  124. if (rc != MDB_SUCCESS) {
  125. mdb_txn_abort(txn);
  126. mdb_env_close(env);
  127. if (rc == MDB_NOTFOUND) return;
  128. throw std::runtime_error("raw_get: dbi_open");
  129. }
  130. MDB_val kv{key.size(), const_cast<char*>(key.data())};
  131. MDB_val vv{0, nullptr};
  132. rc = mdb_get(txn, dbi, &kv, &vv);
  133. if (rc == MDB_SUCCESS) {
  134. out_value.assign(static_cast<const char*>(vv.mv_data), vv.mv_size);
  135. found = true;
  136. }
  137. mdb_txn_abort(txn);
  138. mdb_env_close(env);
  139. }
  140. // =========================================================================
  141. // Task 1.2 — LmdbEnv tests
  142. // =========================================================================
  143. void test_open_creates_dir() {
  144. TmpDir td("create-dir");
  145. check(!fs::exists(td.path), "precondition: tmpdir absent");
  146. LmdbEnvOpts opts;
  147. opts.path = td.path;
  148. LmdbEnv env(opts);
  149. check(fs::exists(td.path) && fs::is_directory(td.path),
  150. "LmdbEnv constructor created the env directory");
  151. // LMDB also created its data.mdb / lock.mdb in there.
  152. check(fs::exists(td.path + "/data.mdb"), "data.mdb exists after open");
  153. check(fs::exists(td.path + "/lock.mdb"), "lock.mdb exists after open");
  154. }
  155. void test_path_accessor() {
  156. TmpDir td("path-accessor");
  157. LmdbEnvOpts opts;
  158. opts.path = td.path;
  159. LmdbEnv env(opts);
  160. check(env.path() == td.path, "env.path() returns configured path");
  161. check(env.raw() != nullptr, "env.raw() returns non-null MDB_env*");
  162. }
  163. void test_mapsize_too_small_fails() {
  164. TmpDir td("mapsize-small");
  165. // Populate via raw LMDB so the data.mdb file grows past the small mapsize
  166. // we'll try to reopen with.
  167. raw_populate(td.path, 4ULL << 20 /* 4 MiB */, 16, 64 * 1024 /* 64 KiB */);
  168. // Sanity: the populated dir reports nonzero on-disk bytes.
  169. uint64_t bytes = 0;
  170. {
  171. LmdbEnvOpts opts;
  172. opts.path = td.path;
  173. opts.map_size_bytes = 4ULL << 20;
  174. LmdbEnv env(opts);
  175. bytes = env.on_disk_bytes();
  176. }
  177. check(bytes > 0, "populated env has nonzero on_disk_bytes");
  178. // Re-opening with a smaller mapsize than the existing data file must
  179. // fail fast — LMDB rejects mapsize < size-of-data-file.
  180. LmdbEnvOpts opts;
  181. opts.path = td.path;
  182. opts.map_size_bytes = 4096; // 4 KiB — far below data.mdb size.
  183. bool threw = false;
  184. try {
  185. LmdbEnv env(opts);
  186. } catch (const std::runtime_error&) {
  187. threw = true;
  188. }
  189. check(threw, "opening with too-small mapsize fails fast");
  190. }
  191. void test_double_open_same_path_separate_envs() {
  192. TmpDir td("double-open");
  193. LmdbEnvOpts opts;
  194. opts.path = td.path;
  195. // Seed via raw LMDB so we have data to read through the second handle.
  196. raw_populate(td.path, 4ULL << 20, 4, 64);
  197. // LMDB supports multi-process access; two envs on the same path should
  198. // both be openable from the same process too.
  199. LmdbEnv a(opts);
  200. LmdbEnv b(opts);
  201. check(a.raw() != nullptr, "first env handle non-null");
  202. check(b.raw() != nullptr, "second env handle non-null");
  203. check(a.raw() != b.raw(), "two LmdbEnv handles are distinct objects");
  204. // Sanity: both envs see the seeded data via raw_get (which opens a third
  205. // handle internally — proves multi-handle reads work).
  206. std::string v;
  207. bool found = false;
  208. raw_get(td.path, "k0", v, found);
  209. check(found, "raw_get sees seeded key while two LmdbEnv handles are open");
  210. }
  211. void test_on_disk_bytes_nonzero_after_create() {
  212. TmpDir td("on-disk-bytes");
  213. LmdbEnvOpts opts;
  214. opts.path = td.path;
  215. LmdbEnv env(opts);
  216. check(env.on_disk_bytes() > 0,
  217. "fresh env reports nonzero on_disk_bytes (data.mdb/lock.mdb initialised)");
  218. }
  219. void test_move_constructor_transfers_ownership() {
  220. TmpDir td("move-ctor");
  221. LmdbEnvOpts opts;
  222. opts.path = td.path;
  223. LmdbEnv a(opts);
  224. MDB_env* raw_a = a.raw();
  225. check(raw_a != nullptr, "source env has non-null raw before move");
  226. LmdbEnv b(std::move(a));
  227. check(b.raw() == raw_a, "moved-to env owns the original handle");
  228. check(a.raw() == nullptr, "moved-from env has null handle");
  229. check(b.path() == td.path, "moved-to env retains path");
  230. }
  231. // =========================================================================
  232. // Task 1.3 — WriteTxn / ReadTxn / LmdbDbi tests
  233. // =========================================================================
  234. void test_write_txn_commit_roundtrip() {
  235. TmpDir td("commit-roundtrip");
  236. LmdbEnvOpts opts;
  237. opts.path = td.path;
  238. LmdbEnv env(opts);
  239. {
  240. WriteTxn wtxn(env);
  241. LmdbDbi dbi(wtxn, "_test");
  242. dbi.put(wtxn, "alpha", "one");
  243. dbi.put(wtxn, "beta", "two");
  244. wtxn.commit();
  245. }
  246. {
  247. ReadTxn rtxn(env);
  248. LmdbDbi dbi(rtxn, "_test");
  249. auto a = dbi.get(rtxn, "alpha");
  250. check(a.has_value() && std::string(*a) == "one", "alpha = one");
  251. auto b = dbi.get(rtxn, "beta");
  252. check(b.has_value() && std::string(*b) == "two", "beta = two");
  253. auto c = dbi.get(rtxn, "gamma");
  254. check(!c.has_value(), "gamma absent -> nullopt");
  255. }
  256. }
  257. void test_write_txn_abort_discards() {
  258. TmpDir td("abort-discards");
  259. LmdbEnvOpts opts;
  260. opts.path = td.path;
  261. LmdbEnv env(opts);
  262. // First commit creates the sub-db so the post-abort read txn can open it.
  263. {
  264. WriteTxn wtxn(env);
  265. LmdbDbi dbi(wtxn, "_test");
  266. dbi.put(wtxn, "seed", "ok");
  267. wtxn.commit();
  268. }
  269. {
  270. WriteTxn wtxn(env);
  271. LmdbDbi dbi(wtxn, "_test");
  272. dbi.put(wtxn, "alpha", "one");
  273. wtxn.abort();
  274. }
  275. {
  276. ReadTxn rtxn(env);
  277. LmdbDbi dbi(rtxn, "_test");
  278. auto a = dbi.get(rtxn, "alpha");
  279. check(!a.has_value(), "aborted put leaves no trace");
  280. auto s = dbi.get(rtxn, "seed");
  281. check(s.has_value(), "seed survived (sanity check that the sub-db is intact)");
  282. }
  283. }
  284. void test_read_during_write() {
  285. TmpDir td("read-during-write");
  286. LmdbEnvOpts opts;
  287. opts.path = td.path;
  288. LmdbEnv env(opts);
  289. // Seed: k=v1.
  290. {
  291. WriteTxn wtxn(env);
  292. LmdbDbi dbi(wtxn, "_test");
  293. dbi.put(wtxn, "k", "v1");
  294. wtxn.commit();
  295. }
  296. // Open a long-lived read txn on the v1 snapshot.
  297. ReadTxn rtxn(env);
  298. LmdbDbi rdbi(rtxn, "_test");
  299. // Update k -> v2 in a separate, committed write txn.
  300. {
  301. WriteTxn wtxn(env);
  302. LmdbDbi wdbi(wtxn, "_test");
  303. wdbi.put(wtxn, "k", "v2");
  304. wtxn.commit();
  305. }
  306. // The pre-existing read txn still sees v1 (LMDB MVCC snapshot).
  307. auto got = rdbi.get(rtxn, "k");
  308. check(got.has_value() && std::string(*got) == "v1",
  309. "long-lived read txn sees pre-write snapshot value");
  310. // A fresh read txn sees v2.
  311. {
  312. ReadTxn r2(env);
  313. LmdbDbi d2(r2, "_test");
  314. auto g2 = d2.get(r2, "k");
  315. check(g2.has_value() && std::string(*g2) == "v2",
  316. "fresh read txn sees post-write value");
  317. }
  318. }
  319. void test_multiple_subdbs_isolated() {
  320. TmpDir td("multi-subdb");
  321. LmdbEnvOpts opts;
  322. opts.path = td.path;
  323. LmdbEnv env(opts);
  324. {
  325. WriteTxn wtxn(env);
  326. LmdbDbi a(wtxn, "_a");
  327. LmdbDbi b(wtxn, "_b");
  328. a.put(wtxn, "k", "from_a");
  329. b.put(wtxn, "k", "from_b");
  330. wtxn.commit();
  331. }
  332. {
  333. ReadTxn rtxn(env);
  334. LmdbDbi a(rtxn, "_a");
  335. LmdbDbi b(rtxn, "_b");
  336. auto ga = a.get(rtxn, "k");
  337. auto gb = b.get(rtxn, "k");
  338. check(ga.has_value() && std::string(*ga) == "from_a",
  339. "sub-db _a returns _a's value for k");
  340. check(gb.has_value() && std::string(*gb) == "from_b",
  341. "sub-db _b returns _b's value for k");
  342. }
  343. }
  344. void test_del_returns_false_on_missing() {
  345. TmpDir td("del-missing");
  346. LmdbEnvOpts opts;
  347. opts.path = td.path;
  348. LmdbEnv env(opts);
  349. {
  350. WriteTxn wtxn(env);
  351. LmdbDbi dbi(wtxn, "_test");
  352. dbi.put(wtxn, "present", "x");
  353. bool d1 = dbi.del(wtxn, "absent");
  354. check(!d1, "del on missing key returns false (not an error)");
  355. bool d2 = dbi.del(wtxn, "present");
  356. check(d2, "del on present key returns true");
  357. bool d3 = dbi.del(wtxn, "present");
  358. check(!d3, "second del on now-absent key returns false");
  359. wtxn.commit();
  360. }
  361. }
  362. void test_count_matches_puts() {
  363. TmpDir td("count");
  364. LmdbEnvOpts opts;
  365. opts.path = td.path;
  366. LmdbEnv env(opts);
  367. const int N = 100;
  368. {
  369. WriteTxn wtxn(env);
  370. LmdbDbi dbi(wtxn, "_test");
  371. for (int i = 0; i < N; ++i) {
  372. std::string k = "k" + std::to_string(i);
  373. dbi.put(wtxn, k, "v");
  374. }
  375. wtxn.commit();
  376. }
  377. {
  378. ReadTxn rtxn(env);
  379. LmdbDbi dbi(rtxn, "_test");
  380. check(dbi.count(rtxn) == static_cast<uint64_t>(N),
  381. "count() matches puts");
  382. }
  383. }
  384. void test_write_txn_throws_if_committed_twice() {
  385. TmpDir td("double-commit");
  386. LmdbEnvOpts opts;
  387. opts.path = td.path;
  388. LmdbEnv env(opts);
  389. WriteTxn wtxn(env);
  390. LmdbDbi dbi(wtxn, "_test");
  391. dbi.put(wtxn, "k", "v");
  392. wtxn.commit();
  393. bool threw = false;
  394. try {
  395. wtxn.commit();
  396. } catch (const std::runtime_error&) {
  397. threw = true;
  398. }
  399. check(threw, "second commit() throws");
  400. }
  401. void test_write_txn_destructor_aborts_if_not_finalised() {
  402. TmpDir td("dtor-abort");
  403. LmdbEnvOpts opts;
  404. opts.path = td.path;
  405. LmdbEnv env(opts);
  406. // Seed an empty sub-db so the post-scope read txn can open it.
  407. {
  408. WriteTxn wtxn(env);
  409. LmdbDbi dbi(wtxn, "_test");
  410. dbi.put(wtxn, "seed", "s");
  411. wtxn.commit();
  412. }
  413. // Drop a write txn without committing — destructor must abort, not commit.
  414. {
  415. WriteTxn wtxn(env);
  416. LmdbDbi dbi(wtxn, "_test");
  417. dbi.put(wtxn, "alpha", "one");
  418. // No commit; let the dtor run.
  419. }
  420. {
  421. ReadTxn rtxn(env);
  422. LmdbDbi dbi(rtxn, "_test");
  423. auto a = dbi.get(rtxn, "alpha");
  424. check(!a.has_value(), "destructor of uncommitted WriteTxn discards writes");
  425. }
  426. }
  427. void test_commit_after_abort_throws() {
  428. TmpDir td("commit-after-abort");
  429. LmdbEnvOpts opts;
  430. opts.path = td.path;
  431. LmdbEnv env(opts);
  432. WriteTxn wtxn(env);
  433. LmdbDbi dbi(wtxn, "_test");
  434. dbi.put(wtxn, "k", "v");
  435. wtxn.abort();
  436. bool threw = false;
  437. try {
  438. wtxn.commit();
  439. } catch (const std::runtime_error&) {
  440. threw = true;
  441. }
  442. check(threw, "commit() after abort() throws");
  443. }
  444. void test_write_txn_move_construct() {
  445. TmpDir td("write-move");
  446. LmdbEnvOpts opts;
  447. opts.path = td.path;
  448. LmdbEnv env(opts);
  449. auto make_txn = [&]() {
  450. WriteTxn t(env);
  451. return t; // forces move-construction
  452. };
  453. {
  454. WriteTxn moved = make_txn();
  455. LmdbDbi dbi(moved, "_test");
  456. dbi.put(moved, "k", "from-moved");
  457. moved.commit();
  458. }
  459. {
  460. ReadTxn rtxn(env);
  461. LmdbDbi dbi(rtxn, "_test");
  462. auto g = dbi.get(rtxn, "k");
  463. check(g.has_value() && std::string(*g) == "from-moved",
  464. "moved WriteTxn commits successfully");
  465. }
  466. }
  467. void test_get_returns_view_into_mmap() {
  468. // The contract says get() returns a string_view that lives for the
  469. // duration of the txn. Exercise that lifetime: read the value, use
  470. // it while the txn is alive, drop the txn.
  471. TmpDir td("mmap-view");
  472. LmdbEnvOpts opts;
  473. opts.path = td.path;
  474. LmdbEnv env(opts);
  475. {
  476. WriteTxn wtxn(env);
  477. LmdbDbi dbi(wtxn, "_test");
  478. dbi.put(wtxn, "k", "hello world");
  479. wtxn.commit();
  480. }
  481. ReadTxn rtxn(env);
  482. LmdbDbi dbi(rtxn, "_test");
  483. auto v = dbi.get(rtxn, "k");
  484. check(v.has_value(), "get returns value");
  485. check(v->size() == 11, "string_view has expected length");
  486. check(std::string(*v) == "hello world", "string_view content matches");
  487. // rtxn drops at end of scope; we don't touch v after that.
  488. }
  489. } // namespace
  490. int main() {
  491. // Task 1.2 — env.
  492. test_open_creates_dir();
  493. test_path_accessor();
  494. test_mapsize_too_small_fails();
  495. test_double_open_same_path_separate_envs();
  496. test_on_disk_bytes_nonzero_after_create();
  497. test_move_constructor_transfers_ownership();
  498. // Task 1.3 — txn + dbi.
  499. test_write_txn_commit_roundtrip();
  500. test_write_txn_abort_discards();
  501. test_read_during_write();
  502. test_multiple_subdbs_isolated();
  503. test_del_returns_false_on_missing();
  504. test_count_matches_puts();
  505. test_write_txn_throws_if_committed_twice();
  506. test_write_txn_destructor_aborts_if_not_finalised();
  507. test_commit_after_abort_throws();
  508. test_write_txn_move_construct();
  509. test_get_returns_view_into_mmap();
  510. std::cout << "test_lmdb_env: all passed\n";
  511. return 0;
  512. }