-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-13348: Allow Source Tasks to Handle Producer Exceptions #11382
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 1 commit
34d93a8
b956c19
22daf36
878b131
ad098eb
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 |
|---|---|---|
|
|
@@ -39,6 +39,7 @@ | |
| import org.apache.kafka.connect.runtime.distributed.ClusterConfigState; | ||
| import org.apache.kafka.connect.runtime.errors.RetryWithToleranceOperator; | ||
| import org.apache.kafka.connect.runtime.errors.Stage; | ||
| import org.apache.kafka.connect.runtime.errors.ToleranceType; | ||
| import org.apache.kafka.connect.source.SourceRecord; | ||
| import org.apache.kafka.connect.source.SourceTask; | ||
| import org.apache.kafka.connect.storage.CloseableOffsetStorageReader; | ||
|
|
@@ -366,7 +367,11 @@ private boolean sendRecords() { | |
| if (e != null) { | ||
| log.error("{} failed to send record to {}: ", WorkerSourceTask.this, topic, e); | ||
| log.trace("{} Failed record: {}", WorkerSourceTask.this, preTransformRecord); | ||
| producerSendException.compareAndSet(null, e); | ||
| if (retryWithToleranceOperator.getErrorToleranceType().equals(ToleranceType.ALL)) { | ||
|
Member
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 can use
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. +1 |
||
| commitTaskRecord(preTransformRecord, null); | ||
|
Member
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 have a debug/trace log in this path?
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. Previously it was suggested to have the tolerance operator handle via the logging report. I would personally find it useful to have it in the connect log regardless of tolerance error logging configuration. I've moved the error/debug log lines to above the tolerance check to log in all instances.
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 should not be logging at
Member
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. Let's keep the existing
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. My misunderstanding, thank you both for the feedback. Update made. |
||
| } else { | ||
| producerSendException.compareAndSet(null, e); | ||
| } | ||
| } else { | ||
| submittedRecord.ack(); | ||
| counter.completeRecord(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -229,6 +229,13 @@ public synchronized boolean withinToleranceLimits() { | |
| } | ||
| } | ||
|
|
||
| // For source connectors that want to skip kafka producer errors. | ||
| // They cannot use withinToleranceLimits() as no failure may have actually occurred prior to the producer failing | ||
| // to write to kafka. | ||
| public synchronized ToleranceType getErrorToleranceType() { | ||
|
Member
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. Does this need to be
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. It does not. Type is immutable and thread safe. I had dug through the ticket that retroactively made this class thread safe and it seemed like a good idea at the time to slap a synchronized on it to match the rest of the class, but is not necessary at all. Removed. |
||
| return errorToleranceType; | ||
| } | ||
|
|
||
| // Visible for testing | ||
| boolean checkRetry(long startTime) { | ||
| return (time.milliseconds() - startTime) < errorRetryTimeout; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -222,6 +222,13 @@ private void createWorkerTask() { | |
| createWorkerTask(TargetState.STARTED); | ||
| } | ||
|
|
||
| private void createWorkerTaskWithErrorToleration() { | ||
|
Member
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. Can we reuse the
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. +1 I have refactored the constructors to be cleaner with various parameter lists. |
||
| workerTask = new WorkerSourceTask(taskId, sourceTask, statusListener, TargetState.STARTED, keyConverter, valueConverter, headerConverter, | ||
| transformationChain, producer, admin, TopicCreationGroup.configuredGroups(sourceConfig), | ||
| offsetReader, offsetWriter, config, clusterConfigState, metrics, plugins.delegatingLoader(), Time.SYSTEM, | ||
| RetryWithToleranceOperatorTest.ALL_OPERATOR, statusBackingStore, Runnable::run); | ||
| } | ||
|
|
||
| private void createWorkerTask(TargetState initialState) { | ||
| createWorkerTask(initialState, keyConverter, valueConverter, headerConverter); | ||
| } | ||
|
|
@@ -815,6 +822,33 @@ public void testSendRecordsTaskCommitRecordFail() throws Exception { | |
| PowerMock.verifyAll(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testSourceTaskIgnoresProducerException() throws Exception { | ||
| createWorkerTaskWithErrorToleration(); | ||
| expectTopicCreation(TOPIC); | ||
|
|
||
| // send two records | ||
| // record 1 will succeed | ||
| // record 2 will invoke the producer's failure callback, but ignore the exception via retryOperator | ||
| // and no ConnectException will be thrown | ||
| SourceRecord record1 = new SourceRecord(PARTITION, OFFSET, TOPIC, 1, KEY_SCHEMA, KEY, RECORD_SCHEMA, RECORD); | ||
| SourceRecord record2 = new SourceRecord(PARTITION, OFFSET, TOPIC, 2, KEY_SCHEMA, KEY, RECORD_SCHEMA, RECORD); | ||
|
|
||
|
|
||
| expectSendRecordOnce(); | ||
| expectSendRecordProducerCallbackFail(); | ||
| sourceTask.commitRecord(EasyMock.anyObject(SourceRecord.class), EasyMock.anyObject(RecordMetadata.class)); | ||
|
Member
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. Instead of
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. +1 |
||
| EasyMock.expectLastCall(); | ||
|
|
||
| PowerMock.replayAll(); | ||
|
|
||
| Whitebox.setInternalState(workerTask, "toSend", Arrays.asList(record1, record2)); | ||
| Whitebox.invokeMethod(workerTask, "sendRecords"); | ||
| assertEquals(false, Whitebox.getInternalState(workerTask, "lastSendFailed")); | ||
|
|
||
| PowerMock.verifyAll(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testSlowTaskStart() throws Exception { | ||
| final CountDownLatch startupLatch = new CountDownLatch(1); | ||
|
|
||
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.
Should we modify this line to respect the errors.log.enable (and possibly errors.log.include.messages) properties?
I wonder if it might be useful to still unconditionally set
producerSendException(or perhaps even convert that field from anAtomicReference<Throwable>to some kind of list, and append to it here) and then modify the contents (and possibly also name) ofmaybeThrowProducerSendExceptionto have our error-handling logic. Thoughts?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.
Actually, that may complicate things by causing records to be given to
SourceTask::commitRecordout of order (a record that caused a producer failure may be committed after a record that was dispatched to the producer after it). So probably best to keep the error-handling logic here, but I do still wonder if we can respect the logging-related configuration properties.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.
Now that this could be a tolerated error, it makes sense to have it respect the errors.log.enable configuration, but the log line would be duplicated, unconditionally writing it in the event we do not tolerate and a config check if we do.
Are you envisioning something like this?
I would need to look more closely at the other layers of objects on top of the SourceTask. enableErrorLog() is available in the ConnectorConfig, but only the SinkConnectorConfig makes use of it. I would need to spin up some additional infrastructure. Not sure if I would want to add WorkerErrantRecordReporter to WorkerSourceTask or have the configuration pass down in some other manner.
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.
Yes, I was thinking the behavior could be something like that code snippet, although we'd also want to respect the errors.log.include.messages property and would probably want the format of the error messages to be similar to the error messages we emit in other places where messages are tolerated (such as when conversion or transformation fails).
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.
The error retry handling infrastructure predominantly concerns itself with the sink side of the house. Insofar that any refactoring I would want to do would probably necessitate a KIP on its own. To that end, I have added an additional executeFailed() function to RetryWithToleranceOperator to allow the source worker to handle error logging with all of the existing infrastructure/configuration that exists for sink tasks.
I toy'ed around with the idea of having the new executeFailed() fire without a tolerance type check. This would work for failing/ignoring as expected, but with no mechanism to then decide if we should call commitRecord(). We could block on the future from executeFailed() and then check withinToleranceLimits() but that introduces non determinism with interrupt/execution exceptions.