-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-13469: Block for in-flight record delivery before end-of-life source task offset commit #11524
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
KAFKA-13469: Block for in-flight record delivery before end-of-life source task offset commit #11524
Changes from all commits
c7454b6
96ce342
e1a711a
4197592
1f17249
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 |
|---|---|---|
|
|
@@ -26,6 +26,9 @@ | |
| import java.util.HashMap; | ||
| import java.util.LinkedList; | ||
| import java.util.Map; | ||
| import java.util.concurrent.CountDownLatch; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
|
|
||
| /** | ||
| * Used to track source records that have been (or are about to be) dispatched to a producer and their accompanying | ||
|
|
@@ -41,10 +44,11 @@ class SubmittedRecords { | |
| private static final Logger log = LoggerFactory.getLogger(SubmittedRecords.class); | ||
|
|
||
| // Visible for testing | ||
| final Map<Map<String, Object>, Deque<SubmittedRecord>> records; | ||
| final Map<Map<String, Object>, Deque<SubmittedRecord>> records = new HashMap<>(); | ||
| private int numUnackedMessages = 0; | ||
| private CountDownLatch messageDrainLatch; | ||
|
|
||
| public SubmittedRecords() { | ||
| this.records = new HashMap<>(); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -68,6 +72,9 @@ SubmittedRecord submit(Map<String, Object> partition, Map<String, Object> offset | |
| SubmittedRecord result = new SubmittedRecord(partition, offset); | ||
| records.computeIfAbsent(result.partition(), p -> new LinkedList<>()) | ||
| .add(result); | ||
| synchronized (this) { | ||
| numUnackedMessages++; | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
|
|
@@ -89,7 +96,9 @@ public boolean removeLastOccurrence(SubmittedRecord record) { | |
| if (deque.isEmpty()) { | ||
| records.remove(record.partition()); | ||
| } | ||
| if (!result) { | ||
| if (result) { | ||
| messageAcked(); | ||
| } else { | ||
| log.warn("Attempted to remove record from submitted queue for partition {}, but the record has not been submitted or has already been removed", record.partition()); | ||
| } | ||
| return result; | ||
|
|
@@ -132,6 +141,28 @@ public CommittableOffsets committableOffsets() { | |
| return new CommittableOffsets(offsets, totalCommittableMessages, totalUncommittableMessages, records.size(), largestDequeSize, largestDequePartition); | ||
| } | ||
|
|
||
| /** | ||
| * Wait for all currently in-flight messages to be acknowledged, up to the requested timeout. | ||
| * This method is expected to be called from the same thread that calls {@link #committableOffsets()}. | ||
| * @param timeout the maximum time to wait | ||
| * @param timeUnit the time unit of the timeout argument | ||
| * @return whether all in-flight messages were acknowledged before the timeout elapsed | ||
| */ | ||
| public boolean awaitAllMessages(long timeout, TimeUnit timeUnit) { | ||
|
rhauch marked this conversation as resolved.
|
||
| // Create a new message drain latch as a local variable to avoid SpotBugs warnings about inconsistent synchronization | ||
| // on an instance variable when invoking CountDownLatch::await outside a synchronized block | ||
| CountDownLatch messageDrainLatch; | ||
| synchronized (this) { | ||
| messageDrainLatch = new CountDownLatch(numUnackedMessages); | ||
| this.messageDrainLatch = messageDrainLatch; | ||
| } | ||
| try { | ||
| return messageDrainLatch.await(timeout, timeUnit); | ||
| } catch (InterruptedException e) { | ||
| return false; | ||
|
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 this clear the interrupted flag before returning, since we're handling the interruption here?
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. I believe this is already accomplished. According to the Javadocs for
|
||
| } | ||
| } | ||
|
|
||
| // Note that this will return null if either there are no committable offsets for the given deque, or the latest | ||
| // committable offset is itself null. The caller is responsible for distinguishing between the two cases. | ||
| private Map<String, Object> committableOffset(Deque<SubmittedRecord> queuedRecords) { | ||
|
|
@@ -146,27 +177,39 @@ private boolean canCommitHead(Deque<SubmittedRecord> queuedRecords) { | |
| return queuedRecords.peek() != null && queuedRecords.peek().acked(); | ||
| } | ||
|
|
||
| static class SubmittedRecord { | ||
| // Synchronize in order to ensure that the number of unacknowledged messages isn't modified in the middle of a call | ||
| // to awaitAllMessages (which might cause us to decrement first, then create a new message drain latch, then count down | ||
| // that latch here, effectively double-acking the message) | ||
| private synchronized void messageAcked() { | ||
| numUnackedMessages--; | ||
| if (messageDrainLatch != null) { | ||
| messageDrainLatch.countDown(); | ||
| } | ||
| } | ||
|
|
||
| class SubmittedRecord { | ||
| private final Map<String, Object> partition; | ||
| private final Map<String, Object> offset; | ||
| private volatile boolean acked; | ||
| private final AtomicBoolean acked; | ||
|
|
||
| public SubmittedRecord(Map<String, Object> partition, Map<String, Object> offset) { | ||
| this.partition = partition; | ||
| this.offset = offset; | ||
| this.acked = false; | ||
| this.acked = new AtomicBoolean(false); | ||
| } | ||
|
|
||
| /** | ||
| * Acknowledge this record; signals that its offset may be safely committed. | ||
| * This is safe to be called from a different thread than what called {@link SubmittedRecords#submit(SourceRecord)}. | ||
| */ | ||
| public void ack() { | ||
| this.acked = true; | ||
| if (this.acked.compareAndSet(false, true)) { | ||
| messageAcked(); | ||
| } | ||
|
Comment on lines
+206
to
+208
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. Nice simplification. |
||
| } | ||
|
|
||
| private boolean acked() { | ||
| return acked; | ||
| return acked.get(); | ||
| } | ||
|
|
||
| private Map<String, Object> partition() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ | |
| import org.apache.kafka.connect.header.Header; | ||
| import org.apache.kafka.connect.header.Headers; | ||
| import org.apache.kafka.connect.runtime.ConnectMetrics.MetricGroup; | ||
| import org.apache.kafka.connect.runtime.SubmittedRecords.SubmittedRecord; | ||
| import org.apache.kafka.connect.runtime.distributed.ClusterConfigState; | ||
| import org.apache.kafka.connect.runtime.errors.RetryWithToleranceOperator; | ||
| import org.apache.kafka.connect.runtime.errors.Stage; | ||
|
|
@@ -65,7 +66,6 @@ | |
| import java.util.concurrent.TimeoutException; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
|
|
||
| import static org.apache.kafka.connect.runtime.SubmittedRecords.SubmittedRecord; | ||
| import static org.apache.kafka.connect.runtime.SubmittedRecords.CommittableOffsets; | ||
| import static org.apache.kafka.connect.runtime.WorkerConfig.TOPIC_TRACKING_ENABLE_CONFIG; | ||
|
|
||
|
|
@@ -260,6 +260,10 @@ public void execute() { | |
| } catch (InterruptedException e) { | ||
| // Ignore and allow to exit. | ||
| } finally { | ||
| submittedRecords.awaitAllMessages( | ||
| workerConfig.getLong(WorkerConfig.OFFSET_COMMIT_TIMEOUT_MS_CONFIG), | ||
|
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. Waiting for up to I initially considered an approach where we would require the entire combination of (awaiting in-flight messages, computing committable offsets, committing offsets to backing store) to complete in We could also consider blocking for at most half of
But I don't know if that's really necessary since in this case, these offsets have no chance of being committed in a future attempt (at least, not by the same task instance), and the offset commit is taking place on the task's work thread instead of the source task offset commit thread, so there's no worry about squatting on that thread and blocking other tasks from being able to commit offsets while it's in use.
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. Yeah, I think I agree. The previous behavior called |
||
| TimeUnit.MILLISECONDS | ||
| ); | ||
| // It should still be safe to commit offsets since any exception would have | ||
| // simply resulted in not getting more records but all the existing records should be ok to flush | ||
| // and commit offsets. Worst case, task.flush() will also throw an exception causing the offset commit | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.