Skip to content

Commit fd1f3ac

Browse files
committed
Improve CI test logging; stabilize share consumer and batch ITs
Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
1 parent 6ce7d8d commit fd1f3ac

File tree

4 files changed

+92
-14
lines changed

4 files changed

+92
-14
lines changed

build.gradle

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,48 @@ configure(javaProjects) { subproject ->
157157
[compileJava, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing', '-parameters']
158158

159159
test {
160+
// Default timeout for every testable method (unless @Timeout). Stops hung tests instead of wedging CI.
161+
// Override: ./gradlew test -Djunit.jupiter.execution.timeout.default=PT20M or -PjunitTimeoutDefault=PT20M
162+
def junitTimeoutDefault = System.getProperty('junit.jupiter.execution.timeout.default')
163+
?: findProperty('junitTimeoutDefault')
164+
?: 'PT10M'
165+
systemProperty 'junit.jupiter.execution.timeout.default', junitTimeoutDefault
166+
160167
testLogging {
161-
events 'skipped', 'failed'
168+
// STARTED: last line before a hang usually identifies the stuck method. Class summary in afterSuite.
169+
events 'started', 'failed'
162170
showStandardStreams = project.hasProperty('showStandardStreams') ?: false
163171
showExceptions = true
164172
showStackTraces = true
165173
exceptionFormat = 'full'
166174
}
167175

176+
afterSuite { desc, result ->
177+
// Top-level class only (skip engine root, nested classes, and empty suites)
178+
if (desc.className == null || result.testCount == 0 || desc.parent?.className != null) {
179+
return
180+
}
181+
long classMs = result.endTime - result.startTime
182+
String duration = classMs >= 60_000
183+
? String.format("%.2f min", classMs / 60_000.0)
184+
: classMs >= 10_000
185+
? String.format("%.2fs", classMs / 1000.0)
186+
: "${classMs}ms"
187+
String fqn = desc.className
188+
String simpleName = fqn.contains('.') ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn
189+
StringBuilder summary = new StringBuilder()
190+
summary.append(simpleName).append(" finished in ").append(duration)
191+
.append(" (").append(result.testCount).append(" tests")
192+
if (result.failedTestCount > 0) {
193+
summary.append(", ").append(result.failedTestCount).append(" failed")
194+
}
195+
if (result.skippedTestCount > 0) {
196+
summary.append(", ").append(result.skippedTestCount).append(" skipped")
197+
}
198+
summary.append(')')
199+
logger.lifecycle(summary.toString())
200+
}
201+
168202
maxHeapSize = '1536m'
169203
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
170204
useJUnitPlatform()

spring-kafka/src/test/java/org/springframework/kafka/annotation/BatchListenerConversion2Tests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public void testBatchOfPojosWithABadOne() throws Exception {
7575
this.template.send(topic, "{\"bar\":\"baz\"}");
7676
this.template.send(topic, "junk");
7777
this.template.send(topic, "{\"bar\":\"baz\"}");
78+
this.template.flush();
7879
assertThat(listener.latch1.await(10, TimeUnit.SECONDS)).isTrue();
7980
assertThat(listener.badFoo).isInstanceOf(BadFoo.class);
8081
assertThat(listener.receivedFoos).isEqualTo(2);

spring-kafka/src/test/java/org/springframework/kafka/core/DefaultShareConsumerFactoryTests.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,19 @@ void integrationTestDefaultShareConsumerFactory(EmbeddedKafkaBroker broker) thro
151151
final String groupId = "testGroup";
152152
var bootstrapServers = broker.getBrokersAsString();
153153

154+
// Apply group config before producing so a joining share consumer consistently uses earliest.
155+
setShareAutoOffsetResetEarliest(bootstrapServers, groupId);
156+
154157
var producerProps = new java.util.Properties();
155158
producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
156159
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
157160
producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
158161

159162
try (var producer = new KafkaProducer<String, String>(producerProps)) {
160163
producer.send(new ProducerRecord<>(topic, "key", "integration-test-value")).get();
164+
producer.flush();
161165
}
162166

163-
setShareAutoOffsetResetEarliest(bootstrapServers, groupId);
164-
165167
var consumerProps = new HashMap<String, Object>();
166168
consumerProps.put("bootstrap.servers", bootstrapServers);
167169
consumerProps.put("key.deserializer", StringDeserializer.class);
@@ -172,11 +174,21 @@ void integrationTestDefaultShareConsumerFactory(EmbeddedKafkaBroker broker) thro
172174
var consumer = factory.createShareConsumer(groupId, "myapp-client-id");
173175
consumer.subscribe(Collections.singletonList(topic));
174176

175-
var records = consumer.poll(Duration.ofSeconds(10));
176-
assertThat(records.count())
177+
// First poll(s) may be empty (assignment / share coordinator); avoid a single-poll race on CI.
178+
int total = 0;
179+
String value = null;
180+
long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30);
181+
while (total == 0 && System.currentTimeMillis() < deadline) {
182+
var records = consumer.poll(Duration.ofSeconds(1));
183+
total += records.count();
184+
if (!records.isEmpty()) {
185+
value = records.iterator().next().value();
186+
}
187+
}
188+
assertThat(total)
177189
.as("Should have received at least one record")
178190
.isGreaterThan(0);
179-
assertThat(records.iterator().next().value())
191+
assertThat(value)
180192
.as("Record value should match")
181193
.isEqualTo("integration-test-value");
182194
consumer.close();
@@ -213,6 +225,8 @@ private static List<String> runSharedConsumerTest(String topic, String groupId,
213225
List<String> consumerIds, int recordCount, EmbeddedKafkaBroker broker) throws Exception {
214226
var bootstrapServers = broker.getBrokersAsString();
215227

228+
setShareAutoOffsetResetEarliest(bootstrapServers, groupId);
229+
216230
var producerProps = new java.util.Properties();
217231
producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
218232
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
@@ -224,8 +238,6 @@ private static List<String> runSharedConsumerTest(String topic, String groupId,
224238
producer.flush();
225239
}
226240

227-
setShareAutoOffsetResetEarliest(bootstrapServers, groupId);
228-
229241
List<String> allReceived = Collections.synchronizedList(new ArrayList<>());
230242
var latch = new java.util.concurrent.CountDownLatch(recordCount);
231243
ExecutorService executor = Executors.newCachedThreadPool();
@@ -255,11 +267,11 @@ private static List<String> runSharedConsumerTest(String topic, String groupId,
255267
});
256268
}
257269

258-
assertThat(latch.await(10, TimeUnit.SECONDS))
270+
assertThat(latch.await(30, TimeUnit.SECONDS))
259271
.as("All records should be received within timeout")
260272
.isTrue();
261273
executor.shutdown();
262-
assertThat(executor.awaitTermination(10, TimeUnit.SECONDS))
274+
assertThat(executor.awaitTermination(30, TimeUnit.SECONDS))
263275
.as("Executor should terminate after shutdown")
264276
.isTrue();
265277
return allReceived;

spring-kafka/src/test/java/org/springframework/kafka/listener/ShareKafkaListenerIntegrationTests.java

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,17 @@
3636
import org.apache.kafka.common.config.ConfigResource;
3737
import org.apache.kafka.common.serialization.StringDeserializer;
3838
import org.apache.kafka.common.serialization.StringSerializer;
39+
import org.awaitility.Awaitility;
3940
import org.jspecify.annotations.Nullable;
4041
import org.junit.jupiter.api.Test;
4142

4243
import org.springframework.beans.factory.annotation.Autowired;
44+
import org.springframework.beans.factory.annotation.Qualifier;
4345
import org.springframework.context.annotation.Bean;
4446
import org.springframework.context.annotation.Configuration;
4547
import org.springframework.kafka.annotation.EnableKafka;
4648
import org.springframework.kafka.annotation.KafkaListener;
49+
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
4750
import org.springframework.kafka.config.ShareKafkaListenerContainerFactory;
4851
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
4952
import org.springframework.kafka.core.DefaultShareConsumerFactory;
@@ -75,6 +78,7 @@
7578
"share-listener-error-handling-test",
7679
"share-listener-factory-props-test"
7780
},
81+
partitions = 1,
7882
brokerProperties = {
7983
"share.coordinator.state.topic.replication.factor=1",
8084
"share.coordinator.state.topic.min.isr=1"
@@ -87,6 +91,9 @@ class ShareKafkaListenerIntegrationTests {
8791
@Autowired
8892
KafkaTemplate<String, String> kafkaTemplate;
8993

94+
@Autowired
95+
KafkaListenerEndpointRegistry kafkaListenerEndpointRegistry;
96+
9097
@Test
9198
void shouldSupportBasicShareKafkaListener() throws Exception {
9299
final String topic = "share-listener-basic-test";
@@ -95,6 +102,7 @@ void shouldSupportBasicShareKafkaListener() throws Exception {
95102

96103
// Send test message
97104
kafkaTemplate.send(topic, "basic-test-message");
105+
kafkaTemplate.flush();
98106

99107
// Wait for processing
100108
assertThat(BasicTestListener.latch.await(10, TimeUnit.SECONDS)).isTrue();
@@ -111,6 +119,7 @@ void shouldSupportExplicitAcknowledgmentWithShareAcknowledgment() throws Excepti
111119
kafkaTemplate.send(topic, "accept", "accept-message");
112120
kafkaTemplate.send(topic, "release", "release-message");
113121
kafkaTemplate.send(topic, "reject", "reject-message");
122+
kafkaTemplate.flush();
114123

115124
// Wait for processing
116125
assertThat(ExplicitAckTestListener.latch.await(15, TimeUnit.SECONDS)).isTrue();
@@ -134,6 +143,7 @@ void shouldSupportShareConsumerAwareListener() throws Exception {
134143

135144
// Send test message
136145
kafkaTemplate.send(topic, "consumer-aware-message");
146+
kafkaTemplate.flush();
137147

138148
// Wait for processing
139149
assertThat(ShareConsumerAwareTestListener.latch.await(10, TimeUnit.SECONDS)).isTrue();
@@ -147,8 +157,12 @@ void shouldSupportAcknowledgingShareConsumerAwareListener() throws Exception {
147157
final String groupId = "share-ack-consumer-aware-group";
148158
setShareAutoOffsetResetEarliest(this.broker.getBrokersAsString(), groupId);
149159

160+
// This test can run first (JUnit 6); wait for containers before producing so share consumers are subscribed.
161+
awaitRunningShareListenerContainers();
162+
150163
// Send test message
151164
kafkaTemplate.send(topic, "ack-consumer-aware-message");
165+
kafkaTemplate.flush();
152166

153167
// Wait for processing
154168
assertThat(AckShareConsumerAwareTestListener.latch.await(30, TimeUnit.SECONDS)).isTrue();
@@ -168,6 +182,7 @@ void shouldHandleMixedAcknowledgmentScenarios() throws Exception {
168182
kafkaTemplate.send(topic, "success1", "success-message-1");
169183
kafkaTemplate.send(topic, "success2", "success-message-2");
170184
kafkaTemplate.send(topic, "retry", "retry-message");
185+
kafkaTemplate.flush();
171186

172187
// Wait for processing
173188
assertThat(MixedAckTestListener.processedLatch.await(15, TimeUnit.SECONDS)).isTrue();
@@ -189,6 +204,7 @@ void shouldHandleProcessingErrorsCorrectly() throws Exception {
189204
kafkaTemplate.send(topic, "success", "success-message");
190205
kafkaTemplate.send(topic, "error", "error-message");
191206
kafkaTemplate.send(topic, "success2", "success-message-2");
207+
kafkaTemplate.flush();
192208

193209
// Wait for processing
194210
assertThat(ErrorHandlingTestListener.latch.await(10, TimeUnit.SECONDS)).isTrue();
@@ -206,6 +222,7 @@ void shouldSupportExplicitAcknowledgmentViaFactoryContainerProperties() throws E
206222

207223
// Send test message
208224
kafkaTemplate.send(topic, "factory-test", "factory-props-message");
225+
kafkaTemplate.flush();
209226

210227
// Wait for processing
211228
assertThat(FactoryPropsTestListener.latch.await(10, TimeUnit.SECONDS)).isTrue();
@@ -243,6 +260,20 @@ private boolean isAcknowledgedInternal(ShareAcknowledgment ack) {
243260
}
244261
}
245262

263+
/**
264+
* Wait until all listener containers have started (needed when a test runs before others).
265+
*/
266+
private void awaitRunningShareListenerContainers() {
267+
Awaitility.await()
268+
.atMost(30, TimeUnit.SECONDS)
269+
.pollInterval(100, TimeUnit.MILLISECONDS)
270+
.untilAsserted(() -> {
271+
assertThat(this.kafkaListenerEndpointRegistry.getListenerContainerIds()).isNotEmpty();
272+
this.kafkaListenerEndpointRegistry.getListenerContainers().forEach(container ->
273+
assertThat(container.isRunning()).isTrue());
274+
});
275+
}
276+
246277
@Configuration
247278
@EnableKafka
248279
static class TestConfig {
@@ -262,7 +293,7 @@ public ShareConsumerFactory<String, String> explicitShareConsumerFactory(Embedde
262293
configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, broker.getBrokersAsString());
263294
configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
264295
configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
265-
configs.put("share.acknowledgement.mode", "explicit");
296+
configs.put(ConsumerConfig.SHARE_ACKNOWLEDGEMENT_MODE_CONFIG, "explicit");
266297
return new DefaultShareConsumerFactory<>(configs);
267298
}
268299

@@ -280,10 +311,10 @@ public ShareKafkaListenerContainerFactory<String, String> explicitShareKafkaList
280311

281312
@Bean
282313
public ShareKafkaListenerContainerFactory<String, String> factoryPropsShareKafkaListenerContainerFactory(
283-
ShareConsumerFactory<String, String> shareConsumerFactory) {
314+
@Qualifier("explicitShareConsumerFactory") ShareConsumerFactory<String, String> explicitShareConsumerFactory) {
284315
ShareKafkaListenerContainerFactory<String, String> factory =
285-
new ShareKafkaListenerContainerFactory<>(shareConsumerFactory);
286-
// Configure explicit acknowledgment via factory's container properties
316+
new ShareKafkaListenerContainerFactory<>(explicitShareConsumerFactory);
317+
// Configure explicit acknowledgment via factory's container properties (consumer must use explicit mode too)
287318
factory.getContainerProperties().setExplicitShareAcknowledgment(true);
288319
return factory;
289320
}

0 commit comments

Comments
 (0)