3434import org .apache .kafka .clients .producer .ProducerRecord ;
3535import org .apache .kafka .common .config .ConfigResource ;
3636import 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 ;
3741import org .apache .kafka .common .serialization .ByteArrayDeserializer ;
3842import org .apache .kafka .common .serialization .ByteArraySerializer ;
3943import org .apache .kafka .common .test .KafkaClusterTestKit ;
4044import org .apache .kafka .common .test .TestKitNodes ;
4145import org .apache .kafka .coordinator .group .GroupCoordinatorConfig ;
46+ import org .apache .kafka .server .IntegrationTestUtils ;
4247import org .apache .kafka .server .config .ReplicationConfigs ;
4348import org .apache .kafka .server .config .ServerConfigs ;
4449import org .apache .kafka .server .log .remote .storage .RemoteLogManagerConfig ;
4550
4651import org .junit .jupiter .api .AfterEach ;
4752import org .junit .jupiter .api .BeforeEach ;
48- import org .junit .jupiter .api .Test ;
4953import org .junit .jupiter .api .TestInfo ;
54+ import org .junit .jupiter .params .ParameterizedTest ;
55+ import org .junit .jupiter .params .provider .EnumSource ;
5056import org .slf4j .Logger ;
5157import org .slf4j .LoggerFactory ;
5258import 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 ;
0 commit comments