-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-5886: Implement KIP-91 delivery.timeout.ms #3849
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,6 +99,12 @@ public class ProducerConfig extends AbstractConfig { | |
| + "specified time waiting for more records to show up. This setting defaults to 0 (i.e. no delay). Setting <code>" + LINGER_MS_CONFIG + "=5</code>, " | ||
| + "for example, would have the effect of reducing the number of requests sent but would add up to 5ms of latency to records sent in the absence of load."; | ||
|
|
||
| /** <code>delivery.timeout.ms</code> */ | ||
| public static final String DELIVERY_TIMEOUT_MS_CONFIG = "delivery.timeout.ms"; | ||
| private static final String DELIVERY_TIMEOUT_MS_DOC = "An upper bound on the time to report success or failure after Producer.send() returns. " | ||
| + "Producer may report failure to send a message earlier than this config if all the retries are exhausted or " | ||
| + "a record is added to a batch nearing expiration."; | ||
|
|
||
| /** <code>client.id</code> */ | ||
| public static final String CLIENT_ID_CONFIG = CommonClientConfigs.CLIENT_ID_CONFIG; | ||
|
|
||
|
|
@@ -224,7 +230,7 @@ public class ProducerConfig extends AbstractConfig { | |
| static { | ||
| CONFIG = new ConfigDef().define(BOOTSTRAP_SERVERS_CONFIG, Type.LIST, Collections.emptyList(), new ConfigDef.NonNullValidator(), Importance.HIGH, CommonClientConfigs.BOOTSTRAP_SERVERS_DOC) | ||
| .define(BUFFER_MEMORY_CONFIG, Type.LONG, 32 * 1024 * 1024L, atLeast(0L), Importance.HIGH, BUFFER_MEMORY_DOC) | ||
| .define(RETRIES_CONFIG, Type.INT, 0, between(0, Integer.MAX_VALUE), Importance.HIGH, RETRIES_DOC) | ||
| .define(RETRIES_CONFIG, Type.INT, Integer.MAX_VALUE, between(0, Integer.MAX_VALUE), Importance.HIGH, RETRIES_DOC) | ||
| .define(ACKS_CONFIG, | ||
| Type.STRING, | ||
| "1", | ||
|
|
@@ -234,6 +240,7 @@ public class ProducerConfig extends AbstractConfig { | |
| .define(COMPRESSION_TYPE_CONFIG, Type.STRING, "none", Importance.HIGH, COMPRESSION_TYPE_DOC) | ||
| .define(BATCH_SIZE_CONFIG, Type.INT, 16384, atLeast(0), Importance.MEDIUM, BATCH_SIZE_DOC) | ||
| .define(LINGER_MS_CONFIG, Type.LONG, 0, atLeast(0L), Importance.MEDIUM, LINGER_MS_DOC) | ||
| .define(DELIVERY_TIMEOUT_MS_CONFIG, Type.LONG, 120 * 1000, atLeast(0L), Importance.MEDIUM, DELIVERY_TIMEOUT_MS_DOC) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we validate the delivery.timeout.ms is greater than request.timeout.ms? |
||
| .define(CLIENT_ID_CONFIG, Type.STRING, "", Importance.MEDIUM, CommonClientConfigs.CLIENT_ID_DOC) | ||
| .define(SEND_BUFFER_CONFIG, Type.INT, 128 * 1024, atLeast(-1), Importance.MEDIUM, CommonClientConfigs.SEND_BUFFER_DOC) | ||
| .define(RECEIVE_BUFFER_CONFIG, Type.INT, 32 * 1024, atLeast(-1), Importance.MEDIUM, CommonClientConfigs.RECEIVE_BUFFER_DOC) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,13 +77,13 @@ private enum FinalState { ABORTED, FAILED, SUCCEEDED } | |
| private boolean retry; | ||
| private boolean reopened = false; | ||
|
|
||
| public ProducerBatch(TopicPartition tp, MemoryRecordsBuilder recordsBuilder, long now) { | ||
| this(tp, recordsBuilder, now, false); | ||
| public ProducerBatch(TopicPartition tp, MemoryRecordsBuilder recordsBuilder, long creationTime) { | ||
| this(tp, recordsBuilder, creationTime, false); | ||
| } | ||
|
|
||
| public ProducerBatch(TopicPartition tp, MemoryRecordsBuilder recordsBuilder, long now, boolean isSplitBatch) { | ||
| this.createdMs = now; | ||
| this.lastAttemptMs = now; | ||
| public ProducerBatch(TopicPartition tp, MemoryRecordsBuilder recordsBuilder, long creationTime, boolean isSplitBatch) { | ||
| this.createdMs = creationTime; | ||
| this.lastAttemptMs = creationTime; | ||
| this.recordsBuilder = recordsBuilder; | ||
| this.topicPartition = tp; | ||
| this.lastAppendTime = createdMs; | ||
|
|
@@ -158,34 +158,50 @@ public void abort(RuntimeException exception) { | |
| } | ||
|
|
||
| /** | ||
| * Complete the request. If the batch was previously aborted, this is a no-op. | ||
| * Finalize the state of a batch. Final state, once set, is immutable. This function may be called | ||
| * once or twice on a batch. It may be called twice if | ||
| * 1. An inflight batch expires before a response from the broker is received. The batch's final | ||
| * state is set to FAILED. But it could succeed on the broker and second time around batch.done() may | ||
| * try to set SUCCEEDED final state. | ||
| * 2. If a transaction abortion happens or if the producer is closed forcefully, the final state is | ||
| * ABORTED but again it could succeed if broker responds with a success. | ||
| * | ||
| * Attempted transitions from [FAILED | ABORTED] --> SUCCEEDED are logged. | ||
| * Attempted transitions from one failure state to the same or a different failed state are ignored. | ||
| * Attempted transitions from SUCCEEDED to the same or a failed state throw an exception. | ||
| * | ||
| * @param baseOffset The base offset of the messages assigned by the server | ||
| * @param logAppendTime The log append time or -1 if CreateTime is being used | ||
| * @param exception The exception that occurred (or null if the request was successful) | ||
| * @return true if the batch was completed successfully and false if the batch was previously aborted | ||
| */ | ||
| public boolean done(long baseOffset, long logAppendTime, RuntimeException exception) { | ||
| final FinalState finalState; | ||
| if (exception == null) { | ||
| final FinalState tryFinalState = (exception == null) ? FinalState.SUCCEEDED : FinalState.FAILED; | ||
|
|
||
| if (tryFinalState == FinalState.SUCCEEDED) { | ||
| log.trace("Successfully produced messages to {} with base offset {}.", topicPartition, baseOffset); | ||
| finalState = FinalState.SUCCEEDED; | ||
| } else { | ||
| log.trace("Failed to produce messages to {}.", topicPartition, exception); | ||
| finalState = FinalState.FAILED; | ||
| } | ||
|
|
||
| if (!this.finalState.compareAndSet(null, finalState)) { | ||
| if (this.finalState.get() == FinalState.ABORTED) { | ||
| log.debug("ProduceResponse returned for {} after batch had already been aborted.", topicPartition); | ||
| return false; | ||
| } else { | ||
| throw new IllegalStateException("Batch has already been completed in final state " + this.finalState.get()); | ||
| if (this.finalState.compareAndSet(null, tryFinalState)) { | ||
| completeFutureAndFireCallbacks(baseOffset, logAppendTime, exception); | ||
| return true; | ||
| } | ||
|
|
||
| if (this.finalState.get() != FinalState.SUCCEEDED) { | ||
| if (tryFinalState == FinalState.SUCCEEDED) { | ||
| // Log if a previously unsuccessful batch succeeded later on. | ||
| log.debug("ProduceResponse returned {} for {} after batch had already been {}.", tryFinalState, | ||
| topicPartition, this.finalState.get()); | ||
| } | ||
| // FAILED --> ABORTED and ABORTED --> FAILED transitions are ignored. | ||
| } else { | ||
| // A SUCCESSFUL batch must not attempt another state change. | ||
| throw new IllegalStateException("A " + this.finalState.get() + " batch must not attempt another state change to " + tryFinalState); | ||
| } | ||
|
|
||
| completeFutureAndFireCallbacks(baseOffset, logAppendTime, exception); | ||
| return true; | ||
| return false; | ||
| } | ||
|
|
||
| private void completeFutureAndFireCallbacks(long baseOffset, long logAppendTime, RuntimeException exception) { | ||
|
|
@@ -300,29 +316,33 @@ public String toString() { | |
| } | ||
|
|
||
| /** | ||
| * A batch whose metadata is not available should be expired if one of the following is true: | ||
| * <ol> | ||
| * <li> the batch is not in retry AND request timeout has elapsed after it is ready (full or linger.ms has reached). | ||
| * <li> the batch is in retry AND request timeout has elapsed after the backoff period ended. | ||
| * </ol> | ||
| * Expire the batch if current time has passed the delivery timeout of the batch. | ||
| * The delivery timeout is measured from batch creation time instead of batch close time | ||
| * because in some cases batch close may be arbitrarily delayed. | ||
| * This methods closes this batch and sets {@code expiryErrorMessage} if the batch has timed out. | ||
| */ | ||
| boolean maybeExpire(int requestTimeoutMs, long retryBackoffMs, long now, long lingerMs, boolean isFull) { | ||
| if (!this.inRetry() && isFull && requestTimeoutMs < (now - this.lastAppendTime)) | ||
| expiryErrorMessage = (now - this.lastAppendTime) + " ms has passed since last append"; | ||
| else if (!this.inRetry() && requestTimeoutMs < (createdTimeMs(now) - lingerMs)) | ||
| expiryErrorMessage = (createdTimeMs(now) - lingerMs) + " ms has passed since batch creation plus linger time"; | ||
| else if (this.inRetry() && requestTimeoutMs < (waitedTimeMs(now) - retryBackoffMs)) | ||
| expiryErrorMessage = (waitedTimeMs(now) - retryBackoffMs) + " ms has passed since last attempt plus backoff time"; | ||
| boolean maybeExpire(long deliveryTimeoutMs, long now) { | ||
| if (deliveryTimeoutMs <= (now - this.createdMs)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are using the creation time of the batch to check for expiration. That will tend to expire some records which were added to the batch after creation earlier than the delivery timeout (by as much as linger.ms). Alternatively, we could use the time that the batch was closed, which will tend to expire records later than the delivery timeout (by as much as linger.ms), but maybe expiring late is bit safer than expiring early? This is equivalent to saying that the delivery timeout excludes linger time.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Batch close may be arbitrrily delayed in some cases. See @junrao's explanation: http://search-hadoop.com/m/Kafka/uyzND1calgR1Udv2J?subj=Re+DISCUSS+KIP+91+Provide+Intuitive+User+Timeouts+in+The+Producer
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. I synced with Jun and it seems reasonable. It would help to document somewhere why we use create time. |
||
| expire(now); | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Expire the batch. This methods closes this batch and sets {@code expiryErrorMessage}. | ||
| */ | ||
| void expire(long now) { | ||
| expiryErrorMessage = (now - this.createdMs) + " ms has passed since batch creation"; | ||
| abortRecordAppends(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking my understanding. With this change, it should no longer be possible to expire a batch before linger.ms has completed and the batch has been closed. If so, do we still need the logic to abort appends on expiration? (It might be safer to have it anyway, just checking if it is still needed for correctness)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the answer to this question is that it is possible to expire while the batch is still being built because closing the batch can be arbitrarily delayed by inflight fetches.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right |
||
| } | ||
|
|
||
| boolean expired = expiryErrorMessage != null; | ||
| if (expired) | ||
| abortRecordAppends(); | ||
| return expired; | ||
| public FinalState finalState() { | ||
| return this.finalState.get(); | ||
| } | ||
|
|
||
| /** | ||
| * If {@link #maybeExpire(int, long, long, long, boolean)} returned true, the sender will fail the batch with | ||
| * If {@link #maybeExpire(long, long)} returned true, the sender will fail the batch with | ||
| * the exception returned by this method. | ||
| * @return An exception indicating the batch expired. | ||
| */ | ||
|
|
@@ -468,4 +488,9 @@ public boolean sequenceHasBeenReset() { | |
| return reopened; | ||
| } | ||
|
|
||
| boolean soonToExpire(long deliveryTimeoutMs, long now, int requestTimeoutMs) { | ||
| // We don't add anything in deliveryTimeoutMs because it may overflow | ||
| // when deliveryTimeoutMs==Long.MAX_VALUE | ||
| return deliveryTimeoutMs <= now + requestTimeoutMs - createdMs; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
verifyAndGetDeliveryTimeout()?