|
18 | 18 | #include "cluster/controller_api.h" |
19 | 19 | #include "cluster/health_monitor_frontend.h" |
20 | 20 | #include "kafka/client/transport.h" |
| 21 | +#include "kafka/data/partition_proxy.h" |
21 | 22 | #include "kafka/data/replicated_partition.h" |
22 | 23 | #include "kafka/protocol/fetch.h" |
23 | 24 | #include "kafka/protocol/find_coordinator.h" |
@@ -346,6 +347,97 @@ TEST_F(ManualFixture, ReplicatedPartitionMidMigrationApiParity) { |
346 | 347 | 10s, [&] { return rp_mig.start_offset() >= trunc; }); |
347 | 348 | } |
348 | 349 |
|
| 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 | + |
349 | 441 | class EndToEndFixture |
350 | 442 | : public s3_imposter_fixture |
351 | 443 | , public manual_metadata_upload_mixin |
|
0 commit comments