[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:
Lakshmi Narayanan Sreethar
2024-07-22 17:50:22 +05:30
committed by Botond Dénes
parent b2ea946837
commit ee74fe4e0e
6 changed files with 81 additions and 2 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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
});
};

View File

@@ -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

View File

@@ -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 {