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 @@ -545,8 +545,8 @@ static Map<String, Object> producerConfigs(ConnectorTaskId id,
producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, Utils.join(config.getList(WorkerConfig.BOOTSTRAP_SERVERS_CONFIG), ","));
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer");
producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer");
// These settings are designed to ensure there is no data loss. They *may* be overridden via configs passed to the
// worker, but this may compromise the delivery guarantees of Kafka Connect.
// These settings will execute infinite retries on retriable exceptions. They *may* be overridden via configs passed to the worker,
// but this may compromise the delivery guarantees of Kafka Connect.
producerProps.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, Integer.toString(Integer.MAX_VALUE));
producerProps.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, Long.toString(Long.MAX_VALUE));
producerProps.put(ProducerConfig.ACKS_CONFIG, "all");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

/**
* WorkerTask that uses a SourceTask to ingest data into Kafka.
Expand All @@ -78,6 +79,7 @@ class WorkerSourceTask extends WorkerTask {
private final OffsetStorageWriter offsetWriter;
private final Time time;
private final SourceTaskMetricsGroup sourceTaskMetricsGroup;
private final AtomicReference<Exception> producerSendException;

private List<SourceRecord> toSend;
private boolean lastSendFailed; // Whether the last send failed *synchronously*, i.e. never made it into the producer's RecordAccumulator
Expand Down Expand Up @@ -133,6 +135,7 @@ public WorkerSourceTask(ConnectorTaskId id,
this.flushing = false;
this.stopRequestedLatch = new CountDownLatch(1);
this.sourceTaskMetricsGroup = new SourceTaskMetricsGroup(id, connectMetrics);
this.producerSendException = new AtomicReference<>();
}

@Override
Expand Down Expand Up @@ -215,6 +218,8 @@ public void execute() {
continue;
}

maybeThrowProducerSendException();

if (toSend == null) {
log.trace("{} Nothing to send to Kafka. Polling source for additional records", this);
long start = time.milliseconds();
Expand All @@ -240,6 +245,15 @@ public void execute() {
}
}

private void maybeThrowProducerSendException() {
if (producerSendException.get() != null) {
throw new ConnectException(
"Unrecoverable exception from producer send callback",
producerSendException.get()
);
}
}

protected List<SourceRecord> poll() throws InterruptedException {
try {
return task.poll();
Expand Down Expand Up @@ -288,6 +302,7 @@ private boolean sendRecords() {
recordBatch(toSend.size());
final SourceRecordWriteCounter counter = new SourceRecordWriteCounter(toSend.size(), sourceTaskMetricsGroup);
for (final SourceRecord preTransformRecord : toSend) {
maybeThrowProducerSendException();
Comment thread
rhauch marked this conversation as resolved.
Outdated

retryWithToleranceOperator.sourceRecord(preTransformRecord);
final SourceRecord record = transformationChain.apply(preTransformRecord);
Expand Down Expand Up @@ -322,22 +337,18 @@ private boolean sendRecords() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null) {
// Given the default settings for zero data loss, this should basically never happen --
// between "infinite" retries, indefinite blocking on full buffers, and "infinite" request
// timeouts, callbacks with exceptions should never be invoked in practice. If the
// user overrode these settings, the best we can do is notify them of the failure via
// logging.
Comment thread
C0urante marked this conversation as resolved.
Outdated
log.error("{} failed to send record to {}:", WorkerSourceTask.this, topic, e);
log.debug("{} Failed record: {}", WorkerSourceTask.this, preTransformRecord);
producerSendException.compareAndSet(null, e);
Comment thread
C0urante marked this conversation as resolved.
Outdated
} else {
recordSent(producerRecord);
counter.completeRecord();
log.trace("{} Wrote record successfully: topic {} partition {} offset {}",
WorkerSourceTask.this,
recordMetadata.topic(), recordMetadata.partition(),
recordMetadata.offset());
commitTaskRecord(preTransformRecord);
}
recordSent(producerRecord);
counter.completeRecord();
}
});
lastSendFailed = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.MetricName;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.TopicAuthorizationException;
import org.apache.kafka.common.record.InvalidRecordException;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.connect.data.Schema;
import org.apache.kafka.connect.errors.ConnectException;
import org.apache.kafka.connect.runtime.ConnectMetrics.MetricGroup;
import org.apache.kafka.connect.runtime.WorkerSourceTask.SourceTaskMetricsGroup;
import org.apache.kafka.connect.runtime.distributed.ClusterConfigState;
Expand Down Expand Up @@ -542,6 +544,21 @@ public void testSendRecordsRetries() throws Exception {
PowerMock.verifyAll();
}

@Test(expected = ConnectException.class)
public void testSendRecordsProducerCallbackFail() throws Exception {
createWorkerTask();

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);

expectSendRecordProducerCallbackFail();

PowerMock.replayAll();

Whitebox.setInternalState(workerTask, "toSend", Arrays.asList(record1, record2));
Whitebox.invokeMethod(workerTask, "sendRecords");
}

@Test
public void testSendRecordsTaskCommitRecordFail() throws Exception {
createWorkerTask();
Expand Down Expand Up @@ -711,16 +728,24 @@ private Capture<ProducerRecord<byte[], byte[]>> expectSendRecordOnce(boolean isR
return expectSendRecordTaskCommitRecordSucceed(false, isRetry);
}

private Capture<ProducerRecord<byte[], byte[]>> expectSendRecordProducerCallbackFail() throws InterruptedException {
return expectSendRecord(false, false, false, false);
}

private Capture<ProducerRecord<byte[], byte[]>> expectSendRecordTaskCommitRecordSucceed(boolean anyTimes, boolean isRetry) throws InterruptedException {
return expectSendRecord(anyTimes, isRetry, true);
return expectSendRecord(anyTimes, isRetry, true, true);
}

private Capture<ProducerRecord<byte[], byte[]>> expectSendRecordTaskCommitRecordFail(boolean anyTimes, boolean isRetry) throws InterruptedException {
return expectSendRecord(anyTimes, isRetry, false);
return expectSendRecord(anyTimes, isRetry, true, false);
}

@SuppressWarnings("unchecked")
private Capture<ProducerRecord<byte[], byte[]>> expectSendRecord(boolean anyTimes, boolean isRetry, boolean succeed) throws InterruptedException {
private Capture<ProducerRecord<byte[], byte[]>> expectSendRecord(
boolean anyTimes,
boolean isRetry,
boolean sendSuccess,
boolean commitSuccess
) throws InterruptedException {
expectConvertKeyValue(anyTimes);
expectApplyTransformationChain(anyTimes);

Expand All @@ -737,15 +762,19 @@ private Capture<ProducerRecord<byte[], byte[]>> expectSendRecord(boolean anyTime

// 2. Converted data passed to the producer, which will need callbacks invoked for flush to work
IExpectationSetters<Future<RecordMetadata>> expect = EasyMock.expect(
producer.send(EasyMock.capture(sent),
EasyMock.capture(producerCallbacks)));
producer.send(EasyMock.capture(sent),
EasyMock.capture(producerCallbacks)));
IAnswer<Future<RecordMetadata>> expectResponse = new IAnswer<Future<RecordMetadata>>() {
@Override
public Future<RecordMetadata> answer() throws Throwable {
synchronized (producerCallbacks) {
for (org.apache.kafka.clients.producer.Callback cb : producerCallbacks.getValues()) {
cb.onCompletion(new RecordMetadata(new TopicPartition("foo", 0), 0, 0,
0L, 0L, 0, 0), null);
if (sendSuccess) {
cb.onCompletion(new RecordMetadata(new TopicPartition("foo", 0), 0, 0,
0L, 0L, 0, 0), null);
} else {
cb.onCompletion(null, new TopicAuthorizationException("foo"));
}
}
producerCallbacks.reset();
}
Expand All @@ -757,8 +786,10 @@ public Future<RecordMetadata> answer() throws Throwable {
else
expect.andAnswer(expectResponse);

// 3. As a result of a successful producer send callback, we'll notify the source task of the record commit
expectTaskCommitRecord(anyTimes, succeed);
if (sendSuccess) {
// 3. As a result of a successful producer send callback, we'll notify the source task of the record commit
expectTaskCommitRecord(anyTimes, commitSuccess);
}

return sent;
}
Expand Down