test_document_ttl.cpp 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. // v2.8.0 — document TTL lifecycle across write paths.
  2. //
  3. // The bug this pins down: `update()` WIPED a document's expiry.
  4. //
  5. // updateIfVersion built a fresh Document from the caller's request, and
  6. // UpdateRequest has no ttl field, so the incoming expiresAt was always 0. It
  7. // deliberately carried createdAt and createdBy across from the stored document
  8. // but not expiresAt - so any TTL'd document that was ever updated became
  9. // permanent. `patch()` mutates the stored document in place and therefore always
  10. // preserved it, which is why the two paths disagreed.
  11. //
  12. // This matters most where it is least visible: a workflow execution written with
  13. // a TTL and then updated as its status changes (the normal case) silently lost
  14. // its expiry and accumulated forever.
  15. #include <atomic>
  16. #include <chrono>
  17. #include <filesystem>
  18. #include <iostream>
  19. #include <string>
  20. #include <unistd.h>
  21. #include <nlohmann/json.hpp>
  22. #include "memory_store.hpp"
  23. using namespace smartbotic::database;
  24. namespace {
  25. int g_pass = 0;
  26. int g_fail = 0;
  27. void check(bool cond, const char* msg) {
  28. if (cond) { ++g_pass; }
  29. else { ++g_fail; std::cerr << "FAIL: " << msg << "\n"; }
  30. }
  31. struct Fixture {
  32. MemoryStore store;
  33. Fixture() : store(MemoryStore::Config{}) {
  34. store.start();
  35. CollectionOptions o;
  36. store.createCollection("things", o);
  37. }
  38. ~Fixture() { store.stop(); }
  39. };
  40. Document mkDoc(const std::string& id, uint64_t expiresAt = 0) {
  41. Document d;
  42. d.id = id;
  43. d.collection = "things";
  44. d.expiresAt = expiresAt;
  45. d.set_data(nlohmann::json{{"n", 1}});
  46. return d;
  47. }
  48. uint64_t future_ms(uint64_t secs) {
  49. const auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
  50. std::chrono::system_clock::now().time_since_epoch()).count();
  51. return static_cast<uint64_t>(now) + secs * 1000ULL;
  52. }
  53. // -------------------------------------------------------------------------
  54. void test_insert_sets_expiry() {
  55. Fixture f;
  56. const uint64_t exp = future_ms(3600);
  57. f.store.insert("things", mkDoc("a", exp));
  58. auto got = f.store.get("things", "a");
  59. check(got.has_value(), "inserted");
  60. check(got && got->expiresAt == exp, "insert stores the expiry it was given");
  61. }
  62. // THE regression. An update with no TTL must not silently make the document
  63. // permanent.
  64. void test_update_preserves_expiry() {
  65. Fixture f;
  66. const uint64_t exp = future_ms(3600);
  67. f.store.insert("things", mkDoc("a", exp));
  68. auto stored = f.store.get("things", "a");
  69. Document next = mkDoc("a"); // no expiry, exactly as UpdateRequest arrives
  70. next.set_data(nlohmann::json{{"n", 2}});
  71. f.store.updateIfVersion("things", "a", next, stored->version);
  72. auto after = f.store.get("things", "a");
  73. check(after.has_value(), "still present after update");
  74. check(after && after->data().value("n", 0) == 2, "the update applied");
  75. check(after && after->expiresAt == exp,
  76. "update MUST preserve the existing expiry - wiping it makes a TTL'd "
  77. "document permanent");
  78. }
  79. void test_update_can_change_expiry_when_given_one() {
  80. Fixture f;
  81. f.store.insert("things", mkDoc("a", future_ms(60)));
  82. auto stored = f.store.get("things", "a");
  83. const uint64_t later = future_ms(7200);
  84. Document next = mkDoc("a", later); // explicit new expiry
  85. f.store.updateIfVersion("things", "a", next, stored->version);
  86. auto after = f.store.get("things", "a");
  87. check(after && after->expiresAt == later,
  88. "an explicit expiry on update replaces the old one");
  89. }
  90. void test_update_of_a_document_with_no_expiry_stays_permanent() {
  91. Fixture f;
  92. f.store.insert("things", mkDoc("a")); // no TTL
  93. auto stored = f.store.get("things", "a");
  94. Document next = mkDoc("a");
  95. f.store.updateIfVersion("things", "a", next, stored->version);
  96. auto after = f.store.get("things", "a");
  97. check(after && after->expiresAt == 0,
  98. "preserving must not invent an expiry for a document that had none");
  99. }
  100. void test_patch_preserves_expiry() {
  101. Fixture f;
  102. const uint64_t exp = future_ms(3600);
  103. f.store.insert("things", mkDoc("a", exp));
  104. f.store.patchDocument("things", "a", nlohmann::json{{"n", 5}}, "");
  105. auto after = f.store.get("things", "a");
  106. check(after && after->expiresAt == exp, "patch preserves the expiry");
  107. check(after && after->data().value("n", 0) == 5, "and applies the patch");
  108. }
  109. } // namespace
  110. int main() {
  111. std::cout << "=== test_document_ttl ===\n";
  112. test_insert_sets_expiry();
  113. test_update_preserves_expiry();
  114. test_update_can_change_expiry_when_given_one();
  115. test_update_of_a_document_with_no_expiry_stays_permanent();
  116. test_patch_preserves_expiry();
  117. std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
  118. return g_fail == 0 ? 0 : 1;
  119. }