Skip to content

Commit 4868cdc

Browse files
fix(inkless): initialize diskless switch for legacy alter configs (#630)
Ensure legacy AlterConfigs emits the same classic-to-diskless switch-pending partition records as incremental AlterConfigs. This keeps diskless.enable=true atomic with the per-partition migration markers, so existing classic leaders are sealed and registered for InitDisklessLog before diskless produces are accepted.
1 parent 5e7c7c9 commit 4868cdc

3 files changed

Lines changed: 95 additions & 12 deletions

File tree

core/src/test/java/kafka/server/InklessTopicTypeSwitcherClusterTest.java

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,25 @@
3434
import org.apache.kafka.clients.producer.ProducerRecord;
3535
import org.apache.kafka.common.config.ConfigResource;
3636
import org.apache.kafka.common.config.TopicConfig;
37+
import org.apache.kafka.common.protocol.ApiKeys;
38+
import org.apache.kafka.common.protocol.Errors;
39+
import org.apache.kafka.common.requests.AlterConfigsRequest;
40+
import org.apache.kafka.common.requests.AlterConfigsResponse;
3741
import org.apache.kafka.common.serialization.ByteArrayDeserializer;
3842
import org.apache.kafka.common.serialization.ByteArraySerializer;
3943
import org.apache.kafka.common.test.KafkaClusterTestKit;
4044
import org.apache.kafka.common.test.TestKitNodes;
4145
import org.apache.kafka.coordinator.group.GroupCoordinatorConfig;
46+
import org.apache.kafka.server.IntegrationTestUtils;
4247
import org.apache.kafka.server.config.ReplicationConfigs;
4348
import org.apache.kafka.server.config.ServerConfigs;
4449
import org.apache.kafka.server.log.remote.storage.RemoteLogManagerConfig;
4550

4651
import org.junit.jupiter.api.AfterEach;
4752
import org.junit.jupiter.api.BeforeEach;
48-
import org.junit.jupiter.api.Test;
4953
import org.junit.jupiter.api.TestInfo;
54+
import org.junit.jupiter.params.ParameterizedTest;
55+
import org.junit.jupiter.params.provider.EnumSource;
5056
import org.slf4j.Logger;
5157
import org.slf4j.LoggerFactory;
5258
import org.testcontainers.junit.jupiter.Container;
@@ -94,6 +100,11 @@ public class InklessTopicTypeSwitcherClusterTest {
94100

95101
private KafkaClusterTestKit cluster;
96102

103+
private enum AlterConfigsMode {
104+
INCREMENTAL,
105+
LEGACY
106+
}
107+
97108
@BeforeEach
98109
public void setup(final TestInfo testInfo) throws Exception {
99110
log.warn("[stage=setup] Initializing containers and 3-broker cluster");
@@ -147,8 +158,9 @@ public void teardown() throws Exception {
147158
cluster.close();
148159
}
149160

150-
@Test
151-
public void migrateClassicTopicToDiskless() throws Exception {
161+
@ParameterizedTest(name = "alterConfigsMode={0}")
162+
@EnumSource(AlterConfigsMode.class)
163+
public void switchClassicTopicToDiskless(final AlterConfigsMode alterConfigsMode) throws Exception {
152164
final String topicSuffix = UUID.randomUUID().toString().substring(0, 8);
153165
final String disklessTopic = "diskless-" + topicSuffix;
154166
final String classicTopic = "classic-" + topicSuffix;
@@ -161,7 +173,7 @@ public void migrateClassicTopicToDiskless() throws Exception {
161173

162174
final Map<String, Object> adminConfigs = baseClientConfigs();
163175
final Map<String, Object> producerConfigs = producerConfigs();
164-
log.warn("[stage=test-start] topics: diskless={}, classic={}, migrating={}", disklessTopic, classicTopic, classicToDisklessTopic);
176+
log.warn("[stage=test-start] alterConfigsMode={}, topics: diskless={}, classic={}, switching={}", alterConfigsMode, disklessTopic, classicTopic, classicToDisklessTopic);
165177

166178
try (Admin admin = AdminClient.create(adminConfigs)) {
167179
final NewTopic diskless = new NewTopic(disklessTopic, NUM_PARTITIONS, (short) -1)
@@ -185,28 +197,28 @@ public void migrateClassicTopicToDiskless() throws Exception {
185197

186198
final Thread producerThread = new Thread(() -> {
187199
try (Producer<byte[], byte[]> producer = new KafkaProducer<>(producerConfigs)) {
188-
log.warn("[stage=pre-migration-produce] Producing baseline records to all topics");
200+
log.warn("[stage=pre-switch-produce] Producing baseline records to all topics");
189201
produceRounds(producer, topics, producedCounts, 12, true);
190202
producerStarted.countDown();
191203

192-
log.warn("[stage=during-migration-produce] Continuing produces during migration");
204+
log.warn("[stage=during-switch-produce] Continuing produces during switch");
193205
while (keepProducing.get()) {
194206
produceRounds(producer, topics, producedCounts, 1, false);
195207
}
196208
} catch (Throwable t) {
197209
producerFailure.set(t);
198210
producerStarted.countDown();
199211
}
200-
}, "migration-producer-thread");
212+
}, "switch-producer-thread");
201213

202214
producerThread.start();
203215
assertTrue(producerStarted.await(30, TimeUnit.SECONDS), "Producer thread did not finish baseline production in time");
204216

205217
try {
206-
log.warn("[stage=migration-start] Enabling diskless for topic={}", classicToDisklessTopic);
207-
alterTopicConfig(admin, classicToDisklessTopic, Map.of(TopicConfig.DISKLESS_ENABLE_CONFIG, "true"));
218+
log.warn("[stage=switch-start] Enabling diskless for topic={} via {}", classicToDisklessTopic, alterConfigsMode);
219+
alterTopicConfig(admin, classicToDisklessTopic, Map.of(TopicConfig.DISKLESS_ENABLE_CONFIG, "true"), alterConfigsMode);
208220

209-
log.warn("[stage=await-migration] Waiting for diskless.enable=true on topic={}", classicToDisklessTopic);
221+
log.warn("[stage=await-switch] Waiting for diskless.enable=true on topic={}", classicToDisklessTopic);
210222
waitForTopicDisklessValue(admin, classicToDisklessTopic, "true");
211223
} finally {
212224
keepProducing.set(false);
@@ -218,10 +230,15 @@ public void migrateClassicTopicToDiskless() throws Exception {
218230
throw new RuntimeException("Producer thread failed", producerFailure.get());
219231
}
220232

233+
try (Producer<byte[], byte[]> producer = new KafkaProducer<>(producerConfigs)) {
234+
log.warn("[stage=post-switch-produce] Producing records after switch");
235+
produceRounds(producer, topics, producedCounts, 1, true);
236+
}
237+
221238
assertEquals("true", getTopicConfig(admin, disklessTopic).get(TopicConfig.DISKLESS_ENABLE_CONFIG));
222239
assertEquals("false", getTopicConfig(admin, classicTopic).get(TopicConfig.DISKLESS_ENABLE_CONFIG));
223240
assertEquals("true", getTopicConfig(admin, classicToDisklessTopic).get(TopicConfig.DISKLESS_ENABLE_CONFIG));
224-
log.warn("[stage=post-migration-validated] Topic configurations and placement checks passed");
241+
log.warn("[stage=post-switch-validated] Topic configurations and placement checks passed");
225242
}
226243

227244
log.warn("[stage=consume-verify] Produced counts before consume verification={}", producedCounts);
@@ -358,14 +375,50 @@ private void waitForTopicDisklessValue(final Admin admin,
358375

359376
private void alterTopicConfig(final Admin admin,
360377
final String topic,
361-
final Map<String, String> newConfigs) throws Exception {
378+
final Map<String, String> newConfigs,
379+
final AlterConfigsMode alterConfigsMode) throws Exception {
380+
switch (alterConfigsMode) {
381+
case INCREMENTAL:
382+
alterTopicConfigWithIncrementalAlterConfigs(admin, topic, newConfigs);
383+
break;
384+
case LEGACY:
385+
alterTopicConfigWithLegacyAlterConfigs(topic, newConfigs);
386+
break;
387+
default:
388+
throw new IllegalArgumentException("Unsupported alter configs mode " + alterConfigsMode);
389+
}
390+
}
391+
392+
private void alterTopicConfigWithIncrementalAlterConfigs(final Admin admin,
393+
final String topic,
394+
final Map<String, String> newConfigs) throws Exception {
362395
final ConfigResource topicResource = new ConfigResource(ConfigResource.Type.TOPIC, topic);
363396
final Collection<AlterConfigOp> operations = newConfigs.entrySet().stream()
364397
.map(entry -> new AlterConfigOp(new ConfigEntry(entry.getKey(), entry.getValue()), AlterConfigOp.OpType.SET))
365398
.toList();
366399
admin.incrementalAlterConfigs(Map.of(topicResource, operations)).all().get(20, TimeUnit.SECONDS);
367400
}
368401

402+
private void alterTopicConfigWithLegacyAlterConfigs(final String topic,
403+
final Map<String, String> newConfigs) throws Exception {
404+
final ConfigResource topicResource = new ConfigResource(ConfigResource.Type.TOPIC, topic);
405+
final Collection<AlterConfigsRequest.ConfigEntry> configEntries = newConfigs.entrySet().stream()
406+
.map(entry -> new AlterConfigsRequest.ConfigEntry(entry.getKey(), entry.getValue()))
407+
.toList();
408+
final AlterConfigsRequest.Config config = new AlterConfigsRequest.Config(configEntries);
409+
final AlterConfigsRequest request = new AlterConfigsRequest.Builder(Map.of(topicResource, config), false)
410+
.build(ApiKeys.ALTER_CONFIGS.latestVersion());
411+
final AlterConfigsResponse response = IntegrationTestUtils.connectAndReceive(request, firstBrokerPort());
412+
final var apiError = response.errors().get(topicResource);
413+
assertTrue(apiError != null, "Legacy AlterConfigs response did not contain topic resource " + topicResource);
414+
assertEquals(Errors.NONE, apiError.error(), "Legacy AlterConfigs failed: " + apiError.message());
415+
}
416+
417+
private int firstBrokerPort() {
418+
final String firstBootstrapServer = cluster.bootstrapServers().split(",")[0];
419+
return Integer.parseInt(firstBootstrapServer.substring(firstBootstrapServer.lastIndexOf(':') + 1));
420+
}
421+
369422
private Map<String, String> getTopicConfig(final Admin admin, final String topic) throws Exception {
370423
int maxRetries = 5;
371424
long retryDelayMs = 1000;

metadata/src/main/java/org/apache/kafka/controller/QuorumController.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,6 +2010,14 @@ public CompletableFuture<Map<ConfigResource, ApiError>> legacyAlterConfigs(
20102010
if (validateOnly) {
20112011
return result.withoutRecords();
20122012
} else {
2013+
List<ApiMessageAndVersion> migrationRecords =
2014+
replicationControl.markClassicToDisklessSwitchStartedForLegacyAlterConfigs(newConfigs, result.response());
2015+
if (!migrationRecords.isEmpty()) {
2016+
List<ApiMessageAndVersion> allRecords = BoundedList.newArrayBacked(MAX_RECORDS_PER_USER_OP);
2017+
allRecords.addAll(result.records());
2018+
allRecords.addAll(migrationRecords);
2019+
return ControllerResult.atomicOf(allRecords, result.response());
2020+
}
20132021
return result;
20142022
}
20152023
});

metadata/src/main/java/org/apache/kafka/controller/ReplicationControlManager.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3010,6 +3010,28 @@ List<ApiMessageAndVersion> markClassicToDisklessSwitchStarted(
30103010
return records;
30113011
}
30123012

3013+
/**
3014+
* Legacy AlterConfigs provides complete config maps rather than per-key operations.
3015+
* Adapt that input and reuse {@link #markClassicToDisklessSwitchStarted(Map, Map)}
3016+
* so both legacy and incremental alter configs emit the same switch-pending records.
3017+
*/
3018+
List<ApiMessageAndVersion> markClassicToDisklessSwitchStartedForLegacyAlterConfigs(
3019+
Map<ConfigResource, Map<String, String>> newConfigs,
3020+
Map<ConfigResource, ApiError> configResults
3021+
) {
3022+
Map<ConfigResource, Map<String, Entry<OpType, String>>> configChanges = new HashMap<>();
3023+
for (Entry<ConfigResource, Map<String, String>> entry : newConfigs.entrySet()) {
3024+
String disklessEnable = entry.getValue().get(DISKLESS_ENABLE_CONFIG);
3025+
if (disklessEnable != null) {
3026+
configChanges.put(entry.getKey(), Map.of(
3027+
DISKLESS_ENABLE_CONFIG,
3028+
new SimpleImmutableEntry<>(SET, disklessEnable)
3029+
));
3030+
}
3031+
}
3032+
return markClassicToDisklessSwitchStarted(configChanges, configResults);
3033+
}
3034+
30133035
private boolean isDisklessTopic(String topicName) {
30143036
return Boolean.parseBoolean(
30153037
configurationControl.currentTopicConfig(topicName)

0 commit comments

Comments
 (0)