/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "catch.hpp" #include "storage/cache/storage_cache_database.h" #include "storage/storage_encryption.h" #include "storage/storage_encrypted_file.h" #include "base/concurrent_timer.h" #include #include #include #include using namespace Storage::Cache; const auto DisableLimitsTests = false; const auto DisableCompactTests = false; const auto DisableLargeTest = true; const auto key = Storage::EncryptionKey(bytes::make_vector( bytes::make_span("\ abcdefgh01234567abcdefgh01234567abcdefgh01234567abcdefgh01234567\ abcdefgh01234567abcdefgh01234567abcdefgh01234567abcdefgh01234567\ abcdefgh01234567abcdefgh01234567abcdefgh01234567abcdefgh01234567\ abcdefgh01234567abcdefgh01234567abcdefgh01234567abcdefgh01234567\ ").subspan(0, Storage::EncryptionKey::kSize))); const auto name = QString("test.db"); const auto SmallSleep = [] { static auto SleepTime = 0; if (SleepTime > 5000) { return false; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); SleepTime += 10; return true; }; QString GetBinlogPath() { using namespace Storage; QFile versionFile(name + "/version"); while (!versionFile.open(QIODevice::ReadOnly)) { if (!SmallSleep()) { return QString(); } } const auto bytes = versionFile.readAll(); if (bytes.size() != 4) { return QString(); } const auto version = *reinterpret_cast(bytes.data()); return name + '/' + QString::number(version) + "/binlog"; } const auto Test1 = [] { static auto result = QByteArray("testbytetestbyt"); return result; }; const auto Test2 = [] { static auto result = QByteArray("bytetestbytetestb"); return result; }; crl::semaphore Semaphore; auto Result = Error(); const auto GetResult = [](Error error) { Result = error; Semaphore.release(); }; auto Value = QByteArray(); const auto GetValue = [](QByteArray value) { Value = value; Semaphore.release(); }; auto ValueWithTag = Database::TaggedValue(); const auto GetValueWithTag = [](Database::TaggedValue value) { ValueWithTag = value; Semaphore.release(); }; Error Open(Database &db, const Storage::EncryptionKey &key) { db.open(base::duplicate(key), GetResult); Semaphore.acquire(); return Result; } void Close(Database &db) { db.close([&] { Semaphore.release(); }); Semaphore.acquire(); } Error Clear(Database &db) { db.clear(GetResult); Semaphore.acquire(); return Result; } QByteArray Get(Database &db, const Key &key) { db.get(key, GetValue); Semaphore.acquire(); return Value; } Database::TaggedValue GetWithTag(Database &db, const Key &key) { db.getWithTag(key, GetValueWithTag); Semaphore.acquire(); return ValueWithTag; } Error Put(Database &db, const Key &key, QByteArray &&value) { db.put(key, std::move(value), GetResult); Semaphore.acquire(); return Result; } Error Put(Database &db, const Key &key, Database::TaggedValue &&value) { db.put(key, std::move(value), GetResult); Semaphore.acquire(); return Result; } Error PutIfEmpty(Database &db, const Key &key, QByteArray &&value) { db.putIfEmpty(key, std::move(value), GetResult); Semaphore.acquire(); return Result; } Error CopyIfEmpty(Database &db, const Key &from, const Key &to) { db.copyIfEmpty(from, to, GetResult); Semaphore.acquire(); return Result; } Error MoveIfEmpty(Database &db, const Key &from, const Key &to) { db.moveIfEmpty(from, to, GetResult); Semaphore.acquire(); return Result; } void Remove(Database &db, const Key &key) { db.remove(key, [&](Error) { Semaphore.release(); }); Semaphore.acquire(); } Error ClearByTag(Database &db, uint8 tag) { db.clearByTag(tag, GetResult); Semaphore.acquire(); return Result; } const auto Settings = [] { auto result = Database::Settings(); result.trackEstimatedTime = false; result.writeBundleDelay = 1 * crl::time(1000); result.pruneTimeout = 1 * crl::time(1500); result.maxDataSize = 20; return result; }(); const auto AdvanceTime = [](int32 seconds) { std::this_thread::sleep_for(std::chrono::milliseconds(1000) * seconds); }; TEST_CASE("init timers", "[storage_cache_database]") { static auto init = [] { int argc = 0; char **argv = nullptr; static QCoreApplication application(argc, argv); static base::ConcurrentTimerEnvironment environment; return true; }(); } TEST_CASE("compacting db", "[storage_cache_database]") { if (DisableCompactTests || !DisableLargeTest) { return; } const auto write = [](Database &db, uint32 from, uint32 till, QByteArray base) { for (auto i = from; i != till; ++i) { auto value = base; value[0] = char('A') + i; const auto result = Put(db, Key{ i, i + 1 }, std::move(value)); REQUIRE(result.type == Error::Type::None); } }; const auto put = [&](Database &db, uint32 from, uint32 till) { write(db, from, till, Test1()); }; const auto reput = [&](Database &db, uint32 from, uint32 till) { write(db, from, till, Test2()); }; const auto remove = [](Database &db, uint32 from, uint32 till) { for (auto i = from; i != till; ++i) { Remove(db, Key{ i, i + 1 }); } }; const auto get = [](Database &db, uint32 from, uint32 till) { for (auto i = from; i != till; ++i) { db.get(Key{ i, i + 1 }, nullptr); } }; const auto check = [](Database &db, uint32 from, uint32 till, QByteArray base) { for (auto i = from; i != till; ++i) { auto value = base; if (!value.isEmpty()) { value[0] = char('A') + i; } const auto result = Get(db, Key{ i, i + 1 }); REQUIRE((result == value)); } }; SECTION("simple compact with min size") { auto settings = Settings; settings.writeBundleDelay = crl::time(100); settings.readBlockSize = 512; settings.maxBundledRecords = 5; settings.compactAfterExcess = (3 * (16 * 5 + 16) + 15 * 32) / 2; settings.compactAfterFullSize = (sizeof(details::BasicHeader) + 40 * 32) / 2 + settings.compactAfterExcess; Database db(name, settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); put(db, 0, 30); remove(db, 0, 15); put(db, 30, 40); reput(db, 15, 29); AdvanceTime(1); const auto path = GetBinlogPath(); const auto size = QFile(path).size(); reput(db, 29, 30); // starts compactor AdvanceTime(2); REQUIRE(QFile(path).size() < size); remove(db, 30, 35); reput(db, 35, 37); put(db, 15, 20); put(db, 40, 45); const auto fullcheck = [&] { check(db, 0, 15, {}); check(db, 15, 20, Test1()); check(db, 20, 30, Test2()); check(db, 30, 35, {}); check(db, 35, 37, Test2()); check(db, 37, 45, Test1()); }; fullcheck(); Close(db); REQUIRE(Open(db, key).type == Error::Type::None); fullcheck(); Close(db); } SECTION("simple compact without min size") { auto settings = Settings; settings.writeBundleDelay = crl::time(100); settings.readBlockSize = 512; settings.maxBundledRecords = 5; settings.compactAfterExcess = 3 * (16 * 5 + 16) + 15 * 32; Database db(name, settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); put(db, 0, 30); remove(db, 0, 15); put(db, 30, 40); reput(db, 15, 29); AdvanceTime(1); const auto path = GetBinlogPath(); const auto size = QFile(path).size(); reput(db, 29, 30); // starts compactor AdvanceTime(2); REQUIRE(QFile(path).size() < size); remove(db, 30, 35); reput(db, 35, 37); put(db, 15, 20); put(db, 40, 45); const auto fullcheck = [&] { check(db, 0, 15, {}); check(db, 15, 20, Test1()); check(db, 20, 30, Test2()); check(db, 30, 35, {}); check(db, 35, 37, Test2()); check(db, 37, 45, Test1()); }; fullcheck(); Close(db); REQUIRE(Open(db, key).type == Error::Type::None); fullcheck(); Close(db); } SECTION("double compact") { auto settings = Settings; settings.writeBundleDelay = crl::time(100); settings.readBlockSize = 512; settings.maxBundledRecords = 5; settings.compactAfterExcess = 3 * (16 * 5 + 16) + 15 * 32; Database db(name, settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); put(db, 0, 30); remove(db, 0, 15); reput(db, 15, 29); AdvanceTime(1); const auto path = GetBinlogPath(); const auto size1 = QFile(path).size(); reput(db, 29, 30); // starts compactor AdvanceTime(2); REQUIRE(QFile(path).size() < size1); put(db, 30, 45); remove(db, 20, 35); put(db, 15, 20); reput(db, 35, 44); const auto size2 = QFile(path).size(); reput(db, 44, 45); // starts compactor AdvanceTime(2); const auto after = QFile(path).size(); REQUIRE(after < size1); REQUIRE(after < size2); const auto fullcheck = [&] { check(db, 0, 15, {}); check(db, 15, 20, Test1()); check(db, 20, 35, {}); check(db, 35, 45, Test2()); }; fullcheck(); Close(db); REQUIRE(Open(db, key).type == Error::Type::None); fullcheck(); Close(db); } SECTION("time tracking compact") { auto settings = Settings; settings.writeBundleDelay = crl::time(100); settings.trackEstimatedTime = true; settings.readBlockSize = 512; settings.maxBundledRecords = 5; settings.compactAfterExcess = 6 * (16 * 5 + 16) + 3 * (16 * 5 + 16) + 15 * 48 + 3 * (16 * 5 + 16) + (16 * 1 + 16); Database db(name, settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); put(db, 0, 30); get(db, 0, 30); //AdvanceTime(1); get's will be written instantly becase !(30 % 5) remove(db, 0, 15); reput(db, 15, 30); get(db, 0, 30); AdvanceTime(1); const auto path = GetBinlogPath(); const auto size = QFile(path).size(); get(db, 29, 30); // starts compactor delayed AdvanceTime(2); REQUIRE(QFile(path).size() < size); const auto fullcheck = [&] { check(db, 15, 30, Test2()); }; fullcheck(); Close(db); REQUIRE(Open(db, key).type == Error::Type::None); fullcheck(); Close(db); } } TEST_CASE("encrypted cache db", "[storage_cache_database]") { if (!DisableLargeTest) { return; } SECTION("writing db") { Database db(name, Settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); REQUIRE(Put(db, Key{ 0, 1 }, Test2()).type == Error::Type::None); REQUIRE(Put(db, Key{ 0, 1 }, Database::TaggedValue(Test1(), 1)).type == Error::Type::None); REQUIRE(PutIfEmpty(db, Key{ 0, 2 }, Test2()).type == Error::Type::None); REQUIRE(PutIfEmpty(db, Key{ 0, 2 }, Test1()).type == Error::Type::None); REQUIRE(CopyIfEmpty(db, Key{ 0, 1 }, Key{ 2, 0 }).type == Error::Type::None); REQUIRE(CopyIfEmpty(db, Key{ 0, 2 }, Key{ 2, 0 }).type == Error::Type::None); REQUIRE(Put(db, Key{ 0, 3 }, Test1()).type == Error::Type::None); REQUIRE(MoveIfEmpty(db, Key{ 0, 3 }, Key{ 3, 0 }).type == Error::Type::None); REQUIRE(MoveIfEmpty(db, Key{ 0, 2 }, Key{ 3, 0 }).type == Error::Type::None); Close(db); } SECTION("reading and writing db") { Database db(name, Settings); REQUIRE(Open(db, key).type == Error::Type::None); REQUIRE((Get(db, Key{ 0, 1 }) == Test1())); const auto withTag1 = GetWithTag(db, Key{ 0, 1 }); REQUIRE(((withTag1.bytes == Test1()) && (withTag1.tag == 1))); REQUIRE(Put(db, Key{ 1, 0 }, Test2()).type == Error::Type::None); const auto withTag2 = GetWithTag(db, Key{ 1, 0 }); REQUIRE(((withTag2.bytes == Test2()) && (withTag2.tag == 0))); REQUIRE(Get(db, Key{ 1, 1 }).isEmpty()); REQUIRE((Get(db, Key{ 0, 2 }) == Test2())); REQUIRE((Get(db, Key{ 2, 0 }) == Test1())); REQUIRE(Get(db, Key{ 0, 3 }).isEmpty()); REQUIRE((Get(db, Key{ 3, 0 }) == Test1())); REQUIRE(Put(db, Key{ 5, 1 }, Database::TaggedValue(Test1(), 1)).type == Error::Type::None); REQUIRE(Put(db, Key{ 6, 1 }, Database::TaggedValue(Test2(), 1)).type == Error::Type::None); REQUIRE(Put(db, Key{ 5, 2 }, Database::TaggedValue(Test1(), 2)).type == Error::Type::None); REQUIRE(Put(db, Key{ 6, 2 }, Database::TaggedValue(Test2(), 2)).type == Error::Type::None); REQUIRE(Put(db, Key{ 5, 3 }, Database::TaggedValue(Test1(), 3)).type == Error::Type::None); REQUIRE(Put(db, Key{ 6, 3 }, Database::TaggedValue(Test2(), 3)).type == Error::Type::None); Close(db); } SECTION("reading db") { Database db(name, Settings); REQUIRE(Open(db, key).type == Error::Type::None); REQUIRE((Get(db, Key{ 0, 1 }) == Test1())); REQUIRE((Get(db, Key{ 1, 0 }) == Test2())); Close(db); } SECTION("deleting in db by tag") { Database db(name, Settings); REQUIRE(Open(db, key).type == Error::Type::None); REQUIRE(ClearByTag(db, 2).type == Error::Type::None); REQUIRE((Get(db, Key{ 1, 0 }) == Test2())); const auto withTag1 = GetWithTag(db, Key{ 5, 1 }); REQUIRE(((withTag1.bytes == Test1()) && (withTag1.tag == 1))); const auto withTag2 = GetWithTag(db, Key{ 6, 1 }); REQUIRE(((withTag2.bytes == Test2()) && (withTag2.tag == 1))); REQUIRE(Get(db, Key{ 5, 2 }).isEmpty()); REQUIRE(Get(db, Key{ 6, 2 }).isEmpty()); const auto withTag3 = GetWithTag(db, Key{ 5, 3 }); REQUIRE(((withTag3.bytes == Test1()) && (withTag3.tag == 3))); const auto withTag4 = GetWithTag(db, Key{ 6, 3 }); REQUIRE(((withTag4.bytes == Test2()) && (withTag4.tag == 3))); Close(db); } SECTION("overwriting values") { Database db(name, Settings); REQUIRE(Open(db, key).type == Error::Type::None); const auto path = GetBinlogPath(); REQUIRE((Get(db, Key{ 0, 1 }) == Test1())); const auto size = QFile(path).size(); REQUIRE(Put(db, Key{ 0, 1 }, Test2()).type == Error::Type::None); const auto next = QFile(path).size(); REQUIRE(next > size); REQUIRE((Get(db, Key{ 0, 1 }) == Test2())); REQUIRE(Put(db, Key{ 0, 1 }, Test2()).type == Error::Type::None); const auto same = QFile(path).size(); REQUIRE(same == next); Close(db); } SECTION("reading db in many chunks") { auto settings = Settings; settings.readBlockSize = 512; settings.maxBundledRecords = 5; settings.trackEstimatedTime = true; Database db(name, settings); const auto count = 30U; REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); for (auto i = 0U; i != count; ++i) { auto value = Test1(); value[0] = char('A') + i; const auto result = Put(db, Key{ i, i * 2 }, std::move(value)); REQUIRE(result.type == Error::Type::None); } Close(db); REQUIRE(Open(db, key).type == Error::Type::None); for (auto i = 0U; i != count; ++i) { auto value = Test1(); value[0] = char('A') + i; REQUIRE((Get(db, Key{ i, i * 2 }) == value)); } Close(db); } } TEST_CASE("cache db remove", "[storage_cache_database]") { if (!DisableLargeTest) { return; } SECTION("db remove deletes value") { Database db(name, Settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); REQUIRE(Put(db, Key{ 0, 1 }, Test1()).type == Error::Type::None); REQUIRE(Put(db, Key{ 1, 0 }, Test2()).type == Error::Type::None); Remove(db, Key{ 0, 1 }); REQUIRE(Get(db, Key{ 0, 1 }).isEmpty()); REQUIRE((Get(db, Key{ 1, 0 }) == Test2())); Close(db); } SECTION("db remove deletes value permanently") { Database db(name, Settings); REQUIRE(Open(db, key).type == Error::Type::None); REQUIRE(Get(db, Key{ 0, 1 }).isEmpty()); REQUIRE((Get(db, Key{ 1, 0 }) == Test2())); Close(db); } } TEST_CASE("cache db bundled actions", "[storage_cache_database]") { if (!DisableLargeTest) { return; } SECTION("db touched written lazily") { auto settings = Settings; settings.trackEstimatedTime = true; Database db(name, settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); const auto path = GetBinlogPath(); REQUIRE(Put(db, Key{ 0, 1 }, Test1()).type == Error::Type::None); const auto size = QFile(path).size(); REQUIRE((Get(db, Key{ 0, 1 }) == Test1())); REQUIRE(QFile(path).size() == size); AdvanceTime(2); Get(db, Key{ 0, 1 }); REQUIRE(QFile(path).size() > size); Close(db); } SECTION("db touched written on close") { auto settings = Settings; settings.trackEstimatedTime = true; Database db(name, settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); const auto path = GetBinlogPath(); REQUIRE(Put(db, Key{ 0, 1 }, Test1()).type == Error::Type::None); const auto size = QFile(path).size(); REQUIRE((Get(db, Key{ 0, 1 }) == Test1())); REQUIRE(QFile(path).size() == size); Close(db); REQUIRE(QFile(path).size() > size); } SECTION("db remove written lazily") { Database db(name, Settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); const auto path = GetBinlogPath(); REQUIRE(Put(db, Key{ 0, 1 }, Test1()).type == Error::Type::None); const auto size = QFile(path).size(); Remove(db, Key{ 0, 1 }); REQUIRE(QFile(path).size() == size); AdvanceTime(2); REQUIRE(QFile(path).size() > size); Close(db); } SECTION("db remove written on close") { Database db(name, Settings); REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); const auto path = GetBinlogPath(); REQUIRE(Put(db, Key{ 0, 1 }, Test1()).type == Error::Type::None); const auto size = QFile(path).size(); Remove(db, Key{ 0, 1 }); REQUIRE(QFile(path).size() == size); Close(db); REQUIRE(QFile(path).size() > size); } } TEST_CASE("cache db limits", "[storage_cache_database]") { if (DisableLimitsTests || !DisableLargeTest) { return; } SECTION("db both limit") { auto settings = Settings; settings.trackEstimatedTime = true; settings.totalSizeLimit = 17 * 3 + 1; settings.totalTimeLimit = 4; Database db(name, settings); db.clear(nullptr); db.open(base::duplicate(key), nullptr); db.put(Key{ 0, 1 }, Test1(), nullptr); db.put(Key{ 1, 0 }, Test2(), nullptr); AdvanceTime(2); db.get(Key{ 1, 0 }, nullptr); AdvanceTime(3); db.put(Key{ 1, 1 }, Test1(), nullptr); db.put(Key{ 2, 0 }, Test2(), nullptr); db.put(Key{ 0, 2 }, Test1(), nullptr); AdvanceTime(2); REQUIRE(Get(db, Key{ 0, 1 }).isEmpty()); REQUIRE(Get(db, Key{ 1, 0 }).isEmpty()); REQUIRE((Get(db, Key{ 1, 1 }) == Test1())); REQUIRE((Get(db, Key{ 2, 0 }) == Test2())); REQUIRE((Get(db, Key{ 0, 2 }) == Test1())); Close(db); } SECTION("db size limit") { auto settings = Settings; settings.trackEstimatedTime = true; settings.totalSizeLimit = 17 * 3 + 1; Database db(name, settings); db.clear(nullptr); db.open(base::duplicate(key), nullptr); db.put(Key{ 0, 1 }, Test1(), nullptr); AdvanceTime(2); db.put(Key{ 1, 0 }, Test2(), nullptr); AdvanceTime(2); db.put(Key{ 1, 1 }, Test1(), nullptr); db.get(Key{ 0, 1 }, nullptr); AdvanceTime(2); db.put(Key{ 2, 0 }, Test2(), nullptr); // Removing { 1, 0 } will be scheduled. REQUIRE((Get(db, Key{ 0, 1 }) == Test1())); REQUIRE((Get(db, Key{ 1, 1 }) == Test1())); REQUIRE((Get(db, Key{ 2, 0 }) == Test2())); AdvanceTime(2); // Removing { 1, 0 } performed. REQUIRE(Get(db, Key{ 1, 0 }).isEmpty()); REQUIRE((Get(db, Key{ 1, 1 }) == Test1())); db.put(Key{ 0, 2 }, Test1(), nullptr); REQUIRE(Put(db, Key{ 2, 2 }, Test2()).type == Error::Type::None); // Removing { 0, 1 } and { 2, 0 } will be scheduled. AdvanceTime(2); // Removing { 0, 1 } and { 2, 0 } performed. REQUIRE(Get(db, Key{ 0, 1 }).isEmpty()); REQUIRE(Get(db, Key{ 2, 0 }).isEmpty()); REQUIRE((Get(db, Key{ 1, 1 }) == Test1())); REQUIRE((Get(db, Key{ 0, 2 }) == Test1())); REQUIRE((Get(db, Key{ 2, 2 }) == Test2())); Close(db); } SECTION("db time limit") { auto settings = Settings; settings.trackEstimatedTime = true; settings.totalTimeLimit = 3; Database db(name, settings); db.clear(nullptr); db.open(base::duplicate(key), nullptr); db.put(Key{ 0, 1 }, Test1(), nullptr); db.put(Key{ 1, 0 }, Test2(), nullptr); db.put(Key{ 1, 1 }, Test1(), nullptr); db.put(Key{ 2, 0 }, Test2(), nullptr); AdvanceTime(1); db.get(Key{ 1, 0 }, nullptr); db.get(Key{ 1, 1 }, nullptr); AdvanceTime(1); db.get(Key{ 1, 0 }, nullptr); db.get(Key{ 0, 1 }, nullptr); AdvanceTime(1); db.get(Key{ 1, 0 }, nullptr); db.get(Key{ 0, 1 }, nullptr); AdvanceTime(3); REQUIRE(Get(db, Key{ 2, 0 }).isEmpty()); REQUIRE(Get(db, Key{ 1, 1 }).isEmpty()); REQUIRE((Get(db, Key{ 1, 0 }) == Test2())); REQUIRE((Get(db, Key{ 0, 1 }) == Test1())); Close(db); } } TEST_CASE("large db", "[storage_cache_database]") { if (DisableLargeTest) { return; } SECTION("time tracking large db") { auto settings = Database::Settings(); settings.writeBundleDelay = crl::time(1000); settings.maxDataSize = 20; settings.totalSizeLimit = 1024 * 1024; settings.totalTimeLimit = 120; settings.pruneTimeout = crl::time(1500); settings.compactAfterExcess = 1024 * 1024; settings.trackEstimatedTime = true; Database db(name, settings); //REQUIRE(Clear(db).type == Error::Type::None); REQUIRE(Open(db, key).type == Error::Type::None); const auto key = [](int index) { return Key{ uint64(index) * 2, (uint64(index) << 32) + 3 }; }; const auto kWriteRecords = 100 * 1024; for (auto i = 0; i != kWriteRecords; ++i) { db.put(key(i), Test1(), nullptr); const auto j = i ? (rand() % i) : 0; if (i % 1024 == 1023) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); Get(db, key(j)); } else { db.get(key(j), nullptr); } } Close(db); } }