Skip to content

Commit bfccd92

Browse files
garyrussellartembilan
authored andcommitted
GH-661: Close Producer after beginTransaction fail
Fixes #661 I could not reproduce the problem that I reported in the issue; so, if any exception occurs on `beginTransaction()`, throw the exception to the caller after closing the producer and prevent its return to the cache. Also, reading the javadocs for `ProducerFencedException`, we should have been closing the producer if that exception occurred anyway. ``` /** * This fatal exception indicates that another producer with the same <code>transactional.id</code> has been * started. It is only possible to have one producer instance with a <code>transactional.id</code> at any * given time, and the latest one to be started "fences" the previous instances so that they can no longer * make transactional requests. When you encounter this exception, you must close the producer instance. */ ``` Cherry-pick to master, 2.0.x, 1.3.x. (cherry picked from commit f48ad87)
1 parent 83e3a45 commit bfccd92

File tree

3 files changed

+144
-4
lines changed

3 files changed

+144
-4
lines changed

Diff for: spring-kafka/src/main/java/org/springframework/kafka/core/DefaultKafkaProducerFactory.java

+42-3
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,21 @@ public Producer<K, V> createProducer() {
216216
return this.producer;
217217
}
218218

219+
/**
220+
* Subclasses must return a raw producer which will be wrapped in a
221+
* {@link CloseSafeProducer}.
222+
* @return the producer.
223+
*/
219224
protected Producer<K, V> createKafkaProducer() {
220225
return new KafkaProducer<K, V>(this.configs, this.keySerializer, this.valueSerializer);
221226
}
222227

228+
/**
229+
* Subclasses must return a producer from the {@link #getCache()} or a
230+
* new raw producer wrapped in a {@link CloseSafeProducer}.
231+
* @return the producer - cannot be null.
232+
* @since 1.3
233+
*/
223234
protected Producer<K, V> createTransactionalProducer() {
224235
Producer<K, V> producer = this.cache.poll();
225236
if (producer == null) {
@@ -235,14 +246,28 @@ protected Producer<K, V> createTransactionalProducer() {
235246
}
236247
}
237248

238-
private static class CloseSafeProducer<K, V> implements Producer<K, V> {
249+
protected BlockingQueue<CloseSafeProducer<K, V>> getCache() {
250+
return this.cache;
251+
}
252+
253+
/**
254+
* A wrapper class for the delegate.
255+
*
256+
* @param <K> the key type.
257+
* @param <V> the value type.
258+
*
259+
*/
260+
protected static class CloseSafeProducer<K, V> implements Producer<K, V> {
239261

240262
private final Producer<K, V> delegate;
241263

242264
private final BlockingQueue<CloseSafeProducer<K, V>> cache;
243265

266+
private volatile boolean txFailed;
267+
244268
CloseSafeProducer(Producer<K, V> delegate) {
245269
this(delegate, null);
270+
Assert.isTrue(!(delegate instanceof CloseSafeProducer), "Cannot double-wrap a producer");
246271
}
247272

248273
CloseSafeProducer(Producer<K, V> delegate, BlockingQueue<CloseSafeProducer<K, V>> cache) {
@@ -282,7 +307,21 @@ public void initTransactions() {
282307

283308
@Override
284309
public void beginTransaction() throws ProducerFencedException {
285-
this.delegate.beginTransaction();
310+
try {
311+
this.delegate.beginTransaction();
312+
}
313+
catch (RuntimeException e) {
314+
this.txFailed = true;
315+
logger.error("Illegal transaction state; producer removed from cache; possible cause: "
316+
+ "broker restarted during transaction", e);
317+
try {
318+
this.delegate.close();
319+
}
320+
catch (Exception ee) {
321+
// empty
322+
}
323+
throw e;
324+
}
286325
}
287326

288327
@Override
@@ -303,7 +342,7 @@ public void abortTransaction() throws ProducerFencedException {
303342

304343
@Override
305344
public void close() {
306-
if (this.cache != null) {
345+
if (this.cache != null && !this.txFailed) {
307346
synchronized (this) {
308347
if (!this.cache.contains(this)) {
309348
this.cache.offer(this);

Diff for: spring-kafka/src/main/java/org/springframework/kafka/core/KafkaTemplate.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,8 @@ public <T> T executeInTransaction(OperationsCallback<K, V, T> callback) {
258258
Producer<K, V> producer = this.producers.get();
259259
Assert.state(producer == null, "Nested calls to 'executeInTransaction' are not allowed");
260260
producer = this.producerFactory.createProducer();
261-
this.producers.set(producer);
262261
producer.beginTransaction();
262+
this.producers.set(producer);
263263
T result = null;
264264
try {
265265
result = callback.doInOperations(this);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.kafka.core;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.BDDMockito.willAnswer;
22+
import static org.mockito.Mockito.inOrder;
23+
import static org.mockito.Mockito.mock;
24+
25+
import java.util.HashMap;
26+
import java.util.concurrent.BlockingQueue;
27+
import java.util.concurrent.atomic.AtomicInteger;
28+
29+
import org.apache.kafka.clients.producer.Producer;
30+
import org.apache.kafka.common.KafkaException;
31+
import org.junit.jupiter.api.Test;
32+
import org.mockito.InOrder;
33+
34+
import org.springframework.kafka.test.utils.KafkaTestUtils;
35+
import org.springframework.kafka.transaction.KafkaTransactionManager;
36+
import org.springframework.transaction.CannotCreateTransactionException;
37+
import org.springframework.transaction.support.TransactionTemplate;
38+
39+
/**
40+
* @author Gary Russell
41+
* @since 1.3.5
42+
*
43+
*/
44+
public class DefaultKafkaProducerFactoryTests {
45+
46+
@SuppressWarnings({ "rawtypes", "unchecked" })
47+
@Test
48+
public void testProducerClosedAfterBadTransition() throws Exception {
49+
final Producer producer = mock(Producer.class);
50+
DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory(new HashMap<>()) {
51+
52+
@Override
53+
protected Producer createTransactionalProducer() {
54+
producer.initTransactions();
55+
BlockingQueue<Producer> cache = getCache();
56+
Producer cached = cache.poll();
57+
return cached == null ? new CloseSafeProducer(producer, cache) : cached;
58+
}
59+
60+
};
61+
pf.setTransactionIdPrefix("foo");
62+
63+
final AtomicInteger flag = new AtomicInteger();
64+
willAnswer(i -> {
65+
if (flag.incrementAndGet() == 2) {
66+
throw new KafkaException("Invalid transition ...");
67+
}
68+
return null;
69+
}).given(producer).beginTransaction();
70+
71+
final KafkaTemplate kafkaTemplate = new KafkaTemplate(pf);
72+
KafkaTransactionManager tm = new KafkaTransactionManager(pf);
73+
TransactionTemplate transactionTemplate = new TransactionTemplate(tm);
74+
transactionTemplate.execute(s -> {
75+
kafkaTemplate.send("foo", "bar");
76+
return null;
77+
});
78+
BlockingQueue cache = KafkaTestUtils.getPropertyValue(pf, "cache", BlockingQueue.class);
79+
assertThat(cache).hasSize(1);
80+
try {
81+
transactionTemplate.execute(s -> {
82+
return null;
83+
});
84+
}
85+
catch (CannotCreateTransactionException e) {
86+
assertThat(e.getCause().getMessage()).contains("Invalid transition");
87+
}
88+
assertThat(cache).hasSize(0);
89+
90+
InOrder inOrder = inOrder(producer);
91+
inOrder.verify(producer).initTransactions();
92+
inOrder.verify(producer).beginTransaction();
93+
inOrder.verify(producer).send(any(), any());
94+
inOrder.verify(producer).commitTransaction();
95+
inOrder.verify(producer).beginTransaction();
96+
inOrder.verify(producer).close();
97+
inOrder.verifyNoMoreInteractions();
98+
pf.destroy();
99+
}
100+
101+
}

0 commit comments

Comments
 (0)