Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -961,9 +961,10 @@ private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback call
// producer callback will make sure to call both 'callback' and interceptor callback
Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);

if (transactionManager != null && transactionManager.isTransactional()) {
transactionManager.failIfNotReadyForSend();
if (transactionManager != null) {
transactionManager.maybeAddPartition(tp);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way we did this before was a little strange. Logically, it makes more sense for the partition to be added to the transaction before we append the record to the accumulator.

}

RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
serializedValue, headers, interceptCallback, remainingWaitMs, true, nowMs);

Expand All @@ -982,9 +983,6 @@ private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback call
serializedValue, headers, interceptCallback, remainingWaitMs, false, nowMs);
}

if (transactionManager != null && transactionManager.isTransactional())
transactionManager.maybeAddPartitionToTransaction(tp);

if (result.batchIsFull || result.newBatchCreated) {
log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
this.sender.wakeup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ void resetSequenceNumbers(Consumer<ProducerBatch> resetSequence) {
private final Set<TopicPartition> newPartitionsInTransaction;
private final Set<TopicPartition> pendingPartitionsInTransaction;
private final Set<TopicPartition> partitionsInTransaction;
private TransactionalRequestResult pendingResult;
private PendingStateTransition pendingTransition;

// This is used by the TxnRequestHandlers to control how long to back off before a given request is retried.
// For instance, this value is lowered by the AddPartitionsToTxnHandler when it receives a CONCURRENT_TRANSACTIONS
Expand Down Expand Up @@ -329,6 +329,8 @@ public synchronized TransactionalRequestResult initializeTransactions() {
}

synchronized TransactionalRequestResult initializeTransactions(ProducerIdAndEpoch producerIdAndEpoch) {
maybeFailWithError();

boolean isEpochBump = producerIdAndEpoch != ProducerIdAndEpoch.NONE;
return handleCachedTransactionRequestResult(() -> {
// If this is an epoch bump, we will transition the state as part of handling the EndTxnRequest
Expand All @@ -347,11 +349,12 @@ synchronized TransactionalRequestResult initializeTransactions(ProducerIdAndEpoc
isEpochBump);
enqueueRequest(handler);
return handler.result;
}, State.INITIALIZING);
}, State.INITIALIZING, "initTransactions");
}

public synchronized void beginTransaction() {
ensureTransactional();
throwIfPendingState("beginTransaction");
maybeFailWithError();
transitionTo(State.IN_TRANSACTION);
}
Expand All @@ -361,7 +364,7 @@ public synchronized TransactionalRequestResult beginCommit() {
maybeFailWithError();
transitionTo(State.COMMITTING_TRANSACTION);
return beginCompletingTransaction(TransactionResult.COMMIT);
}, State.COMMITTING_TRANSACTION);
}, State.COMMITTING_TRANSACTION, "commitTransaction");
}

public synchronized TransactionalRequestResult beginAbort() {
Expand All @@ -373,7 +376,7 @@ public synchronized TransactionalRequestResult beginAbort() {
// We're aborting the transaction, so there should be no need to add new partitions
newPartitionsInTransaction.clear();
return beginCompletingTransaction(TransactionResult.ABORT);
}, State.ABORTING_TRANSACTION);
}, State.ABORTING_TRANSACTION, "abortTransaction");
}

private TransactionalRequestResult beginCompletingTransaction(TransactionResult transactionResult) {
Expand Down Expand Up @@ -404,10 +407,13 @@ private TransactionalRequestResult beginCompletingTransaction(TransactionResult
public synchronized TransactionalRequestResult sendOffsetsToTransaction(final Map<TopicPartition, OffsetAndMetadata> offsets,
final ConsumerGroupMetadata groupMetadata) {
ensureTransactional();
throwIfPendingState("sendOffsetsToTransaction");
maybeFailWithError();
if (currentState != State.IN_TRANSACTION)
throw new KafkaException("Cannot send offsets to transaction either because the producer is not in an " +
"active transaction");

if (currentState != State.IN_TRANSACTION) {
throw new IllegalStateException("Cannot send offsets if a transaction is not in progress " +
"(currentState= " + currentState + ")");
}

log.debug("Begin adding offsets {} for consumer group {} to transaction", offsets, groupMetadata);
AddOffsetsToTxnRequest.Builder builder = new AddOffsetsToTxnRequest.Builder(
Expand All @@ -423,34 +429,31 @@ public synchronized TransactionalRequestResult sendOffsetsToTransaction(final Ma
return handler.result;
}

public synchronized void maybeAddPartitionToTransaction(TopicPartition topicPartition) {
if (isPartitionAdded(topicPartition) || isPartitionPendingAdd(topicPartition))
return;
public synchronized void maybeAddPartition(TopicPartition topicPartition) {
maybeFailWithError();
throwIfPendingState("send");

log.debug("Begin adding new partition {} to transaction", topicPartition);
topicPartitionBookkeeper.addPartition(topicPartition);
newPartitionsInTransaction.add(topicPartition);
if (isTransactional()) {
if (!hasProducerId()) {
throw new IllegalStateException("Cannot add partition " + topicPartition +
" to transaction before completing a call to initTransactions");
} else if (currentState != State.IN_TRANSACTION) {
throw new IllegalStateException("Cannot add partition " + topicPartition +
" to transaction while in state " + currentState);
} else if (isPartitionAdded(topicPartition) || isPartitionPendingAdd(topicPartition)) {
return;
} else {
log.debug("Begin adding new partition {} to transaction", topicPartition);
topicPartitionBookkeeper.addPartition(topicPartition);
newPartitionsInTransaction.add(topicPartition);
}
}
}

RuntimeException lastError() {
return lastError;
}

public synchronized void failIfNotReadyForSend() {
if (hasError())
throw new KafkaException("Cannot perform send because at least one previous transactional or " +
"idempotent request has failed with errors.", lastError);

if (isTransactional()) {
if (!hasProducerId())
throw new IllegalStateException("Cannot perform a 'send' before completing a call to initTransactions " +
"when transactions are enabled.");

if (currentState != State.IN_TRANSACTION)
throw new IllegalStateException("Cannot call send in state " + currentState);
}
}

synchronized boolean isSendToPartitionAllowed(TopicPartition tp) {
if (hasFatalError())
return false;
Expand Down Expand Up @@ -500,8 +503,8 @@ synchronized void transitionToFatalError(RuntimeException exception) {
log.info("Transiting to fatal error state due to {}", exception.toString());
transitionTo(State.FATAL_ERROR, exception);

if (pendingResult != null) {
pendingResult.fail(exception);
if (pendingTransition != null) {
pendingTransition.result.fail(exception);
}
}

Expand Down Expand Up @@ -919,8 +922,8 @@ synchronized void close() {
KafkaException shutdownException = new KafkaException("The producer closed forcefully");
pendingRequests.forEach(handler ->
handler.fatalError(shutdownException));
if (pendingResult != null) {
pendingResult.fail(shutdownException);
if (pendingTransition != null) {
pendingTransition.result.fail(shutdownException);
}
}

Expand Down Expand Up @@ -1073,7 +1076,7 @@ private void transitionTo(State target) {
private void transitionTo(State target, RuntimeException error) {
if (!currentState.isTransitionValid(currentState, target)) {
String idString = transactionalId == null ? "" : "TransactionalId " + transactionalId + ": ";
throw new KafkaException(idString + "Invalid transition attempted from state "
throw new IllegalStateException(idString + "Invalid transition attempted from state "
+ currentState.name() + " to state " + target.name());
}

Expand Down Expand Up @@ -1103,10 +1106,12 @@ private void maybeFailWithError() {
// for ProducerFencedException, do not wrap it as a KafkaException
// but create a new instance without the call trace since it was not thrown because of the current call
if (lastError instanceof ProducerFencedException) {
throw new ProducerFencedException("The producer has been rejected from the broker because " +
"it tried to use an old epoch with the transactionalId");
throw new ProducerFencedException("Producer with transactionalId '" + transactionalId
+ "' and " + producerIdAndEpoch + " has been fenced by another producer " +
"with the same transactionalId");
} else if (lastError instanceof InvalidProducerEpochException) {
throw new InvalidProducerEpochException("Producer attempted to produce with an old epoch " + producerIdAndEpoch);
throw new InvalidProducerEpochException("Producer with transactionalId '" + transactionalId
+ "' and " + producerIdAndEpoch + " attempted to produce with an old epoch");
} else {
throw new KafkaException("Cannot execute transactional method because we are in an error state", lastError);
}
Expand Down Expand Up @@ -1183,20 +1188,40 @@ private TxnOffsetCommitHandler txnOffsetCommitHandler(TransactionalRequestResult
return new TxnOffsetCommitHandler(result, builder);
}

private void throwIfPendingState(String operation) {
if (pendingTransition != null) {
if (pendingTransition.result.isAcked()) {
pendingTransition = null;
} else {
throw new IllegalStateException("Cannot attempt operation `" + operation + "` "
+ "because the previous call to `" + pendingTransition.operation + "` "
+ "timed out and must be retried");
}
}
}

private TransactionalRequestResult handleCachedTransactionRequestResult(
Supplier<TransactionalRequestResult> transactionalRequestResultSupplier,
State targetState) {
Supplier<TransactionalRequestResult> transactionalRequestResultSupplier,
State nextState,
String operation
) {
ensureTransactional();

if (pendingResult != null && currentState == targetState) {
TransactionalRequestResult result = pendingResult;
if (result.isCompleted())
pendingResult = null;
return result;
if (pendingTransition != null) {
if (pendingTransition.result.isAcked()) {
pendingTransition = null;
} else if (nextState != pendingTransition.state) {
throw new IllegalStateException("Cannot attempt operation `" + operation + "` "
+ "because the previous call to `" + pendingTransition.operation + "` "
+ "timed out and must be retried");
} else {
return pendingTransition.result;
}
}

pendingResult = transactionalRequestResultSupplier.get();
return pendingResult;
TransactionalRequestResult result = transactionalRequestResultSupplier.get();
pendingTransition = new PendingStateTransition(result, nextState, operation);
return result;
}

// package-private for testing
Expand Down Expand Up @@ -1762,4 +1787,22 @@ private boolean isFatalException(Errors error) {
|| error == Errors.PRODUCER_FENCED
|| error == Errors.UNSUPPORTED_FOR_MESSAGE_FORMAT;
}

private static final class PendingStateTransition {
private final TransactionalRequestResult result;
private final State state;
private final String operation;

private PendingStateTransition(
TransactionalRequestResult result,
State state,
String operation
) {
this.result = result;
this.state = state;
this.operation = operation;
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@
import org.apache.kafka.common.errors.InterruptException;
import org.apache.kafka.common.errors.TimeoutException;

import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public final class TransactionalRequestResult {

private final CountDownLatch latch;
private volatile RuntimeException error = null;
private final String operation;
private volatile boolean isAcked = false;

public TransactionalRequestResult(String operation) {
this(new CountDownLatch(1), operation);
Expand All @@ -49,29 +48,20 @@ public void done() {
}

public void await() {
boolean completed = false;

while (!completed) {
try {
latch.await();
completed = true;
} catch (InterruptedException e) {
// Keep waiting until done, we have no other option for these transactional requests.
}
}

if (!isSuccessful())
throw error();
this.await(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
}

public void await(long timeout, TimeUnit unit) {
try {
boolean success = latch.await(timeout, unit);
if (!isSuccessful()) {
throw error();
}
if (!success) {
throw new TimeoutException("Timeout expired after " + timeout + " " + unit.name().toLowerCase(Locale.ROOT) + " while awaiting " + operation);
throw new TimeoutException("Timeout expired after " + unit.toMillis(timeout) +
"ms while awaiting " + operation);
}

isAcked = true;
if (error != null) {
throw error;
}
} catch (InterruptedException e) {
throw new InterruptException("Received interrupt while awaiting " + operation, e);
Expand All @@ -83,11 +73,15 @@ public RuntimeException error() {
}

public boolean isSuccessful() {
return error == null;
return isCompleted() && error == null;
}

public boolean isCompleted() {
return latch.getCount() == 0L;
}

public boolean isAcked() {
return isAcked;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public boolean isValid() {

@Override
public String toString() {
return "(producerId=" + producerId + ", epoch=" + epoch + ")";
return "ProducerIdAndEpoch(producerId=" + producerId + ", epoch=" + epoch + ")";
}

@Override
Expand Down
Loading