From 9e72079402093863c8a6dd19dc54869315a94c2d Mon Sep 17 00:00:00 2001 From: Benny Halevy Date: Thu, 19 Feb 2026 12:01:29 +0200 Subject: [PATCH] database: apply auto_snapshot_ttl When automatically taking a snapshot before a table is truncated or dropped, set snapshot_options::expires_at if auto_snapshot_ttl is set. This is then stored in the snapshot manifest.json file. Add a unit test to verify that the auto_snapshot_ttl is applied in the snapshot manifest.json. Signed-off-by: Benny Halevy --- replica/database.cc | 10 +++++- test/boost/database_test.cc | 64 +++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/replica/database.cc b/replica/database.cc index ec359e9685..f772f4c58c 100644 --- a/replica/database.cc +++ b/replica/database.cc @@ -3152,7 +3152,15 @@ future<> database::truncate_table_on_all_shards(sharded& sharded_db, s auto truncated_at = truncated_at_opt.value_or(db_clock::now()); auto name = snapshot_name_opt.value_or( format("{:d}-{}", truncated_at.time_since_epoch().count(), cf.schema()->cf_name())); - co_await snapshot_table_on_all_shards(sharded_db, table_shards, name, db::snapshot_options{}); + auto ttl = sharded_db.local().get_config().auto_snapshot_ttl(); + db::snapshot_options opts; + if (ttl) { + // Add one second to compensate for created_at being truncated + // to second resolution, ensuring the snapshot lives for at least + // auto_snapshot_ttl seconds. + opts.expires_at = opts.created_at + (ttl + 1) * 1s; + } + co_await snapshot_table_on_all_shards(sharded_db, table_shards, name, opts); } co_await sharded_db.invoke_on_all([&] (database& db) { diff --git a/test/boost/database_test.cc b/test/boost/database_test.cc index 0e63888381..80724a408b 100644 --- a/test/boost/database_test.cc +++ b/test/boost/database_test.cc @@ -549,7 +549,7 @@ static std::set collect_sstables(const std::set& all_files, co } // Validate that the manifest.json lists exactly the SSTables present in the snapshot directory -static future<> validate_manifest(const locator::topology& topology, const fs::path& snapshot_dir, const std::set& in_snapshot_dir, gc_clock::time_point min_time, bool tablets_enabled) { +static future<> validate_manifest(const locator::topology& topology, const fs::path& snapshot_dir, const std::set& in_snapshot_dir, gc_clock::time_point min_time, bool tablets_enabled, std::optional ttl = std::nullopt) { sstring suffix = "-TOC.txt"; auto sstables_in_snapshot = collect_sstables(in_snapshot_dir, suffix); std::set sstables_in_manifest; @@ -606,7 +606,15 @@ static future<> validate_manifest(const locator::topology& topology, const fs::p BOOST_REQUIRE(created_at_seconds > 0); auto& expires_at = manifest_snapshot["expires_at"]; BOOST_REQUIRE(expires_at.IsNumber()); - BOOST_REQUIRE_GE(expires_at.GetInt64(), created_at_seconds); + auto expires_at_seconds = expires_at.GetInt64(); + if (ttl) { + BOOST_REQUIRE_GE(expires_at_seconds, created_at_seconds + *ttl); + BOOST_REQUIRE_LE(expires_at_seconds, created_at_seconds + *ttl + 1); + } else { + BOOST_REQUIRE_GE(expires_at.GetInt64(), created_at_seconds); + } + } else if (ttl) { + BOOST_FAIL("manifest should have expires_at when ttl is set"); } BOOST_REQUIRE(manifest_json.HasMember("table")); @@ -757,6 +765,58 @@ SEASTAR_TEST_CASE(snapshot_skip_flush_works) { }); } +SEASTAR_THREAD_TEST_CASE(test_auto_snapshot_ttl) { + bool tablets_enabled = true; + bool create_mvs = false; + int ttl = 1; +#ifdef SCYLLA_BUILD_MODE_DEBUG + ttl = 3; +#endif + + auto db_cfg_ptr = make_shared(); + db_cfg_ptr->tablets_mode_for_new_keyspaces(tablets_enabled ? db::tablets_mode_t::mode::enabled : db::tablets_mode_t::mode::disabled); + db_cfg_ptr->auto_snapshot(true); + db_cfg_ptr->auto_snapshot_ttl(ttl); + std::string ks_name = "ks"; + std::string table_name = "test"; + size_t num_keys = 100; + do_with_some_data_in_thread({table_name}, [&] (cql_test_env& e) { + auto min_time = gc_clock::now(); + take_snapshot(e, ks_name, table_name).get(); + + auto& cf = e.local_db().find_column_family(ks_name, table_name); + auto table_directory = table_dir(cf); + + auto in_table_dir = collect_files(table_directory).get(); + // snapshot triggered a flush and wrote the data down. + BOOST_REQUIRE_GE(in_table_dir.size(), 9); + + testlog.debug("Dropping table {}.{}", ks_name, table_name); + replica::database::legacy_drop_table_on_all_shards(e.db(), e.get_system_keyspace(), ks_name, table_name, true).get(); + + fs::path snapshot_dir; + auto snapshot_base_dir = table_directory / sstables::snapshots_dir; + directory_lister lister(snapshot_base_dir, lister::dir_entry_types::of()); + while (auto de = lister.get().get()) { + if (de->name.starts_with("pre-drop")) { + BOOST_REQUIRE(snapshot_dir.empty()); // only one pre-drop snapshot should be present + testlog.debug("Found auto-snapshot directory: {}", snapshot_base_dir / de->name); + snapshot_dir = snapshot_base_dir / de->name; + } + } + + auto in_snapshot_dir = collect_files(snapshot_dir).get(); + + in_table_dir.insert("manifest.json"); + in_table_dir.insert("schema.cql"); + // all files were copied and manifest was generated + BOOST_REQUIRE_EQUAL(in_table_dir, in_snapshot_dir); + + const auto& topology = e.local_db().get_token_metadata().get_topology(); + validate_manifest(topology, snapshot_dir, in_snapshot_dir, min_time, tablets_enabled, ttl).get(); + }, create_mvs, db_cfg_ptr, num_keys).get(); +} + SEASTAR_TEST_CASE(snapshot_list_okay) { return do_with_some_data_in_thread({"cf"}, [] (cql_test_env& e) { auto& cf = e.local_db().find_column_family("ks", "cf");