[Backport 6.1] sstables: do not reload components of unlinked sstables
The SSTable is removed from the reclaimed memory tracking logic only
when its object is deleted. However, there is a risk that the Bloom
filter reloader may attempt to reload the SSTable after it has been
unlinked but before the SSTable object is destroyed. Prevent this by
removing the SSTable from the reclaimed list maintained by the manager
as soon as it is unlinked.
The original logic that updated the memory tracking in
`sstables_manager::deactivate()` is left in place as (a) the variables
have to be updated only when the SSTable object is actually deleted, as
the memory used by the filter is not freed as long as the SSTable is
alive, and (b) the `_reclaimed.erase(*sst)` is still useful during
shutdown, for example, when the SSTable is not unlinked but just
destroyed.
Fixes https://github.com/scylladb/scylladb/issues/19722
Closes scylladb/scylladb#19717
* github.com:scylladb/scylladb:
boost/bloom_filter_test: add testcase to verify unlinked sstables are not reloaded
sstables: do not reload components of unlinked sstables
sstables/sstables_manager: introduce on_unlink method
(cherry picked from commit 591876b44e)
Backported from #19717 to 6.1
Closes scylladb/scylladb#19828
This commit is contained in:
committed by
Botond Dénes
parent
b2ea946837
commit
ee74fe4e0e
@@ -2986,6 +2986,7 @@ sstable::unlink(storage::sync_dir sync) noexcept {
|
||||
|
||||
co_await std::move(remove_fut);
|
||||
_stats.on_delete();
|
||||
_manager.on_unlink(this);
|
||||
}
|
||||
|
||||
thread_local sstables_stats::stats sstables_stats::_shard_stats;
|
||||
|
||||
@@ -323,6 +323,11 @@ void sstables_manager::validate_new_keyspace_storage_options(const data_dictiona
|
||||
}, so.value);
|
||||
}
|
||||
|
||||
void sstables_manager::on_unlink(sstable* sst) {
|
||||
// Remove the sst from manager's reclaimed list to prevent any attempts to reload its components.
|
||||
_reclaimed.erase(*sst);
|
||||
}
|
||||
|
||||
sstables_registry::~sstables_registry() = default;
|
||||
|
||||
} // namespace sstables
|
||||
|
||||
@@ -188,6 +188,9 @@ public:
|
||||
|
||||
void validate_new_keyspace_storage_options(const data_dictionary::storage_options&);
|
||||
|
||||
// To be called by the sstable to signal its unlinking
|
||||
void on_unlink(sstable* sst);
|
||||
|
||||
private:
|
||||
void add(sstable* sst);
|
||||
// Transition the sstable to the "inactive" state. It has no
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "readers/from_mutations_v2.hh"
|
||||
#include "utils/bloom_filter.hh"
|
||||
#include "utils/error_injection.hh"
|
||||
|
||||
SEASTAR_TEST_CASE(test_sstable_reclaim_memory_from_components_and_reload_reclaimed_components) {
|
||||
return test_env::do_with_async([] (test_env& env) {
|
||||
@@ -52,6 +53,11 @@ std::pair<shared_sstable, size_t> create_sstable_with_bloom_filter(test_env& env
|
||||
return {sst, sst_bf_memory};
|
||||
}
|
||||
|
||||
void dispose_and_stop_tracking_bf_memory(shared_sstable&& sst, test_env_sstables_manager& mgr) {
|
||||
mgr.remove_sst_from_reclaimed(sst.get());
|
||||
shared_sstable::dispose(sst.release().release());
|
||||
}
|
||||
|
||||
SEASTAR_TEST_CASE(test_sstable_manager_auto_reclaim_and_reload_of_bloom_filter) {
|
||||
return test_env::do_with_async([] (test_env& env) {
|
||||
simple_schema ss;
|
||||
@@ -89,7 +95,7 @@ SEASTAR_TEST_CASE(test_sstable_manager_auto_reclaim_and_reload_of_bloom_filter)
|
||||
|
||||
// Test auto reload - disposing sst3 should trigger reload of the
|
||||
// smallest filter in the reclaimed list, which is sst1's bloom filter.
|
||||
shared_sstable::dispose(sst3.release().release());
|
||||
dispose_and_stop_tracking_bf_memory(std::move(sst3), sst_mgr);
|
||||
REQUIRE_EVENTUALLY_EQUAL(sst1->filter_memory_size(), sst1_bf_memory);
|
||||
// only sst4's bloom filter memory should be reported as reclaimed
|
||||
REQUIRE_EVENTUALLY_EQUAL(sst_mgr.get_total_memory_reclaimed(), sst4_bf_memory);
|
||||
@@ -154,7 +160,7 @@ SEASTAR_TEST_CASE(test_bloom_filter_reclaim_during_reload) {
|
||||
utils::get_local_injector().enable("reload_reclaimed_components/pause", true);
|
||||
|
||||
// dispose sst2 to trigger reload of sst1's bloom filter
|
||||
shared_sstable::dispose(sst2.release().release());
|
||||
dispose_and_stop_tracking_bf_memory(std::move(sst2), sst_mgr);
|
||||
// _total_reclaimable_memory will be updated when the reload begins; wait for it.
|
||||
REQUIRE_EVENTUALLY_EQUAL(sst_mgr.get_total_reclaimable_memory(), sst1_bf_memory);
|
||||
|
||||
@@ -223,3 +229,57 @@ SEASTAR_TEST_CASE(test_bloom_filters_with_bad_partition_estimate) {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SEASTAR_TEST_CASE(test_bloom_filter_reload_after_unlink) {
|
||||
return test_env::do_with_async([] (test_env& env) {
|
||||
#ifndef SCYLLA_ENABLE_ERROR_INJECTION
|
||||
fmt::print("Skipping test as it depends on error injection. Please run in mode where it's enabled (debug,dev).\n");
|
||||
return;
|
||||
#endif
|
||||
simple_schema ss;
|
||||
auto schema = ss.schema();
|
||||
|
||||
auto mut = mutation(schema, ss.make_pkey(1));
|
||||
mut.partition().apply_insert(*schema, ss.make_ckey(1), ss.new_timestamp());
|
||||
|
||||
// bloom filter will be reclaimed automatically due to low memory
|
||||
auto sst = make_sstable_containing(env.make_sstable(schema), {mut});
|
||||
auto& sst_mgr = env.manager();
|
||||
BOOST_REQUIRE_EQUAL(sst->filter_memory_size(), 0);
|
||||
auto memory_reclaimed = sst_mgr.get_total_memory_reclaimed();
|
||||
|
||||
// manager's reclaimed set has the sst now
|
||||
auto& reclaimed_set = sst_mgr.get_reclaimed_set();
|
||||
BOOST_REQUIRE_EQUAL(reclaimed_set.size(), 1);
|
||||
BOOST_REQUIRE_EQUAL(reclaimed_set.begin()->get_filename(), sst->get_filename());
|
||||
|
||||
// hold a copy of shared sst object in async thread to test reload after unlink
|
||||
utils::get_local_injector().enable("test_bloom_filter_reload_after_unlink");
|
||||
auto async_sst_holder = seastar::async([sst] {
|
||||
// do nothing just hold a copy of sst and wait for message signalling test completion
|
||||
utils::get_local_injector().inject("test_bloom_filter_reload_after_unlink", [] (auto& handler) {
|
||||
auto ret = handler.wait_for_message(std::chrono::steady_clock::now() + std::chrono::seconds{5});
|
||||
return ret;
|
||||
}).get();
|
||||
});
|
||||
|
||||
// unlink the sst and release the object
|
||||
sst->unlink().get();
|
||||
sst.release();
|
||||
|
||||
// reclaimed set should be now empty but the total memory reclaimed should
|
||||
// be still the same as the sst object is not deactivated yet due to a copy
|
||||
// being alive in the async thread.
|
||||
BOOST_REQUIRE_EQUAL(sst_mgr.get_reclaimed_set().size(), 0);
|
||||
BOOST_REQUIRE_EQUAL(sst_mgr.get_total_memory_reclaimed(), memory_reclaimed);
|
||||
|
||||
// message async thread to complete waiting and thus release its copy of sst, triggering deactivation
|
||||
utils::get_local_injector().receive_message("test_bloom_filter_reload_after_unlink");
|
||||
async_sst_holder.get();
|
||||
|
||||
REQUIRE_EVENTUALLY_EQUAL(sst_mgr.get_total_memory_reclaimed(), 0);
|
||||
}, {
|
||||
// set available memory = 0 to force reclaim the bloom filter
|
||||
.available_memory = 0
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,5 +44,7 @@ custom_args:
|
||||
- '-c1 -m256M'
|
||||
commitlog_cleanup_test:
|
||||
- '-c1 -m2G'
|
||||
bloom_filter_test:
|
||||
- '-c1'
|
||||
run_in_debug:
|
||||
- logalloc_standard_allocator_segment_pool_backend_test
|
||||
|
||||
@@ -57,6 +57,14 @@ public:
|
||||
size_t get_total_reclaimable_memory() {
|
||||
return _total_reclaimable_memory;
|
||||
}
|
||||
|
||||
void remove_sst_from_reclaimed(sstable* sst) {
|
||||
_reclaimed.erase(*sst);
|
||||
}
|
||||
|
||||
auto& get_reclaimed_set() {
|
||||
return _reclaimed;
|
||||
}
|
||||
};
|
||||
|
||||
class test_env_compaction_manager {
|
||||
|
||||
Reference in New Issue
Block a user