Add a new test that sets up a CQL environment with a very small prepared statements cache. The test reproduces a scenario described in #10440, where a privileged section of prepared statement cache gets large and that could possibly starve the unprivileged section, making it impossible to execute BATCH statements. Additionally, at the end of the test, prepared statements/"simulated batches" with prepared statements are executed a random number of times, stressing the cache. To create a CQL environment with small prepared cache, cql_test_config is extended to allow setting custom memory_config value.
776 lines
31 KiB
C++
776 lines
31 KiB
C++
/*
|
|
* Copyright (C) 2017-present ScyllaDB
|
|
*/
|
|
|
|
/*
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
#include <boost/test/unit_test.hpp>
|
|
#include "utils/loading_shared_values.hh"
|
|
#include "utils/loading_cache.hh"
|
|
#include <seastar/core/aligned_buffer.hh>
|
|
#include <seastar/core/file.hh>
|
|
#include <seastar/core/thread.hh>
|
|
#include <seastar/core/sstring.hh>
|
|
#include <seastar/core/seastar.hh>
|
|
#include <seastar/core/sleep.hh>
|
|
#include <seastar/util/defer.hh>
|
|
|
|
|
|
#include "seastarx.hh"
|
|
#include "test/lib/eventually.hh"
|
|
#include <seastar/testing/test_case.hh>
|
|
#include <seastar/testing/thread_test_case.hh>
|
|
#include "test/lib/tmpdir.hh"
|
|
#include "test/lib/log.hh"
|
|
#include "test/lib/random_utils.hh"
|
|
#include "test/lib/cql_test_env.hh"
|
|
|
|
#include <vector>
|
|
#include <numeric>
|
|
#include <random>
|
|
|
|
/// Get a random integer in the [0, max) range.
|
|
/// \param max bound of the random value range
|
|
/// \return The uniformly distributed random integer from the [0, \ref max) range.
|
|
static int rand_int(int max) {
|
|
return tests::random::get_int(max - 1);
|
|
}
|
|
|
|
static const sstring test_file_name = "loading_cache_test.txt";
|
|
static const sstring test_string = "1";
|
|
static bool file_prepared = false;
|
|
static constexpr int num_loaders = 1000;
|
|
|
|
static thread_local int load_count;
|
|
static const tmpdir& get_tmpdir() {
|
|
static thread_local tmpdir tmp;
|
|
return tmp;
|
|
}
|
|
|
|
static future<> prepare() {
|
|
if (file_prepared) {
|
|
return make_ready_future<>();
|
|
}
|
|
|
|
return open_file_dma((get_tmpdir().path() / test_file_name.c_str()).c_str(), open_flags::create | open_flags::wo).then([] (file f) {
|
|
return do_with(std::move(f), [] (file& f) {
|
|
auto size = test_string.size() + 1;
|
|
auto aligned_size = align_up(size, f.disk_write_dma_alignment());
|
|
auto buf = allocate_aligned_buffer<char>(aligned_size, f.disk_write_dma_alignment());
|
|
auto wbuf = buf.get();
|
|
std::copy_n(test_string.c_str(), size, wbuf);
|
|
return f.dma_write(0, wbuf, aligned_size).then([aligned_size, buf = std::move(buf)] (size_t s) {
|
|
BOOST_REQUIRE_EQUAL(s, aligned_size);
|
|
file_prepared = true;
|
|
}).finally([&f] () mutable {
|
|
return f.close();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
static future<sstring> loader(const int& k) {
|
|
return open_file_dma((get_tmpdir().path() / test_file_name.c_str()).c_str(), open_flags::ro).then([] (file f) -> future<sstring> {
|
|
return do_with(std::move(f), [] (file& f) -> future<sstring> {
|
|
auto size = align_up(test_string.size() + 1, f.disk_read_dma_alignment());
|
|
return f.dma_read_exactly<char>(0, size).then([] (auto buf) {
|
|
sstring str(buf.get());
|
|
BOOST_REQUIRE_EQUAL(str, test_string);
|
|
++load_count;
|
|
return make_ready_future<sstring>(std::move(str));
|
|
}).finally([&f] () mutable {
|
|
return f.close();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_shared_values_parallel_loading_same_key) {
|
|
return seastar::async([] {
|
|
std::vector<int> ivec(num_loaders);
|
|
load_count = 0;
|
|
utils::loading_shared_values<int, sstring> shared_values;
|
|
std::list<typename utils::loading_shared_values<int, sstring>::entry_ptr> anchors_list;
|
|
|
|
prepare().get();
|
|
|
|
std::fill(ivec.begin(), ivec.end(), 0);
|
|
|
|
parallel_for_each(ivec, [&] (int& k) {
|
|
return shared_values.get_or_load(k, loader).then([&] (auto entry_ptr) {
|
|
anchors_list.emplace_back(std::move(entry_ptr));
|
|
});
|
|
}).get();
|
|
|
|
// "loader" must be called exactly once
|
|
BOOST_REQUIRE_EQUAL(load_count, 1);
|
|
BOOST_REQUIRE_EQUAL(shared_values.size(), 1);
|
|
anchors_list.clear();
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_shared_values_parallel_loading_different_keys) {
|
|
return seastar::async([] {
|
|
std::vector<int> ivec(num_loaders);
|
|
load_count = 0;
|
|
utils::loading_shared_values<int, sstring> shared_values;
|
|
std::list<typename utils::loading_shared_values<int, sstring>::entry_ptr> anchors_list;
|
|
|
|
prepare().get();
|
|
|
|
std::iota(ivec.begin(), ivec.end(), 0);
|
|
|
|
parallel_for_each(ivec, [&] (int& k) {
|
|
return shared_values.get_or_load(k, loader).then([&] (auto entry_ptr) {
|
|
anchors_list.emplace_back(std::move(entry_ptr));
|
|
});
|
|
}).get();
|
|
|
|
// "loader" must be called once for each key
|
|
BOOST_REQUIRE_EQUAL(load_count, num_loaders);
|
|
BOOST_REQUIRE_EQUAL(shared_values.size(), num_loaders);
|
|
anchors_list.clear();
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_shared_values_rehash) {
|
|
return seastar::async([] {
|
|
std::vector<int> ivec(num_loaders);
|
|
load_count = 0;
|
|
utils::loading_shared_values<int, sstring> shared_values;
|
|
std::list<typename utils::loading_shared_values<int, sstring>::entry_ptr> anchors_list;
|
|
|
|
prepare().get();
|
|
|
|
std::iota(ivec.begin(), ivec.end(), 0);
|
|
|
|
// verify that load factor is always in the (0.25, 0.75) range
|
|
for (int k = 0; k < num_loaders; ++k) {
|
|
shared_values.get_or_load(k, loader).then([&] (auto entry_ptr) {
|
|
anchors_list.emplace_back(std::move(entry_ptr));
|
|
}).get();
|
|
BOOST_REQUIRE_LE(shared_values.size(), 3 * shared_values.buckets_count() / 4);
|
|
}
|
|
|
|
BOOST_REQUIRE_GE(shared_values.size(), shared_values.buckets_count() / 4);
|
|
|
|
// minimum buckets count (by default) is 16, so don't check for less than 4 elements
|
|
for (int k = 0; k < num_loaders - 4; ++k) {
|
|
anchors_list.pop_back();
|
|
shared_values.rehash();
|
|
BOOST_REQUIRE_GE(shared_values.size(), shared_values.buckets_count() / 4);
|
|
}
|
|
|
|
anchors_list.clear();
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_shared_values_parallel_loading_explicit_eviction) {
|
|
return seastar::async([] {
|
|
std::vector<int> ivec(num_loaders);
|
|
load_count = 0;
|
|
utils::loading_shared_values<int, sstring> shared_values;
|
|
std::vector<typename utils::loading_shared_values<int, sstring>::entry_ptr> anchors_vec(num_loaders);
|
|
|
|
prepare().get();
|
|
|
|
std::iota(ivec.begin(), ivec.end(), 0);
|
|
|
|
parallel_for_each(ivec, [&] (int& k) {
|
|
return shared_values.get_or_load(k, loader).then([&] (auto entry_ptr) {
|
|
anchors_vec[k] = std::move(entry_ptr);
|
|
});
|
|
}).get();
|
|
|
|
int rand_key = rand_int(num_loaders);
|
|
BOOST_REQUIRE(shared_values.find(rand_key) != nullptr);
|
|
anchors_vec[rand_key] = nullptr;
|
|
BOOST_REQUIRE_MESSAGE(shared_values.find(rand_key) == nullptr, format("explicit removal for key {} failed", rand_key));
|
|
anchors_vec.clear();
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_loading_same_key) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
std::vector<int> ivec(num_loaders);
|
|
load_count = 0;
|
|
utils::loading_cache<int, sstring> loading_cache(num_loaders, 1s, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
std::fill(ivec.begin(), ivec.end(), 0);
|
|
|
|
parallel_for_each(ivec, [&] (int& k) {
|
|
return loading_cache.get_ptr(k, loader).discard_result();
|
|
}).get();
|
|
|
|
// "loader" must be called exactly once
|
|
BOOST_REQUIRE_EQUAL(load_count, 1);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 1);
|
|
});
|
|
}
|
|
|
|
SEASTAR_THREAD_TEST_CASE(test_loading_cache_removing_key) {
|
|
using namespace std::chrono;
|
|
load_count = 0;
|
|
utils::loading_cache<int, sstring> loading_cache(num_loaders, 100s, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
loading_cache.get_ptr(0, loader).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(load_count, 1);
|
|
BOOST_REQUIRE(loading_cache.find(0) != nullptr);
|
|
|
|
loading_cache.remove(0);
|
|
BOOST_REQUIRE(loading_cache.find(0) == nullptr);
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_loading_different_keys) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
std::vector<int> ivec(num_loaders);
|
|
load_count = 0;
|
|
utils::loading_cache<int, sstring> loading_cache(num_loaders, 1h, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
std::iota(ivec.begin(), ivec.end(), 0);
|
|
|
|
parallel_for_each(ivec, [&] (int& k) {
|
|
return loading_cache.get_ptr(k, loader).discard_result();
|
|
}).get();
|
|
|
|
BOOST_REQUIRE_EQUAL(load_count, num_loaders);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), num_loaders);
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_loading_expiry_eviction) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
utils::loading_cache<int, sstring, 1> loading_cache(num_loaders, 20ms, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
loading_cache.get_ptr(0, loader).discard_result().get();
|
|
|
|
// Check unprivileged section eviction
|
|
BOOST_REQUIRE(loading_cache.size() == 1);
|
|
sleep(20ms).get();
|
|
REQUIRE_EVENTUALLY_EQUAL(loading_cache.size(), 0);
|
|
|
|
// Check privileged section eviction
|
|
loading_cache.get_ptr(0, loader).discard_result().get();
|
|
BOOST_REQUIRE(loading_cache.find(0) != nullptr);
|
|
|
|
sleep(20ms).get();
|
|
REQUIRE_EVENTUALLY_EQUAL(loading_cache.size(), 0);
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_loading_expiry_reset_on_sync_op) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
utils::loading_cache<int, sstring> loading_cache(num_loaders, 30ms, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
loading_cache.get_ptr(0, loader).discard_result().get();
|
|
auto vp = loading_cache.find(0);
|
|
auto load_time = steady_clock::now();
|
|
|
|
// Check that the expiration timer is reset every time we call a find()
|
|
for (int i = 0; i < 10; ++i) {
|
|
// seastar::lowres_clock has 10ms resolution. This means that we should use 10ms threshold to compensate.
|
|
if (steady_clock::now() <= load_time + 20ms) {
|
|
BOOST_REQUIRE(vp != nullptr);
|
|
} else {
|
|
// If there was a delay and we weren't able to execute the next loop iteration during 20ms let's repopulate
|
|
// the cache.
|
|
loading_cache.get_ptr(0, loader).discard_result().get();
|
|
BOOST_TEST_MESSAGE("Test " << i << " was skipped. Repopulating...");
|
|
}
|
|
vp = loading_cache.find(0);
|
|
load_time = steady_clock::now();
|
|
sleep(10ms).get();
|
|
}
|
|
|
|
sleep(30ms).get();
|
|
REQUIRE_EVENTUALLY_EQUAL(loading_cache.size(), 0);
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_move_item_to_mru_list_front_on_sync_op) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
utils::loading_cache<int, sstring> loading_cache(2, 1h, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
for (int i = 0; i < 2; ++i) {
|
|
loading_cache.get_ptr(i, loader).discard_result().get();
|
|
}
|
|
|
|
auto vp0 = loading_cache.find(0);
|
|
BOOST_REQUIRE(vp0 != nullptr);
|
|
|
|
loading_cache.get_ptr(2, loader).discard_result().get();
|
|
|
|
// "0" should be at the beginning of the list and "1" right after it before we try to add a new entry to the
|
|
// cache ("2"). And hence "1" should get evicted.
|
|
vp0 = loading_cache.find(0);
|
|
auto vp1 = loading_cache.find(1);
|
|
BOOST_REQUIRE(vp0 != nullptr);
|
|
BOOST_REQUIRE(vp1 == nullptr);
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_loading_reloading_privileged_gen) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
load_count = 0;
|
|
utils::loading_cache<int, sstring, 1, utils::loading_cache_reload_enabled::yes> loading_cache(num_loaders, 100ms, 20ms, testlog, loader);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
prepare().get();
|
|
// Push the entry into the privileged section. Make sure it's being reloaded.
|
|
loading_cache.get_ptr(0).discard_result().get();
|
|
loading_cache.get_ptr(0).discard_result().get();
|
|
sleep(60ms).get();
|
|
BOOST_REQUIRE(eventually_true([&] { return load_count >= 3; }));
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_loading_reloading_unprivileged) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
load_count = 0;
|
|
utils::loading_cache<int, sstring, 1, utils::loading_cache_reload_enabled::yes> loading_cache(num_loaders, 100ms, 20ms, testlog, loader);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
prepare().get();
|
|
// Load one entry into the unprivileged section.
|
|
// Make sure it's reloaded.
|
|
loading_cache.get_ptr(0).discard_result().get();
|
|
sleep(60ms).get();
|
|
BOOST_REQUIRE(eventually_true([&] { return load_count >= 2; }));
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_max_size_eviction) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
load_count = 0;
|
|
utils::loading_cache<int, sstring> loading_cache(1, 1s, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
for (int i = 0; i < num_loaders; ++i) {
|
|
loading_cache.get_ptr(i % 2, loader).discard_result().get();
|
|
}
|
|
|
|
BOOST_REQUIRE_EQUAL(load_count, num_loaders);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 1);
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_max_size_eviction_unprivileged_first) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
load_count = 0;
|
|
utils::loading_cache<int, sstring, 1> loading_cache(4, 1h, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
// Touch the value with the key "-1" twice
|
|
loading_cache.get_ptr(-1, loader).discard_result().get();
|
|
loading_cache.find(-1);
|
|
|
|
for (int i = 0; i < num_loaders; ++i) {
|
|
loading_cache.get_ptr(i, loader).discard_result().get();
|
|
}
|
|
|
|
BOOST_REQUIRE_EQUAL(load_count, num_loaders + 1);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 4);
|
|
// Make sure that the value we touched twice is still in the cache
|
|
BOOST_REQUIRE(loading_cache.find(-1) != nullptr);
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_eviction_unprivileged) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
load_count = 0;
|
|
utils::loading_cache<int, sstring, 1> loading_cache(4, 10ms, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
// Touch the value with the key "-1" twice
|
|
loading_cache.get_ptr(-1, loader).discard_result().get();
|
|
loading_cache.find(-1);
|
|
|
|
for (int i = 0; i < num_loaders; ++i) {
|
|
loading_cache.get_ptr(i, loader).discard_result().get();
|
|
}
|
|
|
|
// Make sure that the value we touched twice is eventually evicted
|
|
REQUIRE_EVENTUALLY_EQUAL(loading_cache.find(-1), nullptr);
|
|
REQUIRE_EVENTUALLY_EQUAL(loading_cache.size(), 0);
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_eviction_unprivileged_minimum_size) {
|
|
return seastar::async([] {
|
|
// Test that unprivileged section is not starved.
|
|
//
|
|
// This scenario is tested: cache max_size is 50 and there are 49 entries in
|
|
// privileged section. After adding 5 elements (that go to unprivileged
|
|
// section) all of them should stay in unprivileged section and elements
|
|
// in privileged section should get evicted.
|
|
//
|
|
// Wrong handling of this situation caused problems with BATCH statements
|
|
// where all prepared statements in the batch have to stay in cache at
|
|
// the same time for the batch to correctly execute.
|
|
|
|
using namespace std::chrono;
|
|
utils::loading_cache<int, sstring, 1> loading_cache(50, 1h, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
// Add 49 elements to privileged section
|
|
for (int i = 0; i < 49; i++) {
|
|
// Touch the value with the key "i" twice
|
|
loading_cache.get_ptr(i, loader).discard_result().get();
|
|
loading_cache.find(i);
|
|
}
|
|
|
|
// Add 5 elements to unprivileged section
|
|
for (int i = 50; i < 55; i++) {
|
|
loading_cache.get_ptr(i, loader).discard_result().get();
|
|
}
|
|
|
|
// Make sure that none of 5 elements were evicted
|
|
for (int i = 50; i < 55; i++) {
|
|
BOOST_REQUIRE(loading_cache.find(i) != nullptr);
|
|
}
|
|
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 50);
|
|
});
|
|
}
|
|
|
|
struct sstring_length_entry_size {
|
|
size_t operator()(const sstring& val) {
|
|
return val.size();
|
|
}
|
|
};
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_section_size_correctly_calculated) {
|
|
return seastar::async([] {
|
|
auto load_len1 = [] (const int& key) { return make_ready_future<sstring>(tests::random::get_sstring(1)); };
|
|
auto load_len5 = [] (const int& key) { return make_ready_future<sstring>(tests::random::get_sstring(5)); };
|
|
auto load_len10 = [] (const int& key) { return make_ready_future<sstring>(tests::random::get_sstring(10)); };
|
|
auto load_len95 = [] (const int& key) { return make_ready_future<sstring>(tests::random::get_sstring(95)); };
|
|
|
|
using namespace std::chrono;
|
|
utils::loading_cache<int, sstring, 1, utils::loading_cache_reload_enabled::no, sstring_length_entry_size> loading_cache(100, 1h, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 0);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 0);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 0);
|
|
|
|
loading_cache.get_ptr(1, load_len1).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 0);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 1);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 1);
|
|
|
|
loading_cache.get_ptr(2, load_len5).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 0);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 6);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 2);
|
|
|
|
// Move "2" to privileged section by touching it the second time.
|
|
loading_cache.get_ptr(2, load_len5).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 5);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 1);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 2);
|
|
|
|
loading_cache.get_ptr(3, load_len10).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 5);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 11);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 3);
|
|
|
|
// Move "1" to privileged section. load_len10 should not get executed, as "1"
|
|
// is already in the cache.
|
|
loading_cache.get_ptr(1, load_len10).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 6);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 10);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 3);
|
|
|
|
// Flood cache with elements of size 10,
|
|
// unprivileged. "1" and "2" should stay in the privileged section.
|
|
for (int i = 11; i < 30; i++) {
|
|
loading_cache.get_ptr(i, load_len10).discard_result().get();
|
|
}
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 6);
|
|
// We shrink the cache BEFORE adding element,
|
|
// so after adding the element, the cache
|
|
// can exceed max_size...
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 100);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 12);
|
|
|
|
// Flood cache with elements of size 10, privileged.
|
|
for (int i = 11; i < 30; i++) {
|
|
loading_cache.get_ptr(i, load_len10).discard_result().get();
|
|
loading_cache.get_ptr(i, load_len10).discard_result().get();
|
|
}
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 100);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 0);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 10);
|
|
|
|
// Add one new unprivileged entry.
|
|
loading_cache.get_ptr(31, load_len1).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 90);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 1);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 10);
|
|
|
|
// Add another unprivileged entry, privileged entry should get evicted.
|
|
loading_cache.get_ptr(32, load_len5).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 90);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 6);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 11);
|
|
|
|
// Make it privileged by touching it again.
|
|
loading_cache.get_ptr(32, load_len5).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 95);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 1);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 11);
|
|
|
|
// Add another unprivileged entry.
|
|
loading_cache.get_ptr(33, load_len10).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 95);
|
|
// We shrink the cache BEFORE adding element,
|
|
// so after adding the element, the cache
|
|
// can exceed max_size...
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 11);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 12);
|
|
|
|
// Add another unprivileged entry, privileged entry should get evicted.
|
|
loading_cache.get_ptr(34, load_len10).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 85);
|
|
// We shrink the cache BEFORE adding element,
|
|
// so after adding the element, the cache
|
|
// can exceed max_size...
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 21);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 12);
|
|
|
|
// Add a big unprivileged entry, filling almost entire cache.
|
|
loading_cache.get_ptr(35, load_len95).discard_result().get();
|
|
BOOST_REQUIRE_EQUAL(loading_cache.privileged_section_memory_footprint(), 75);
|
|
// We shrink the cache BEFORE adding element,
|
|
// so after adding the element, the cache
|
|
// can exceed max_size...
|
|
BOOST_REQUIRE_EQUAL(loading_cache.unprivileged_section_memory_footprint(), 95 + 21);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 12);
|
|
});
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_loading_cache_reload_during_eviction) {
|
|
return seastar::async([] {
|
|
using namespace std::chrono;
|
|
load_count = 0;
|
|
utils::loading_cache<int, sstring, 0, utils::loading_cache_reload_enabled::yes> loading_cache(1, 100ms, 10ms, testlog, loader);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
prepare().get();
|
|
|
|
auto curr_time = lowres_clock::now();
|
|
int i = 0;
|
|
|
|
// this will cause reloading when values are being actively evicted due to the limited cache size
|
|
do_until(
|
|
[&] { return lowres_clock::now() - curr_time > 1s; },
|
|
[&] { return loading_cache.get_ptr(i++ % 2).discard_result(); }
|
|
).get();
|
|
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 1);
|
|
});
|
|
}
|
|
|
|
SEASTAR_THREAD_TEST_CASE(test_loading_cache_remove_leaves_no_old_entries_behind) {
|
|
using namespace std::chrono;
|
|
load_count = 0;
|
|
|
|
auto load_v1 = [] (auto key) { return make_ready_future<sstring>("v1"); };
|
|
auto load_v2 = [] (auto key) { return make_ready_future<sstring>("v2"); };
|
|
auto load_v3 = [] (auto key) { return make_ready_future<sstring>("v3"); };
|
|
|
|
{
|
|
utils::loading_cache<int, sstring> loading_cache(num_loaders, 100s, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
//
|
|
// Test remove() concurrent with loading
|
|
//
|
|
|
|
auto f = loading_cache.get_ptr(0, [&](auto key) {
|
|
return yield().then([&] {
|
|
return load_v1(key);
|
|
});
|
|
});
|
|
|
|
loading_cache.remove(0);
|
|
|
|
BOOST_REQUIRE_EQUAL(loading_cache.find(0), nullptr);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 0);
|
|
|
|
auto ptr1 = f.get0();
|
|
BOOST_REQUIRE_EQUAL(*ptr1, "v1");
|
|
|
|
BOOST_REQUIRE_EQUAL(loading_cache.find(0), nullptr);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 0);
|
|
|
|
ptr1 = loading_cache.get_ptr(0, load_v2).get0();
|
|
loading_cache.remove(0);
|
|
BOOST_REQUIRE_EQUAL(*ptr1, "v2");
|
|
|
|
//
|
|
// Test that live ptr1, removed from cache, does not prevent reload of new value
|
|
//
|
|
auto ptr2 = loading_cache.get_ptr(0, load_v3).get0();
|
|
ptr1 = nullptr;
|
|
BOOST_REQUIRE_EQUAL(*ptr2, "v3");
|
|
}
|
|
|
|
// Test remove_if()
|
|
{
|
|
utils::loading_cache<int, sstring> loading_cache(num_loaders, 100s, testlog);
|
|
auto stop_cache_reload = seastar::defer([&loading_cache] { loading_cache.stop().get(); });
|
|
|
|
//
|
|
// Test remove_if() concurrent with loading
|
|
//
|
|
auto f = loading_cache.get_ptr(0, [&](auto key) {
|
|
return yield().then([&] {
|
|
return load_v1(key);
|
|
});
|
|
});
|
|
|
|
loading_cache.remove_if([] (auto&& v) { return v == "v1"; });
|
|
|
|
BOOST_REQUIRE_EQUAL(loading_cache.find(0), nullptr);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 0);
|
|
|
|
auto ptr1 = f.get0();
|
|
BOOST_REQUIRE_EQUAL(*ptr1, "v1");
|
|
|
|
BOOST_REQUIRE_EQUAL(loading_cache.find(0), nullptr);
|
|
BOOST_REQUIRE_EQUAL(loading_cache.size(), 0);
|
|
|
|
ptr1 = loading_cache.get_ptr(0, load_v2).get0();
|
|
loading_cache.remove_if([] (auto&& v) { return v == "v2"; });
|
|
BOOST_REQUIRE_EQUAL(*ptr1, "v2");
|
|
|
|
//
|
|
// Test that live ptr1, removed from cache, does not prevent reload of new value
|
|
//
|
|
auto ptr2 = loading_cache.get_ptr(0, load_v3).get0();
|
|
ptr1 = nullptr;
|
|
BOOST_REQUIRE_EQUAL(*ptr2, "v3");
|
|
ptr2 = nullptr;
|
|
}
|
|
}
|
|
|
|
SEASTAR_TEST_CASE(test_prepared_statement_small_cache) {
|
|
// CQL prepared statement cache uses loading_cache
|
|
// internally.
|
|
constexpr auto CACHE_SIZE = 950000;
|
|
|
|
cql_test_config small_cache_config;
|
|
small_cache_config.qp_mcfg = {CACHE_SIZE, CACHE_SIZE};
|
|
return do_with_cql_env_thread([](cql_test_env& e) {
|
|
e.execute_cql("CREATE TABLE tbl1 (a int, b int, PRIMARY KEY (a))").get();
|
|
|
|
auto current_uid = 0;
|
|
|
|
// Prepare 100 queries and execute them twice,
|
|
// filling "privileged section" of loading_cache.
|
|
std::vector<cql3::prepared_cache_key_type> prepared_ids_privileged;
|
|
for (int i = 0; i < 100; i++) {
|
|
auto prepared_id = e.prepare(fmt::format("SELECT * FROM tbl1 WHERE a = {}", current_uid++)).get0();
|
|
e.execute_prepared(prepared_id, {}).get();
|
|
e.execute_prepared(prepared_id, {}).get();
|
|
prepared_ids_privileged.push_back(prepared_id);
|
|
}
|
|
|
|
int how_many_in_cache = 0;
|
|
for (auto& prepared_id : prepared_ids_privileged) {
|
|
if (e.local_qp().get_prepared(prepared_id)) {
|
|
how_many_in_cache++;
|
|
}
|
|
}
|
|
|
|
// Assumption: CACHE_SIZE should hold at least 50 queries,
|
|
// but not more than 99 queries. Other checks in this
|
|
// test rely on that fact.
|
|
BOOST_REQUIRE(how_many_in_cache >= 50);
|
|
BOOST_REQUIRE(how_many_in_cache <= 99);
|
|
|
|
// Then prepare 5 queries and execute them one time,
|
|
// which will occupy "unprivileged section" of loading_cache.
|
|
std::vector<cql3::prepared_cache_key_type> prepared_ids_unprivileged;
|
|
for (int i = 0; i < 5; i++) {
|
|
auto prepared_id = e.prepare(fmt::format("SELECT * FROM tbl1 WHERE a = {}", current_uid++)).get0();
|
|
e.execute_prepared(prepared_id, {}).get();
|
|
prepared_ids_unprivileged.push_back(prepared_id);
|
|
}
|
|
|
|
// Check that all of those prepared queries can still be
|
|
// executed. This simulates as if you wanted to execute
|
|
// a BATCH with all of them, which requires all of those
|
|
// prepared statements to be executable (in the cache).
|
|
for (auto& prepared_id : prepared_ids_unprivileged) {
|
|
e.execute_prepared(prepared_id, {}).get();
|
|
}
|
|
|
|
// Deterministic random for reproducibility.
|
|
testing::local_random_engine.seed(12345);
|
|
|
|
// Prepare 500 queries and execute them a random number of times.
|
|
for (int i = 0; i < 500; i++) {
|
|
auto prepared_id = e.prepare(fmt::format("SELECT * FROM tbl1 WHERE a = {}", current_uid++)).get0();
|
|
auto times = rand_int(4);
|
|
for (int j = 0; j < times; j++) {
|
|
e.execute_prepared(prepared_id, {}).get();
|
|
}
|
|
}
|
|
|
|
// Prepare 100 simulated "batches" and execute them
|
|
// a random number of times.
|
|
for (int i = 0; i < 100; i++) {
|
|
std::vector<cql3::prepared_cache_key_type> prepared_ids_batch;
|
|
for (int j = 0; j < 5; j++) {
|
|
auto prepared_id = e.prepare(fmt::format("SELECT * FROM tbl1 WHERE a = {}", current_uid++)).get0();
|
|
prepared_ids_batch.push_back(prepared_id);
|
|
}
|
|
auto times = rand_int(4);
|
|
for (int j = 0; j < times; j++) {
|
|
for (auto& prepared_id : prepared_ids_batch) {
|
|
e.execute_prepared(prepared_id, {}).get();
|
|
}
|
|
}
|
|
}
|
|
}, small_cache_config);
|
|
}
|