Skip to content

Commit 39313f9

Browse files
cluster/partition: bootstrap partition_mode on leadership and creation
Record the partition's durable storage mode (partition_mode) in partition_properties. It is populated: - at partition creation, from the topic-config mode, when the migration feature is active (partition_manager::manage); - on becoming leader, for a partition that predates this (maybe_bootstrap_ partition_mode); a node that became leader after an in-place ts->ct switch keys on the archival manifest (holds_archived_data) since topic_mode() already reads cloud; - on migration-feature activation, sweeping current leaders that were elected while the feature was inactive. The creation-time classification (set_creation_partition_mode) is preferred by the bootstrap over re-reading topic_mode(), so the mode does not depend on the async config propagation a runtime re-derivation would race. Also add partition_properties_stm::seed_partition_mode(), which pre-writes the kvstore-backed local snapshot before the STM starts so a partition restored with a non-empty log comes up with partition_mode already set (the seed survives start(), whose initial cleanup only runs for an empty log). Used by the migration-recovery path. partition_mode stays inert here; serving/behavior accessors begin keying on it in later PRs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9f4201d commit 39313f9

9 files changed

Lines changed: 177 additions & 0 deletions

src/v/cluster/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,7 @@ redpanda_cc_library(
14071407
":types",
14081408
"//src/v/base",
14091409
"//src/v/storage",
1410+
"//src/v/utils:prefix_logger",
14101411
"@fmt",
14111412
"@seastar",
14121413
],

src/v/cluster/archival/archival_metadata_stm.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,6 +1674,11 @@ archival_metadata_stm::manifest() const {
16741674
return *_manifest;
16751675
}
16761676

1677+
bool archival_metadata_stm::holds_archived_data() const {
1678+
return _manifest->size() > 0
1679+
|| _manifest->get_archive_start_offset() != model::offset{};
1680+
}
1681+
16771682
model::offset archival_metadata_stm::get_start_offset() const {
16781683
auto p = _manifest->get_start_offset();
16791684
if (p.has_value()) {

src/v/cluster/archival/archival_metadata_stm.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ class archival_metadata_stm final : public raft::persisted_stm<> {
216216
/// added with `add_segments`.
217217
const cloud_storage::partition_manifest& manifest() const;
218218

219+
/// True if the partition holds tiered-storage data: the manifest has
220+
/// segments, or a non-empty spillover archive. Used to detect that a
221+
/// partition is (still) tiered storage independent of the topic config.
222+
bool holds_archived_data() const;
223+
219224
ss::future<> stop() override;
220225

221226
static ss::future<> make_snapshot(

src/v/cluster/partition.cc

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,6 +1771,58 @@ void partition::update_partition_mode() {
17711771
_partition_properties_stm->partition_mode());
17721772
}
17731773

1774+
ss::future<> partition::maybe_bootstrap_partition_mode() {
1775+
if (!_partition_properties_stm || !is_leader()) {
1776+
co_return;
1777+
}
1778+
if (!_feature_table.local().is_active(
1779+
features::feature::tiered_to_cloud_migration)) {
1780+
co_return;
1781+
}
1782+
// Already bootstrapped (or already advanced by a migration): nothing to do.
1783+
if (
1784+
_partition_properties_stm->partition_mode()
1785+
!= model::redpanda_storage_mode::unset) {
1786+
co_return;
1787+
}
1788+
// Pick the mode to record.
1789+
// - holds_archived_data(): the partition holds tiered-storage data, so it
1790+
// is (still) tiered regardless of topic_mode -- covers a node that
1791+
// became leader *after* the operator switched the topic ts->ct, whose
1792+
// archival manifest (loaded on a running partition) reveals it is
1793+
// mid-migration while topic_mode() already reads `cloud`.
1794+
// - _creation_partition_mode: the classification manage() computed
1795+
// deterministically at creation, authoritative when set. Preferred over
1796+
// re-reading topic_mode() here because it captures the mode as it was
1797+
// at creation, immune to a later async topic-config flip.
1798+
// - topic_mode(): the fallback for a partition with no creation-time hint
1799+
// (created before this classifier existed). Legacy shadow_indexing
1800+
// topics have topic_mode() == unset and are left unset (SI fallback).
1801+
const bool holds_ts_data = _archival_meta_stm
1802+
&& _archival_meta_stm->holds_archived_data();
1803+
model::redpanda_storage_mode target;
1804+
if (holds_ts_data) {
1805+
target = model::redpanda_storage_mode::tiered;
1806+
} else if (
1807+
_creation_partition_mode != model::redpanda_storage_mode::unset) {
1808+
target = _creation_partition_mode;
1809+
} else {
1810+
target = get_ntp_config().topic_mode();
1811+
}
1812+
if (target == model::redpanda_storage_mode::unset) {
1813+
co_return;
1814+
}
1815+
auto res = co_await _partition_properties_stm->set_partition_mode(target);
1816+
if (res.has_error()) {
1817+
vlog(
1818+
clusterlog.debug,
1819+
"{}: failed to bootstrap partition_mode to {}: {}",
1820+
_raft->ntp(),
1821+
target,
1822+
res.error().message());
1823+
}
1824+
}
1825+
17741826
ss::future<result<model::offset>> partition::set_writes_disabled(
17751827
partition_properties_stm::writes_disabled disable,
17761828
model::timeout_clock::time_point deadline,

src/v/cluster/partition.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,25 @@ class partition : public ss::enable_lw_shared_from_this<partition> {
164164

165165
bool is_elected_leader() const;
166166
bool is_leader() const;
167+
168+
// On becoming leader, capture the partition's current
169+
// (topic-config-derived) storage mode into partition_properties, so it is
170+
// remembered across later topic-config changes (the TS->CT migration).
171+
// Gated behind the partition_mode feature; one-time per partition and
172+
// idempotent. Legacy shadow_indexing topics (no explicit storage_mode) are
173+
// left unset. Invoked by partition_manager's leadership notification.
174+
ss::future<> maybe_bootstrap_partition_mode();
175+
176+
// The partition_mode to record at bootstrap, classified deterministically
177+
// at creation (partition_manager::manage), so it does not depend on the
178+
// async config propagation a runtime re-derivation would race. `unset`
179+
// means "no creation-time classification" (e.g. created while the feature
180+
// was inactive), in which case the bootstrap falls back to the runtime
181+
// signals.
182+
void set_creation_partition_mode(model::redpanda_storage_mode m) {
183+
_creation_partition_mode = m;
184+
}
185+
167186
bool has_followers() const;
168187
void block_new_leadership() const;
169188
void unblock_new_leadership() const;
@@ -429,6 +448,11 @@ class partition : public ss::enable_lw_shared_from_this<partition> {
429448
// at start and whenever the STM signals a change.
430449
void update_partition_mode();
431450

451+
// Creation-time partition_mode classification; see
452+
// set_creation_partition_mode. `unset` until manage() sets it.
453+
model::redpanda_storage_mode _creation_partition_mode{
454+
model::redpanda_storage_mode::unset};
455+
432456
consensus_ptr _raft; // never null
433457
ss::shared_ptr<cluster::log_eviction_stm> _log_eviction_stm;
434458
ss::shared_ptr<cluster::rm_stm> _rm_stm;

src/v/cluster/partition_manager.cc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
#include "cluster/partition_recovery_manager.h"
2626
#include "cluster/topic_configuration.h"
2727
#include "cluster/types.h"
28+
#include "features/feature_table.h"
2829
#include "model/metadata.h"
2930
#include "raft/consensus.h"
3031
#include "raft/consensus_utils.h"
3132
#include "raft/fundamental.h"
3233
#include "ssx/async-clear.h"
34+
#include "ssx/future-util.h"
3335

3436
#include <seastar/core/lowres_clock.hh>
3537
#include <seastar/core/shared_ptr.hh>
@@ -73,6 +75,11 @@ partition_manager::partition_manager(
7375
if (a) {
7476
a.value().get().notify_leadership(leader_id);
7577
}
78+
// Bootstrap the partition's durable storage mode on becoming
79+
// leader (one-time, idempotent; no-op when not leader). The
80+
// shared_ptr keeps the partition alive across the async write.
81+
ssx::spawn_with_gate(
82+
_gate, [p] { return p->maybe_bootstrap_partition_mode(); });
7683
}
7784
});
7885
_shutdown_watchdog.set_callback(
@@ -100,9 +107,36 @@ partition_manager::get_topic_partition_table(
100107

101108
ss::future<> partition_manager::start() {
102109
maybe_arm_shutdown_watchdog();
110+
// Bootstrap partition_mode once the migration feature activates. The
111+
// leadership-notification bootstrap (maybe_bootstrap_partition_mode) is
112+
// gated on the feature being active, so a partition that became leader
113+
// while the feature was inactive is left with partition_mode == unset.
114+
// Re-run the (idempotent) bootstrap on every current leader when the
115+
// feature turns active; partitions that gain leadership after activation
116+
// are covered by the leadership notification.
117+
ssx::spawn_with_gate(_gate, [this] {
118+
return bootstrap_partition_mode_on_migration_feature();
119+
});
103120
co_return;
104121
}
105122

123+
ss::future<>
124+
partition_manager::bootstrap_partition_mode_on_migration_feature() {
125+
try {
126+
co_await _feature_table.local().await_feature(
127+
features::feature::tiered_to_cloud_migration, _as);
128+
} catch (...) {
129+
// Aborted on shutdown before the feature activated.
130+
co_return;
131+
}
132+
for (auto& [_, p] : _ntp_table) {
133+
if (p->is_leader()) {
134+
ssx::spawn_with_gate(
135+
_gate, [p] { return p->maybe_bootstrap_partition_mode(); });
136+
}
137+
}
138+
}
139+
106140
ss::future<consensus_ptr> partition_manager::manage(
107141
storage::ntp_config ntp_cfg,
108142
raft::group_id group,
@@ -330,6 +364,17 @@ ss::future<consensus_ptr> partition_manager::manage(
330364
read_replica_bucket,
331365
_cloud_topics_state);
332366

367+
// Populate partition_mode at creation when the feature is active, so it is
368+
// classified deterministically here from the topic config rather than via
369+
// the racy runtime bootstrap. The bootstrap remains for the feature-switch
370+
// case (a partition that predates the feature activation). Left unset when
371+
// the feature is inactive.
372+
if (
373+
_feature_table.local().is_active(
374+
features::feature::tiered_to_cloud_migration)) {
375+
p->set_creation_partition_mode(log->config().topic_mode());
376+
}
377+
333378
_ntp_table.emplace(log->config().ntp(), p);
334379
_raft_table.emplace(group, p);
335380

src/v/cluster/partition_manager.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ class partition_manager
277277
void check_partitions_shutdown_state();
278278

279279
void maybe_arm_shutdown_watchdog();
280+
281+
// Awaits the tiered_to_cloud_migration feature and then re-runs
282+
// maybe_bootstrap_partition_mode on every current leader, covering
283+
// partitions that became leader while the feature was inactive.
284+
ss::future<> bootstrap_partition_mode_on_migration_feature();
280285
storage::api& _storage;
281286
/// used to wait for concurrent recoveries
282287
ss::sharded<raft::group_manager>& _raft_manager;

src/v/cluster/partition_properties_stm.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "model/record_batch_types.h"
2222
#include "raft/persisted_stm.h"
2323
#include "storage/log.h"
24+
#include "utils/prefix_logger.h"
2425

2526
#include <fmt/format.h>
2627

@@ -143,6 +144,30 @@ ss::future<raft::stm_snapshot> partition_properties_stm::take_local_snapshot(
143144
serde::to_iobuf(local_snapshot{.state_updates = std::move(state)}));
144145
}
145146

147+
ss::future<> partition_properties_stm::seed_partition_mode(
148+
storage::kvstore& kvstore,
149+
ss::logger& logger,
150+
const model::ntp& ntp,
151+
model::offset snapshot_offset,
152+
model::redpanda_storage_mode mode) {
153+
// A single base state entry (update_offset {}, strictly below any log start
154+
// offset) carrying the mode, matching the STM's initial in-memory state
155+
// plus partition_mode.
156+
local_snapshot snap;
157+
snap.state_updates.push_back(
158+
state_snapshot{
159+
.writes_disabled = writes_disabled::no,
160+
.update_offset = model::offset{},
161+
.writes_revision_id = {},
162+
.partition_mode = mode});
163+
prefix_logger log(logger, fmt::format("[{}]", ntp));
164+
raft::kvstore_backed_stm_snapshot backend(
165+
partition_properties_stm_snapshot, log, ntp, kvstore);
166+
co_await backend.persist_local_snapshot(
167+
raft::stm_snapshot::create(
168+
0, snapshot_offset, serde::to_iobuf(std::move(snap))));
169+
}
170+
146171
ss::future<> partition_properties_stm::do_apply(const model::record_batch& b) {
147172
if (
148173
b.header().type

src/v/cluster/partition_properties_stm.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@ class partition_properties_stm
6565
// callers (ntp_config) fall back to the topic-config-derived mode.
6666
model::redpanda_storage_mode partition_mode() const;
6767

68+
// Seed the kvstore-backed local snapshot with a single state entry
69+
// recording `mode`, before the partition (STM) starts, so a partition
70+
// restored with a non-empty log comes up with partition_mode already set --
71+
// before any leadership/config-propagation race could misclassify it. The
72+
// seed survives persisted_stm start, whose initial cleanup only runs for an
73+
// empty log (dirty_offset == {}), whereas a restored/bootstrapped partition
74+
// has a non-empty log. `snapshot_offset` is the last-applied offset the
75+
// seed represents (prev(log start)).
76+
static ss::future<> seed_partition_mode(
77+
storage::kvstore& kvstore,
78+
ss::logger& logger,
79+
const model::ntp& ntp,
80+
model::offset snapshot_offset,
81+
model::redpanda_storage_mode mode);
82+
6883
// Registers a callback invoked on this shard whenever partition_mode may
6984
// have changed (on apply and on raft-snapshot restore), so the owner can
7085
// re-read partition_mode() and propagate it (e.g. into ntp_config).

0 commit comments

Comments
 (0)