Skip to content

Commit f9e4fcc

Browse files
tests: add spilled-migrating-partition routing unit test
MigratingSpilledEmptyLiveManifestRoutesToTieredStorage builds a partition whose live manifest has been fully offloaded to the archive (spillover) and set migrating, then asserts make_partition_proxy routes it like a directly-constructed replicated_partition. Without the spillover-aware gate the empty live manifest sends it to the cloud-topic path, which throws (cloud-topics subsystem uninitialized in the fixture). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bad9af7 commit f9e4fcc

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

src/v/cloud_storage/tests/cloud_storage_e2e_test.cc

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "cluster/controller_api.h"
1919
#include "cluster/health_monitor_frontend.h"
2020
#include "kafka/client/transport.h"
21+
#include "kafka/data/partition_proxy.h"
2122
#include "kafka/data/replicated_partition.h"
2223
#include "kafka/protocol/fetch.h"
2324
#include "kafka/protocol/find_coordinator.h"
@@ -346,6 +347,97 @@ TEST_F(ManualFixture, ReplicatedPartitionMidMigrationApiParity) {
346347
10s, [&] { return rp_mig.start_offset() >= trunc; });
347348
}
348349

350+
// A migrating partition whose live STM manifest has been fully offloaded to the
351+
// archive (spillover) -- manifest().size()==0 while get_archive_start_offset()
352+
// is set -- still holds tiered-storage data in the archive and must be served
353+
// as tiered storage, not misrouted to the (empty) cloud-topic path. Regression
354+
// for the make_partition_proxy structural gate, which once keyed only on the
355+
// live manifest and so would read a spilled, still-migrating partition as cut
356+
// over.
357+
TEST_F(ManualFixture, MigratingSpilledEmptyLiveManifestRoutesToTieredStorage) {
358+
test_local_cfg.get("cloud_storage_disable_upload_loop_for_tests")
359+
.set_value(true);
360+
test_local_cfg.get("cloud_storage_spillover_manifest_max_segments")
361+
.set_value(std::make_optional<size_t>(2));
362+
test_local_cfg.get("cloud_storage_spillover_manifest_size")
363+
.set_value(std::optional<size_t>{});
364+
const model::topic topic_name("migrating-spilled");
365+
model::ntp ntp(model::kafka_namespace, topic_name, 0);
366+
367+
cluster::topic_properties props;
368+
props.shadow_indexing = model::shadow_indexing_mode::full;
369+
props.cleanup_policy_bitflags = model::cleanup_policy_bitflags::deletion;
370+
add_topic({model::kafka_namespace, topic_name}, 1, props).get();
371+
wait_for_leader(ntp).get();
372+
373+
auto partition = app.partition_manager.local().get(ntp);
374+
auto& archiver = partition->archiver().value().get();
375+
archiver.initialize_probe();
376+
tests::remote_segment_generator gen(make_kafka_client().get(), *partition);
377+
auto deferred_g_close = ss::defer([&gen] { gen.stop().get(); });
378+
auto total_records = gen.num_segments(20)
379+
.batches_per_segment(5)
380+
.produce()
381+
.get();
382+
ASSERT_GE(total_records, 50);
383+
ASSERT_TRUE(archiver.sync_for_tests().get());
384+
archiver.apply_spillover().get();
385+
386+
auto* stm = partition->archival_meta_stm().get();
387+
// Spillover populated the archive.
388+
ASSERT_NE(stm->get_archive_start_offset(), model::offset{});
389+
390+
// Drive the *live* manifest to empty while the archive remains: advance the
391+
// live start offset past its last segment, then drop the truncated
392+
// segments. This is the degenerate state the gate must handle -- live
393+
// manifest empty, archive non-empty.
394+
ss::abort_source as;
395+
const auto past_last = model::next_offset(stm->manifest().get_last_offset());
396+
auto ec
397+
= stm->truncate(past_last, ss::lowres_clock::now() + 30s, as).get();
398+
ASSERT_FALSE(ec) << "truncate failed: " << ec.message();
399+
ec = stm->cleanup_metadata(ss::lowres_clock::now() + 30s, as).get();
400+
ASSERT_FALSE(ec) << "cleanup_metadata failed: " << ec.message();
401+
ASSERT_EQ(stm->manifest().size(), 0u)
402+
<< "precondition: live manifest must be empty";
403+
ASSERT_NE(stm->get_archive_start_offset(), model::offset{})
404+
<< "precondition: archive must still hold data";
405+
406+
// Flip into the migrating state: cloud storage mode (cloud_topic_enabled) +
407+
// the archival STM migration flag.
408+
auto log = partition->log();
409+
ASSERT_TRUE(log->config().has_overrides());
410+
auto overrides = log->config().get_overrides();
411+
overrides.storage_mode = model::redpanda_storage_mode::cloud;
412+
log->set_overrides(overrides);
413+
auto mec
414+
= stm->set_migration_state(true, ss::lowres_clock::now() + 30s, as).get();
415+
ASSERT_FALSE(mec) << "set_migration_state failed: " << mec.message();
416+
ASSERT_TRUE(log->config().cloud_topic_enabled());
417+
ASSERT_TRUE(stm->is_migrating());
418+
419+
// Local-log trim must stay clamped: a migrating partition still holding
420+
// archived data (here a spilled, empty live manifest) must not free-evict
421+
// its local log. max_removable_local_log_offset() keys on
422+
// holds_archived_data(), so it stays constrained rather than returning
423+
// offset::max() -- which it would if it only checked the empty live
424+
// manifest size.
425+
EXPECT_NE(stm->max_removable_local_log_offset(), model::offset::max());
426+
427+
// The structural gate (make_partition_proxy) must route this to tiered
428+
// storage: the archive still holds the data. It must behave identically to
429+
// a directly-constructed replicated_partition -- not the cloud-topic path,
430+
// which would serve an empty L1 (or, with the cloud-topics subsystem
431+
// uninitialized here, throw).
432+
kafka::replicated_partition expected_ts(partition);
433+
kafka::partition_proxy proxy = kafka::make_partition_proxy(partition);
434+
EXPECT_EQ(proxy.start_offset(), expected_ts.start_offset());
435+
EXPECT_EQ(proxy.high_watermark(), expected_ts.high_watermark());
436+
// The high watermark covers the produced records (served from the archive),
437+
// which the empty cloud-topic path could not do.
438+
EXPECT_GT(proxy.high_watermark(), model::offset(0));
439+
}
440+
349441
class EndToEndFixture
350442
: public s3_imposter_fixture
351443
, public manual_metadata_upload_mixin

0 commit comments

Comments
 (0)