Croway commented on PR #28730:
URL: https://github.com/apache/camel/pull/28730#issuecomment-6544237001

   @ramu11 here it is. Drop it into 
`components/camel-kafka/src/test/java/org/apache/camel/component/kafka/integration/`
 and run:
   
   ```
   mvn -pl components/camel-kafka-common,components/camel-kafka verify 
-Dit.test=KafkaExactlyOnceReReviewIT -Dtest=skip 
-Dsurefire.failIfNoSpecifiedTests=false
   ```
   
   It uses the test-infra Kafka container. Each test creates its own 
CamelContext, topics, group and `transactionalId`, and asserts the **correct** 
exactly-once outcome, so a failing test is a remaining problem. Lines starting 
with `REPRO` in `target/camel-kafka-test.log` hold what each test observed 
(attempts per record key, committed output, committed source offset, failures).
   
   | Test | Scenario | At `f9d92812` |
   |---|---|---|
   | `o1`..`o9` | first review round (rollbackOnly, removeHeaders, split + 
aggregation, seda after the producer, DLC without redelivery, filter, producer 
without exactlyOnce, startup rejections, wireTap next to a failing send) | pass 
|
   | `n1`, `n2` | Kafka DLQ topic, plain or exactly-once endpoint (point 1) | 
**fail**: offset committed, DLQ topic empty |
   | `n3` | DLC with `maximumRedeliveries(2)` | pass (record skipped, DLQ once) 
|
   | `n4`, `n5` | seda or wireTap as the only path to Kafka (point 2) | 
**fail**: offset committed, output empty |
   | `n6` | split fan-out, each child sends (point 3) | **fail**: replays 
forever |
   | `n7` | two routes sharing one sink URI / `transactionalId` | pass, logs 
the fencing failure |
   | `n8` | split + `groupedBody()`, then one send (the fan-out pattern that 
works) | pass |
   
   `n1`, `n2`, `n4`, `n5` and `n6` should pass once the three points are 
addressed. Feel free to take whatever is useful into the PR's own ITs.
   
   <details>
   <summary><code>KafkaExactlyOnceReReviewIT.java</code></summary>
   
   ```java
   /*
    * Licensed to the Apache Software Foundation (ASF) under one or more
    * contributor license agreements.  See the NOTICE file distributed with
    * this work for additional information regarding copyright ownership.
    * The ASF licenses this file to You under the Apache License, Version 2.0
    * (the "License"); you may not use this file except in compliance with
    * the License.  You may obtain a copy of the License at
    *
    *      http://www.apache.org/licenses/LICENSE-2.0
    *
    * Unless required by applicable law or agreed to in writing, software
    * distributed under the License is distributed on an "AS IS" BASIS,
    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    * See the License for the specific language governing permissions and
    * limitations under the License.
    */
   package org.apache.camel.component.kafka.integration;
   
   import java.time.Duration;
   import java.util.ArrayList;
   import java.util.LinkedHashMap;
   import java.util.List;
   import java.util.Map;
   import java.util.Properties;
   import java.util.concurrent.Callable;
   import java.util.concurrent.ConcurrentHashMap;
   import java.util.concurrent.CopyOnWriteArrayList;
   import java.util.concurrent.TimeUnit;
   import java.util.concurrent.atomic.AtomicBoolean;
   import java.util.concurrent.atomic.AtomicInteger;
   
   import org.apache.camel.CamelContext;
   import org.apache.camel.Exchange;
   import org.apache.camel.Processor;
   import org.apache.camel.builder.AggregationStrategies;
   import org.apache.camel.builder.RouteBuilder;
   import org.apache.camel.component.kafka.KafkaComponent;
   import org.apache.camel.component.kafka.KafkaConstants;
   import org.apache.camel.impl.DefaultCamelContext;
   import org.apache.camel.spi.CamelEvent;
   import org.apache.camel.support.EventNotifierSupport;
   import org.apache.camel.test.infra.kafka.services.KafkaService;
   import org.apache.camel.test.infra.kafka.services.KafkaServiceFactory;
   import org.apache.kafka.clients.admin.AdminClient;
   import org.apache.kafka.clients.admin.AdminClientConfig;
   import org.apache.kafka.clients.admin.NewTopic;
   import org.apache.kafka.clients.consumer.ConsumerConfig;
   import org.apache.kafka.clients.consumer.KafkaConsumer;
   import org.apache.kafka.clients.consumer.OffsetAndMetadata;
   import org.apache.kafka.clients.producer.KafkaProducer;
   import org.apache.kafka.clients.producer.ProducerConfig;
   import org.apache.kafka.clients.producer.ProducerRecord;
   import org.apache.kafka.common.TopicPartition;
   import org.apache.kafka.common.serialization.StringDeserializer;
   import org.apache.kafka.common.serialization.StringSerializer;
   import org.awaitility.core.ConditionTimeoutException;
   import org.junit.jupiter.api.AfterEach;
   import org.junit.jupiter.api.BeforeEach;
   import org.junit.jupiter.api.Test;
   import org.junit.jupiter.api.TestInfo;
   import org.junit.jupiter.api.extension.RegisterExtension;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import static org.awaitility.Awaitility.await;
   import static org.junit.jupiter.api.Assertions.assertEquals;
   import static org.junit.jupiter.api.Assertions.assertThrows;
   import static org.junit.jupiter.api.Assertions.assertTrue;
   import static org.junit.jupiter.api.Assertions.fail;
   
   /**
    * Exactly-once scenarios from the review of PR #28730. Every test asserts 
the correct exactly-once outcome, so a
    * failing test is a remaining problem. Lines starting with "REPRO" in 
target/camel-kafka-test.log hold the observed
    * values (attempts per record key, committed output, committed source 
offset, failures).
    * <p>
    * o1..o9 cover the first review round and pass at f9d92812. n1..n8 are the 
second round: at f9d92812 n1, n2 (Kafka DLQ
    * topic), n4, n5 (seda/wireTap as the only path to Kafka) and n6 (split 
fan-out) fail; n3, n7 and n8 pass, and n7 logs
    * the fencing between two producers sharing one transactionalId.
    * <p>
    * Run with: mvn -pl components/camel-kafka-common,components/camel-kafka 
verify -Dit.test=KafkaExactlyOnceReReviewIT
    * -Dtest=skip -Dsurefire.failIfNoSpecifiedTests=false
    */
   class KafkaExactlyOnceReReviewIT {
   
       private static final Logger LOG = 
LoggerFactory.getLogger(KafkaExactlyOnceReReviewIT.class);
   
       @RegisterExtension
       static KafkaService service = 
KafkaServiceFactory.createSingletonService();
   
       private final Map<String, AtomicInteger> attempts = new 
ConcurrentHashMap<>();
       private final List<String> failures = new CopyOnWriteArrayList<>();
       private final List<CamelContext> contexts = new ArrayList<>();
       private AdminClient admin;
       private String id;
   
       @BeforeEach
       void setUp(TestInfo info) {
           id = info.getTestMethod().orElseThrow().getName() + "-" + 
Long.toHexString(System.nanoTime());
           admin = 
AdminClient.create(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, boot()));
       }
   
       @AfterEach
       void tearDown() {
           for (CamelContext context : contexts) {
               context.stop();
           }
           admin.close();
       }
   
       // 
=================================================================================================================
       // Original findings: expected to be fixed
       // 
=================================================================================================================
   
       @Test
       void o1MarkRollbackOnlyReplaysTheRecord() throws Exception {
           String in = topics("in", "out")[0];
           AtomicBoolean rolledBack = new AtomicBoolean();
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   
from(source(in)).routeId(id).process(countAttempt()).to(sink(id + "-out", "tx"))
                           .filter(e -> "m1".equals(key(e)) && 
rolledBack.compareAndSet(false, true))
                           .markRollbackOnly().end();
               }
           });
           produce(in, "m0", "m1", "m2");
           awaitCommitted(in, 3);
           List<String> out = readCommittedKeys(id + "-out");
           LOG.info("REPRO o1 rollbackOnly: attempts={} out={} committed={}", 
attempts, out, committed(id, in));
           assertEquals(List.of("m0", "m1", "m2"), out);
           assertEquals(2, attempts("m1"));
       }
   
       @Test
       void o2RemoveHeadersKeepsTheOffsets() throws Exception {
           String in = topics("in", "out")[0];
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   
from(source(in)).routeId(id).process(countAttempt()).removeHeaders("*").to(sink(id
 + "-out", "tx"));
               }
           });
           produce(in, "m0", "m1");
           awaitCommitted(in, 2);
           List<String> out = readCommittedValues(id + "-out");
           LOG.info("REPRO o2 removeHeaders: attempts={} out={}", attempts, 
out);
           assertEquals(List.of("m0", "m1"), out);
       }
   
       @Test
       void o3SplitWithAggregationStrategySendsTheParent() throws Exception {
           String in = topics("in", "out")[0];
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   from(source(in)).routeId(id).process(countAttempt())
                           .split(body().tokenize(","), 
AggregationStrategies.useLatest()).transform(body().append("!")).end()
                           .to(sink(id + "-out", "tx"));
               }
           });
           produce(in, Map.of("m0", "a,b,c"));
           awaitCommitted(in, 1);
           List<String> out = readCommittedValues(id + "-out");
           LOG.info("REPRO o3 split+aggregation: attempts={} out={}", attempts, 
out);
           assertEquals(List.of("c!"), out);
           assertEquals(1, attempts("m0"));
       }
   
       @Test
       void o4SedaAfterTheProducerDoesNotWedgeTheRoute() throws Exception {
           String in = topics("in", "out")[0];
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   
from(source(in)).routeId(id).process(countAttempt()).to(sink(id + "-out", 
"tx")).to("seda:audit");
                   from("seda:audit").to("log:audit?level=DEBUG");
               }
           });
           produce(in, "m0", "m1", "m2");
           awaitCommitted(in, 3);
           List<String> out = readCommittedKeys(id + "-out");
           LOG.info("REPRO o4 seda after producer: attempts={} out={} 
failures={}", attempts, out, sample());
           assertEquals(List.of("m0", "m1", "m2"), out);
       }
   
       @Test
       void o5DeadLetterChannelWithoutRedeliverySkipsThePoisonRecord() throws 
Exception {
           String in = topics("in", "out")[0];
           AtomicInteger dlq = new AtomicInteger();
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   errorHandler(deadLetterChannel("direct:dlq"));
                   from("direct:dlq").process(e -> dlq.incrementAndGet());
                   from(source(in)).routeId(id).process(countAttempt())
                           .to(sink(id + "-out", "tx") + 
"&maxRequestSize=1024");
               }
           });
           produce(in, poisonRecords());
           awaitCommitted(in, 3);
           List<String> out = readCommittedKeys(id + "-out");
           LOG.info("REPRO o5 DLC no redelivery: attempts={} dlq={} out={}", 
attempts, dlq.get(), out);
           assertEquals(List.of("small-0", "small-2"), out);
           assertEquals(1, dlq.get());
       }
   
       @Test
       void o6FilteredRecordsCommitTheirOffsets() throws Exception {
           String in = topics("in", "out")[0];
           CamelContext context = start(new RouteBuilder() {
               @Override
               public void configure() {
                   from(source(in)).routeId(id).process(countAttempt())
                           
.filter(header(KafkaConstants.KEY).startsWith("m")).to(sink(id + "-out", "tx"));
               }
           });
           produce(in, "m0", "skip1", "skip2", "skip3");
           awaitCommitted(in, 4);
           context.getRouteController().stopRoute(id);
           context.getRouteController().startRoute(id);
           produce(in, "m4");
           awaitCommitted(in, 5);
           LOG.info("REPRO o6 filter: attempts={}", attempts);
           assertEquals(1, attempts("skip1"), "not consumed again after the 
restart");
       }
   
       @Test
       void o7ProducerWithoutExactlyOnceIsRejectedAndTheRecordReplays() throws 
Exception {
           String in = topics("in", "out")[0];
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   
from(source(in)).routeId(id).process(countAttempt()).to("kafka:" + id + 
"-out?transacted=true");
               }
           });
           produce(in, "m0");
           await().atMost(60, TimeUnit.SECONDS).until(() -> attempts("m0") >= 
2);
           LOG.info("REPRO o7 producer without exactlyOnce: attempts={} 
committed={} out={} failures={}", attempts,
                   committed(id, in), readCommittedKeys(id + "-out"), sample());
           assertEquals(null, committed(id, in));
           assertTrue(readCommittedKeys(id + "-out").isEmpty());
       }
   
       @Test
       void o8RepositioningOptionsAreRejectedAtStartup() throws Exception {
           String in = topics("in")[0];
           for (String extra : List.of("&seekTo=BEGINNING", "&seekTo=END", 
"&batching=true", "&consumersCount=2")) {
               CamelContext context = newContext();
               context.addRoutes(new RouteBuilder() {
                   @Override
                   public void configure() {
                       from(source(in) + extra).to(sink(id + "-out", "tx"));
                   }
               });
               Exception e = assertThrows(Exception.class, context::start, 
extra);
               LOG.info("REPRO o8 {} rejected: {}", extra, rootMessage(e));
               context.stop();
           }
       }
   
       @Test
       void o9WireTapNextToTheMainSendCannotSkipAFailedRecord() throws 
Exception {
           String in = topics("in", "out", "audit")[0];
           AtomicBoolean failed = new AtomicBoolean();
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   from(source(in)).routeId(id).process(countAttempt())
                           .wireTap("direct:audit")
                           .to(sink(id + "-out", "tx"))
                           .delay(1000)
                           .process(failOnce("m1", failed));
                   from("direct:audit").to(sink(id + "-audit", "audit"));
               }
           });
           produce(in, "m0", "m1", "m2");
           awaitCommitted(in, 3);
           List<String> out = readCommittedKeys(id + "-out");
           LOG.info("REPRO o9 wireTap next to main send: attempts={} out={} 
audit={} failures={}", attempts, out,
                   readCommittedKeys(id + "-audit"), sample());
           assertEquals(List.of("m0", "m1", "m2"), out);
       }
   
       // 
=================================================================================================================
       // New probes
       // 
=================================================================================================================
   
       /** A Kafka DLQ topic is the usual dead letter channel of a Kafka route. 
*/
       @Test
       void n1DeadLetterChannelToAKafkaTopic() throws Exception {
           deadLetterToKafka("kafka:" + id + "-dlq");
       }
   
       @Test
       void n2DeadLetterChannelToAnExactlyOnceKafkaTopic() throws Exception {
           deadLetterToKafka("kafka:" + id + 
"-dlq?exactlyOnce=true&transactionalId=" + id + "-dlq-tx");
       }
   
       private void deadLetterToKafka(String dlqUri) throws Exception {
           String in = topics("in", "out", "dlq")[0];
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   errorHandler(deadLetterChannel(dlqUri));
                   from(source(in)).routeId(id).process(countAttempt())
                           .to(sink(id + "-out", "tx") + 
"&maxRequestSize=1024");
               }
           });
           produce(in, poisonRecords());
           boolean committed3 = awaitQuietly(() -> 
Long.valueOf(3).equals(committed(id, in)), 45);
           List<String> out = readCommittedKeys(id + "-out");
           List<String> dlq = readCommittedKeys(id + "-dlq");
           LOG.info("REPRO {}: dlqUri={} committedOffset={} out={} dlqTopic={} 
attempts={} failures={}", id, dlqUri,
                   committed(id, in), out, dlq, attempts, sample());
           if (committed3) {
               assertTrue(dlq.contains("big-1"), "the offset moved past big-1, 
so it must be in the DLQ topic: " + dlq);
           } else {
               fail("the route never moved past big-1: attempts=" + attempts);
           }
       }
   
       /** A dead letter channel usually redelivers a few times first. */
       @Test
       void n3DeadLetterChannelWithRedeliveries() throws Exception {
           String in = topics("in", "out")[0];
           AtomicInteger dlq = new AtomicInteger();
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   
errorHandler(deadLetterChannel("direct:dlq").maximumRedeliveries(2).redeliveryDelay(0));
                   from("direct:dlq").process(e -> dlq.incrementAndGet());
                   from(source(in)).routeId(id).process(countAttempt())
                           .to(sink(id + "-out", "tx") + 
"&maxRequestSize=1024");
               }
           });
           produce(in, poisonRecords());
           boolean committed3 = awaitQuietly(() -> 
Long.valueOf(3).equals(committed(id, in)), 45);
           LOG.info("REPRO n3 DLC maximumRedeliveries(2): committed3={} 
committedOffset={} attempts={} dlq={} out={}"
                    + " failures={}",
                   committed3, committed(id, in), attempts, dlq.get(), 
readCommittedKeys(id + "-out"), sample());
           assertTrue(committed3, "the poison record is never skipped: 
attempts=" + attempts + " dlq=" + dlq.get());
           assertEquals(1, dlq.get());
       }
   
       /** The only Kafka send is in a SEDA route. */
       @Test
       void n4SedaIsTheOnlyPathToKafka() throws Exception {
           onlyKafkaPathIsACopy(true);
       }
   
       /** The only Kafka send is behind a wireTap. */
       @Test
       void n5WireTapIsTheOnlyPathToKafka() throws Exception {
           onlyKafkaPathIsACopy(false);
       }
   
       private void onlyKafkaPathIsACopy(boolean seda) throws Exception {
           String in = topics("in", "out")[0];
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   if (seda) {
                       
from(source(in)).routeId(id).process(countAttempt()).to("seda:out");
                       from("seda:out").to(sink(id + "-out", "tx"));
                   } else {
                       
from(source(in)).routeId(id).process(countAttempt()).wireTap("direct:out").to("log:main?level=DEBUG");
                       from("direct:out").to(sink(id + "-out", "tx"));
                   }
               }
           });
           produce(in, "m0", "m1", "m2");
           boolean committed3 = awaitQuietly(() -> 
Long.valueOf(3).equals(committed(id, in)), 30);
           List<String> out = readCommittedKeys(id + "-out");
           LOG.info("REPRO {}: committedOffset={} out={} attempts={} 
failures={}", id, committed(id, in), out, attempts,
                   sample());
           if (committed3) {
               assertEquals(List.of("m0", "m1", "m2"), out, "offsets committed, 
so the outputs must exist");
           }
       }
   
       /** One input record fans out into several output records. */
       @Test
       void n6SplitFanOutToKafka() throws Exception {
           String in = topics("in", "out")[0];
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   from(source(in)).routeId(id).process(countAttempt())
                           .split(body().tokenize(",")).to(sink(id + "-out", 
"tx")).end();
               }
           });
           produce(in, Map.of("m0", "a,b,c"));
           boolean committed1 = awaitQuietly(() -> 
Long.valueOf(1).equals(committed(id, in)), 30);
           List<String> out = readCommittedValues(id + "-out");
           LOG.info("REPRO n6 split fan-out: committed1={} committedOffset={} 
out={} attempts={} failures={}", committed1,
                   committed(id, in), out, attempts, sample());
           if (committed1) {
               assertEquals(List.of("a", "b", "c"), out, "offset committed, so 
every split output must exist once");
           }
       }
   
       /** Workaround for the fan-out: one send of an iterable body, so every 
record is in one transaction. */
       @Test
       void n8FanOutAsOneIterableSend() throws Exception {
           String in = topics("in", "out")[0];
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   from(source(in)).routeId(id).process(countAttempt())
                           .split(body().tokenize(","), 
AggregationStrategies.groupedBody()).transform(body().append("!")).end()
                           .to(sink(id + "-out", "tx"));
               }
           });
           produce(in, Map.of("m0", "a,b,c"));
           boolean committed1 = awaitQuietly(() -> 
Long.valueOf(1).equals(committed(id, in)), 30);
           List<String> out = readCommittedValues(id + "-out");
           LOG.info("REPRO n8 fan-out as one iterable send: committed1={} 
out={} attempts={} failures={}", committed1, out,
                   attempts, sample());
           assertTrue(committed1);
           assertEquals(List.of("a!", "b!", "c!"), out);
       }
   
       /** Two routes write to the same sink endpoint URI, so to the same 
transactionalId. */
       @Test
       void n7TwoRoutesShareOneSinkUri() throws Exception {
           String in1 = topics("in1", "in2", "out")[0];
           String in2 = id + "-in2";
           String sink = sink(id + "-out", "shared");
           start(new RouteBuilder() {
               @Override
               public void configure() {
                   from(source(in1, id + "-g1")).routeId(id + 
"-r1").process(countAttempt()).to(sink);
                   from(source(in2, id + "-g2")).routeId(id + 
"-r2").process(countAttempt()).to(sink);
               }
           });
           String[] a = new String[10];
           String[] b = new String[10];
           for (int i = 0; i < 10; i++) {
               a[i] = "a" + i;
               b[i] = "b" + i;
           }
           long start = System.currentTimeMillis();
           produce(in1, a);
           produce(in2, b);
           boolean drained = awaitQuietly(() -> readCommittedKeys(id + 
"-out").size() >= 20, 90);
           List<String> out = readCommittedKeys(id + "-out");
           long fenced = failures.stream().filter(f -> f.contains("Fenced") || 
f.contains("fenced")).count();
           LOG.info("REPRO n7 shared transactionalId: drained={} in {} ms 
out={} (distinct {}) attempts={} fencedFailures={}"
                    + " failures={}",
                   drained, System.currentTimeMillis() - start, out.size(), 
out.stream().distinct().count(),
                   
attempts.values().stream().mapToInt(AtomicInteger::get).sum(), fenced, 
sample());
           assertTrue(drained, "not all records delivered");
           assertEquals(20, out.size(), "exactly once");
       }
   
       // 
=================================================================================================================
       // helpers
       // 
=================================================================================================================
   
       private static String boot() {
           return service.getBootstrapServers();
       }
   
       private String source(String topic) {
           return source(topic, id);
       }
   
       private static String source(String topic, String group) {
           return "kafka:" + topic + "?groupId=" + group + 
"&exactlyOnce=true&autoOffsetReset=earliest"
                  + 
"&autoCommitEnable=false&allowManualCommit=true&breakOnFirstError=true&pollTimeoutMs=1000";
       }
   
       private String sink(String topic, String txSuffix) {
           return "kafka:" + topic + "?exactlyOnce=true&transactionalId=" + id 
+ "-" + txSuffix + "&requestRequiredAcks=-1";
       }
   
       private static Map<String, String> poisonRecords() {
           Map<String, String> records = new LinkedHashMap<>();
           records.put("small-0", "small");
           records.put("big-1", "x".repeat(4000));
           records.put("small-2", "small");
           return records;
       }
   
       private CamelContext newContext() {
           CamelContext context = new DefaultCamelContext();
           KafkaComponent kafka = new KafkaComponent(context);
           kafka.getConfiguration().setBrokers(boot());
           context.addComponent("kafka", kafka);
           context.getManagementStrategy().addEventNotifier(new 
EventNotifierSupport() {
               @Override
               public void notify(CamelEvent event) {
                   if (event instanceof CamelEvent.ExchangeFailedEvent failed) {
                       
failures.add(String.valueOf(failed.getExchange().getException()));
                   }
               }
           });
           return context;
       }
   
       private CamelContext start(RouteBuilder routes) throws Exception {
           CamelContext context = newContext();
           context.addRoutes(routes);
           context.start();
           contexts.add(context);
           return context;
       }
   
       private static String key(Exchange e) {
           return e.getMessage().getHeader(KafkaConstants.KEY, String.class);
       }
   
       private Processor countAttempt() {
           return e -> attempts.computeIfAbsent(key(e), k -> new 
AtomicInteger()).incrementAndGet();
       }
   
       private static Processor failOnce(String key, AtomicBoolean failed) {
           return e -> {
               if (key.equals(key(e)) && failed.compareAndSet(false, true)) {
                   throw new IllegalStateException("failure injected by the 
test after the send");
               }
           };
       }
   
       private int attempts(String key) {
           AtomicInteger count = attempts.get(key);
           return count == null ? 0 : count.get();
       }
   
       private List<String> sample() {
           return failures.stream().distinct().limit(3).toList();
       }
   
       private static String rootMessage(Throwable t) {
           while (t.getCause() != null) {
               t = t.getCause();
           }
           return t.getMessage();
       }
   
       private static boolean awaitQuietly(Callable<Boolean> condition, int 
seconds) {
           try {
               await().atMost(seconds, TimeUnit.SECONDS).until(condition);
               return true;
           } catch (ConditionTimeoutException e) {
               return false;
           }
       }
   
       private void awaitCommitted(String topic, long offset) {
           await().atMost(60, TimeUnit.SECONDS).until(() -> 
Long.valueOf(offset).equals(committed(id, topic)));
       }
   
       /** Creates the topics id-name and returns their names. */
       private String[] topics(String... names) throws Exception {
           String[] topics = new String[names.length];
           try (AdminClient client = 
AdminClient.create(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, boot()))) 
{
               List<NewTopic> newTopics = new ArrayList<>();
               for (int i = 0; i < names.length; i++) {
                   topics[i] = id + "-" + names[i];
                   newTopics.add(new NewTopic(topics[i], 1, (short) 1));
               }
               client.createTopics(newTopics).all().get(30, TimeUnit.SECONDS);
           }
           return topics;
       }
   
       private static void produce(String topic, String... keys) {
           Map<String, String> records = new LinkedHashMap<>();
           for (String key : keys) {
               records.put(key, key);
           }
           produce(topic, records);
       }
   
       private static void produce(String topic, Map<String, String> records) {
           Properties props = new Properties();
           props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, boot());
           props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, 
StringSerializer.class.getName());
           props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, 
StringSerializer.class.getName());
           try (KafkaProducer<String, String> producer = new 
KafkaProducer<>(props)) {
               records.forEach((k, v) -> producer.send(new 
ProducerRecord<>(topic, k, v)));
               producer.flush();
           }
       }
   
       private static List<String> readCommittedKeys(String topic) {
           return readCommitted(topic, true);
       }
   
       private static List<String> readCommittedValues(String topic) {
           return readCommitted(topic, false);
       }
   
       private static List<String> readCommitted(String topic, boolean keys) {
           Properties props = new Properties();
           props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, boot());
           props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
           props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
           props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 
StringDeserializer.class.getName());
           props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, 
StringDeserializer.class.getName());
           List<String> result = new ArrayList<>();
           try (KafkaConsumer<String, String> consumer = new 
KafkaConsumer<>(props)) {
               List<TopicPartition> partitions = consumer.partitionsFor(topic, 
Duration.ofSeconds(10)).stream()
                       .map(p -> new TopicPartition(topic, 
p.partition())).toList();
               consumer.assign(partitions);
               consumer.seekToBeginning(partitions);
               Map<TopicPartition, Long> end = consumer.endOffsets(partitions);
               long deadline = System.currentTimeMillis() + 15000;
               while (System.currentTimeMillis() < deadline
                       && partitions.stream().anyMatch(tp -> 
consumer.position(tp) < end.get(tp))) {
                   consumer.poll(Duration.ofMillis(200)).forEach(r -> 
result.add(keys ? r.key() : r.value()));
               }
           }
           return result;
       }
   
       private Long committed(String group, String topic) throws Exception {
           OffsetAndMetadata offset = 
admin.listConsumerGroupOffsets(group).partitionsToOffsetAndMetadata()
                   .get(30, TimeUnit.SECONDS).get(new TopicPartition(topic, 0));
           return offset == null ? null : offset.offset();
       }
   }
   ```
   
   </details>
   
   _Claude Code on behalf of Croway_
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to