MartijnVisser commented on code in PR #314:
URL: 
https://github.com/apache/flink-connector-kafka/pull/314#discussion_r3981061938


##########
flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/sink/internal/KafkaCommitter.java:
##########
@@ -184,6 +188,33 @@ private void logFencedRequest(
         }
     }
 
+    /**
+     * With reused transactional ids, a fenced commit during recovery usually 
means that the
+     * transaction was committed before the failure and its id was recycled 
for a later checkpoint,
+     * whose transaction is still open on the broker under a newer epoch. 
Nobody owns that
+     * transaction any more: the writer skips the id as precommitted and this 
committer cannot
+     * commit it. Bumping the epoch aborts it, so that it does not block 
read_committed consumers
+     * until the transaction timeout expires.
+     */
+    private void abortNewerTransaction(String transactionalId) {
+        FlinkKafkaInternalProducer<?, ?> producer =
+                producerFactory.apply(kafkaProducerConfig, transactionalId);
+        try {
+            producer.initTransactions();
+            LOG.info(
+                    "Aborted open transaction of a newer epoch under {} after 
its commit was fenced.",
+                    transactionalId);
+        } catch (KafkaException e) {

Review Comment:
   `org.apache.kafka.common.errors.InterruptException extends KafkaException`, 
so a cancel arriving during the epoch bump is swallowed here and only surfaces 
on the next request in the batch. `commit()` converts that exception into 
`InterruptedException` a few lines up precisely because the producer is left 
inconsistent.
   
   Skip the abort when the thread is already interrupted.



##########
flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/sink/internal/KafkaCommitter.java:
##########
@@ -108,6 +109,9 @@ public void 
commit(Collection<CommitRequest<KafkaCommittable>> requests)
                 request.retryLater();
             } catch (ProducerFencedException e) {
                 logFencedRequest(request, e);
+                if (reusesTransactionalIds) {
+                    abortNewerTransaction(transactionalId);

Review Comment:
   Blocking, as a question. `ProducerFencedException` means someone bumped the 
epoch under this id. This reads that as "our own recycled transaction, now 
orphaned" and bumps it again — but the warning right below names two other 
readings, and under either of those the id has a live owner.
   
   I checked both directions against a broker. On `main`: the orphan stays 
`Ongoing`, and a live owner of the same id commits fine. On this branch: the 
orphan is aborted, and the live owner gets `ProducerFencedException: There is a 
newer producer with the same transactionalId which fences the current one.` on 
its commit.
   
   The case I can't rule out is a task manager that has lost its heartbeat but 
is still running: the job restarts elsewhere, the new attempt's writer takes 
the pooled id over, and the old committer's `commit()` then arrives and is 
fenced. Today that committer loses, which is what the fencing is for; with this 
change it wins and fails the healthy attempt over.
   
   Please either show that window is closed, or bump only where the transaction 
is provably orphaned — the committable carries `(producerId, epoch)` and 
`Admin.describeTransactions` returns the broker's current epoch.



##########
flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/sink/internal/KafkaCommitter.java:
##########
@@ -184,6 +188,33 @@ private void logFencedRequest(
         }
     }
 
+    /**
+     * With reused transactional ids, a fenced commit during recovery usually 
means that the
+     * transaction was committed before the failure and its id was recycled 
for a later checkpoint,
+     * whose transaction is still open on the broker under a newer epoch. 
Nobody owns that
+     * transaction any more: the writer skips the id as precommitted and this 
committer cannot
+     * commit it. Bumping the epoch aborts it, so that it does not block 
read_committed consumers
+     * until the transaction timeout expires.
+     */
+    private void abortNewerTransaction(String transactionalId) {
+        FlinkKafkaInternalProducer<?, ?> producer =
+                producerFactory.apply(kafkaProducerConfig, transactionalId);
+        try {
+            producer.initTransactions();

Review Comment:
   `initTransactions()` blocks up to `max.block.ms`. `KafkaSinkBuilder` sets 
`transaction.timeout.ms` but never `max.block.ms`, so the default 60s applies 
per fenced request, serially through the request loop; a scale-down that 
concentrates ids on one committer multiplies it. `KafkaCommitterTest` doesn't 
see this because `getProperties()` pins it to 100.
   
   Please copy `kafkaProducerConfig` and set a short `max.block.ms` for this 
throwaway producer.



##########
flink-connector-kafka/src/test/java/org/apache/flink/connector/kafka/sink/internal/KafkaCommitterTest.java:
##########
@@ -219,6 +221,49 @@ private AtomicBoolean interruptOnMessage(Thread 
mainThread, ServerSocket serverS
         return interrupting;
     }
 
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testFencedCommitAbortsNewerTransactionOnlyIfIdsAreReused(
+            boolean reusesTransactionalIds) throws IOException, 
InterruptedException {
+        Properties properties = getProperties();
+        List<MockProducer> createdProducers = new ArrayList<>();
+        BiFunction<Properties, String, FlinkKafkaInternalProducer<?, ?>> 
fencingFactory =
+                (props, transactionalId) -> {
+                    MockProducer producer =
+                            new MockProducer(props, new 
ProducerFencedException("test"));
+                    createdProducers.add(producer);
+                    return producer;
+                };
+        try (final KafkaCommitter committer =
+                        new KafkaCommitter(
+                                properties,
+                                TRANS_ID,
+                                SUB_ID,
+                                ATTEMPT,
+                                reusesTransactionalIds,
+                                fencingFactory);
+                ReadableBackchannel<TransactionFinished> backchannel =
+                        BackchannelFactory.getInstance()
+                                .getReadableBackchannel(SUB_ID, ATTEMPT, 
TRANS_ID)) {
+            // committable restored from state: the committer resumes it with 
its own producer
+            final MockCommitRequest<KafkaCommittable> request =
+                    new MockCommitRequest<>(
+                            new KafkaCommittable(PRODUCER_ID, EPOCH, TRANS_ID, 
null));
+            committer.commit(Collections.singletonList(request));
+
+            assertThat(backchannel).has(transactionFinished(false));
+            assertThat(committer.getCommittingProducer()).isNull();
+            
assertThat(createdProducers).allMatch(FlinkKafkaInternalProducer::isClosed);
+            // with reused ids the fenced id may hold a newer, orphaned 
transaction; bumping the
+            // epoch through initTransactions aborts it
+            long abortedIds =
+                    createdProducers.stream()
+                            .filter(MockProducer::isTransactionsInitialized)
+                            .count();
+            assertThat(abortedIds).isEqualTo(reusesTransactionalIds ? 1 : 0);

Review Comment:
   `MockProducer` is constructed as `super(properties, 
KafkaCommitterTest.TRANS_ID)` (line 316), so it ignores the id the factory is 
handed, and `initTransactions()` only sets a flag. This asserts that some 
producer was re-inited — not that the right id was, and not that anything was 
aborted; passing the wrong id to `producerFactory` would still pass.
   
   Assert the id the factory captured, and add a broker-level case. 
`ExactlyOnceKafkaWriterITCase` already has the container fixture and 
`AdminUtils.getOpenTransactionsForTopics`, which is enough to assert no 
`ONGOING` transaction survives.



-- 
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