| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141 |
- // v2.8.0 — document TTL lifecycle across write paths.
- //
- // The bug this pins down: `update()` WIPED a document's expiry.
- //
- // updateIfVersion built a fresh Document from the caller's request, and
- // UpdateRequest has no ttl field, so the incoming expiresAt was always 0. It
- // deliberately carried createdAt and createdBy across from the stored document
- // but not expiresAt - so any TTL'd document that was ever updated became
- // permanent. `patch()` mutates the stored document in place and therefore always
- // preserved it, which is why the two paths disagreed.
- //
- // This matters most where it is least visible: a workflow execution written with
- // a TTL and then updated as its status changes (the normal case) silently lost
- // its expiry and accumulated forever.
- #include <atomic>
- #include <chrono>
- #include <filesystem>
- #include <iostream>
- #include <string>
- #include <unistd.h>
- #include <nlohmann/json.hpp>
- #include "memory_store.hpp"
- using namespace smartbotic::database;
- namespace {
- int g_pass = 0;
- int g_fail = 0;
- void check(bool cond, const char* msg) {
- if (cond) { ++g_pass; }
- else { ++g_fail; std::cerr << "FAIL: " << msg << "\n"; }
- }
- struct Fixture {
- MemoryStore store;
- Fixture() : store(MemoryStore::Config{}) {
- store.start();
- CollectionOptions o;
- store.createCollection("things", o);
- }
- ~Fixture() { store.stop(); }
- };
- Document mkDoc(const std::string& id, uint64_t expiresAt = 0) {
- Document d;
- d.id = id;
- d.collection = "things";
- d.expiresAt = expiresAt;
- d.set_data(nlohmann::json{{"n", 1}});
- return d;
- }
- uint64_t future_ms(uint64_t secs) {
- const auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
- std::chrono::system_clock::now().time_since_epoch()).count();
- return static_cast<uint64_t>(now) + secs * 1000ULL;
- }
- // -------------------------------------------------------------------------
- void test_insert_sets_expiry() {
- Fixture f;
- const uint64_t exp = future_ms(3600);
- f.store.insert("things", mkDoc("a", exp));
- auto got = f.store.get("things", "a");
- check(got.has_value(), "inserted");
- check(got && got->expiresAt == exp, "insert stores the expiry it was given");
- }
- // THE regression. An update with no TTL must not silently make the document
- // permanent.
- void test_update_preserves_expiry() {
- Fixture f;
- const uint64_t exp = future_ms(3600);
- f.store.insert("things", mkDoc("a", exp));
- auto stored = f.store.get("things", "a");
- Document next = mkDoc("a"); // no expiry, exactly as UpdateRequest arrives
- next.set_data(nlohmann::json{{"n", 2}});
- f.store.updateIfVersion("things", "a", next, stored->version);
- auto after = f.store.get("things", "a");
- check(after.has_value(), "still present after update");
- check(after && after->data().value("n", 0) == 2, "the update applied");
- check(after && after->expiresAt == exp,
- "update MUST preserve the existing expiry - wiping it makes a TTL'd "
- "document permanent");
- }
- void test_update_can_change_expiry_when_given_one() {
- Fixture f;
- f.store.insert("things", mkDoc("a", future_ms(60)));
- auto stored = f.store.get("things", "a");
- const uint64_t later = future_ms(7200);
- Document next = mkDoc("a", later); // explicit new expiry
- f.store.updateIfVersion("things", "a", next, stored->version);
- auto after = f.store.get("things", "a");
- check(after && after->expiresAt == later,
- "an explicit expiry on update replaces the old one");
- }
- void test_update_of_a_document_with_no_expiry_stays_permanent() {
- Fixture f;
- f.store.insert("things", mkDoc("a")); // no TTL
- auto stored = f.store.get("things", "a");
- Document next = mkDoc("a");
- f.store.updateIfVersion("things", "a", next, stored->version);
- auto after = f.store.get("things", "a");
- check(after && after->expiresAt == 0,
- "preserving must not invent an expiry for a document that had none");
- }
- void test_patch_preserves_expiry() {
- Fixture f;
- const uint64_t exp = future_ms(3600);
- f.store.insert("things", mkDoc("a", exp));
- f.store.patchDocument("things", "a", nlohmann::json{{"n", 5}}, "");
- auto after = f.store.get("things", "a");
- check(after && after->expiresAt == exp, "patch preserves the expiry");
- check(after && after->data().value("n", 0) == 5, "and applies the patch");
- }
- } // namespace
- int main() {
- std::cout << "=== test_document_ttl ===\n";
- test_insert_sets_expiry();
- test_update_preserves_expiry();
- test_update_can_change_expiry_when_given_one();
- test_update_of_a_document_with_no_expiry_stays_permanent();
- test_patch_preserves_expiry();
- std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
- return g_fail == 0 ? 0 : 1;
- }
|