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 @@ -626,7 +626,7 @@ private WorkerTask buildWorkerTask(ClusterConfigState configState,
// Note we pass the configState as it performs dynamic transformations under the covers
return new WorkerSourceTask(id, (SourceTask) task, statusListener, initialState, keyConverter, valueConverter,
headerConverter, transformationChain, producer, admin, topicCreationGroups,
offsetReader, offsetWriter, config, configState, metrics, loader, time, retryWithToleranceOperator, herder.statusBackingStore());
offsetReader, offsetWriter, config, configState, metrics, loader, time, retryWithToleranceOperator, herder.statusBackingStore(), executor);
} else if (task instanceof SinkTask) {
TransformationChain<SinkRecord> transformationChain = new TransformationChain<>(connConfig.<SinkRecord>transformations(), retryWithToleranceOperator);
log.info("Initializing: {}", transformationChain);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
Expand All @@ -86,6 +87,7 @@ class WorkerSourceTask extends WorkerTask {
private final TopicAdmin admin;
private final CloseableOffsetStorageReader offsetReader;
private final OffsetStorageWriter offsetWriter;
private final Executor closeExecutor;
private final SourceTaskMetricsGroup sourceTaskMetricsGroup;
private final AtomicReference<Exception> producerSendException;
private final boolean isTopicTrackingEnabled;
Expand Down Expand Up @@ -123,7 +125,8 @@ public WorkerSourceTask(ConnectorTaskId id,
ClassLoader loader,
Time time,
RetryWithToleranceOperator retryWithToleranceOperator,
StatusBackingStore statusBackingStore) {
StatusBackingStore statusBackingStore,
Executor closeExecutor) {

super(id, statusListener, initialState, loader, connectMetrics,
retryWithToleranceOperator, time, statusBackingStore);
Expand All @@ -139,6 +142,7 @@ public WorkerSourceTask(ConnectorTaskId id,
this.admin = admin;
this.offsetReader = offsetReader;
this.offsetWriter = offsetWriter;
this.closeExecutor = closeExecutor;

this.toSend = null;
this.lastSendFailed = false;
Expand Down Expand Up @@ -171,13 +175,9 @@ protected void close() {
log.warn("Could not stop task", t);
}
}
if (producer != null) {
try {
producer.close(Duration.ofSeconds(30));
} catch (Throwable t) {
log.warn("Could not close producer", t);
}
}

closeProducer(Duration.ofSeconds(30));

if (admin != null) {
Comment thread
C0urante marked this conversation as resolved.
Outdated
try {
admin.close(Duration.ofSeconds(30));
Expand All @@ -202,6 +202,14 @@ public void removeMetrics() {
public void cancel() {
super.cancel();
offsetReader.close();
// We proactively close the producer here as the main work thread for the task may
// be blocked indefinitely in a call to Producer::send if automatic topic creation is
// not enabled on either the connector or the Kafka cluster. Closing the producer should
// unblock it in that case and allow shutdown to proceed normally.
// With a duration of 0, the producer's own shutdown logic should be fairly quick,
// but closing user-pluggable classes like interceptors may lag indefinitely. So, we
// call close on a separate thread in order to avoid blocking the herder's tick thread.
closeExecutor.execute(() -> closeProducer(Duration.ZERO));
}

@Override
Expand Down Expand Up @@ -259,6 +267,16 @@ public void execute() {
}
}

private void closeProducer(Duration duration) {
if (producer != null) {
try {
producer.close(duration);
} catch (Throwable t) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The producer's close(...) method can throw an InterruptException if the method fails to join the IO thread. This can theoretically happen even if the timeout is 0 if the thread is interrupted (e.g., the executor is shutdown) before the join can wait. Although the likelihood of this is small, what do you think about catching InterruptException and ignoring the error?

Suggested change
} catch (Throwable t) {
} catch (InterruptException t) {
// ignore, since this is likely due to the worker's executor being shut down
} catch (Throwable t) {

Two things. First, the producer throws InterruptExeption, not InterruptedException. Second, even though the WorkerSourceTask::close() that calls this closeProducer(Duration) method doesn't directly use the executor, the Worker does use that same executor to stop this WorkerSourceTask, which ultimately does call WorkerSourceTask::close(). IOW, this closeProducer(Duration) method is always called from the executor, and the executor could be shutdown at any moment, thus the potential InterruptException.

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.

Right now there aren't any code paths that lead to the worker's executor being shut down. One might be added in the future, but it seems a little premature to try to catch it right now and may mislead readers of the code base. Plus, if an InterruptException does get generated somehow, it might be worth knowing about as it may indicate unhealthy (or at least unexpected) behavior from the worker, the task, an interceptor, etc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Right now there aren't any code paths that lead to the worker's executor being shut down.

Hmm, that seems to have been done a long time ago. I wonder if that was an oversight, or whether that was intentional since in Connect the Worker::stop() is called when the herder is stopped, which only happens (in Connect) when the shutdown hook is called -- at which point the JVM is terminating anyway. Luckily MM2 works the same way.

But in our test cases that use EmbeddedConnectCluster, those tests are not cleaning up all resources of the Worker (and thus Herder) -- we might have threads that still keep running. Seems like we should address that in a different issue. I'll log something.

@rhauch rhauch Feb 26, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I've logged https://issues.apache.org/jira/browse/KAFKA-12380 for shutting down the worker's executor. Again, it's not an issue in runtime, but a potential issue in our tests.

log.warn("Could not close producer for {}", id, t);
}
}
}

private void maybeThrowProducerSendException() {
if (producerSendException.get() != null) {
throw new ConnectException(
Expand Down Expand Up @@ -488,7 +506,9 @@ public boolean commitOffsets() {
while (!outstandingMessages.isEmpty()) {
try {
long timeoutMs = timeout - time.milliseconds();
if (timeoutMs <= 0) {
// If the task has been cancelled, no more records will be sent from the producer; in that case, if any outstanding messages remain,
// we can stop flushing immediately
if (isCancelled() || timeoutMs <= 0) {
log.error("{} Failed to flush, timed out while waiting for producer to flush outstanding {} messages", this, outstandingMessages.size());
finishFailedFlush();
recordCommitFailure(time.milliseconds() - started, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ protected boolean isStopping() {
return stopping;
}

protected boolean isCancelled() {
return cancelled;
}

private void doClose() {
try {
close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,24 +65,27 @@ public class ConnectWorkerIntegrationTest {
private static final long OFFSET_COMMIT_INTERVAL_MS = TimeUnit.SECONDS.toMillis(30);
private static final int NUM_WORKERS = 3;
private static final int NUM_TASKS = 4;
private static final int MESSAGES_PER_POLL = 10;
private static final String CONNECTOR_NAME = "simple-source";
private static final String TOPIC_NAME = "test-topic";

private EmbeddedConnectCluster.Builder connectBuilder;
private EmbeddedConnectCluster connect;
Map<String, String> workerProps = new HashMap<>();
Properties brokerProps = new Properties();
private Map<String, String> workerProps;
private Properties brokerProps;

@Rule
public TestRule watcher = ConnectIntegrationTestUtils.newTestWatcher(log);

@Before
public void setup() {
// setup Connect worker properties
workerProps = new HashMap<>();
workerProps.put(OFFSET_COMMIT_INTERVAL_MS_CONFIG, String.valueOf(OFFSET_COMMIT_INTERVAL_MS));
workerProps.put(CONNECTOR_CLIENT_POLICY_CLASS_CONFIG, "All");

// setup Kafka broker properties
brokerProps = new Properties();
brokerProps.put("auto.create.topics.enable", String.valueOf(false));

// build a Connect cluster backed by Kafka and Zk
Expand Down Expand Up @@ -288,14 +291,48 @@ public void testTaskStatuses() throws Exception {
decreasedNumTasks, "Connector task statuses did not update in time.");
}

@Test
public void testSourceTaskNotBlockedOnShutdownWithNonExistentTopic() throws Exception {
// When automatic topic creation is disabled on the broker
brokerProps.put("auto.create.topics.enable", "false");
connect = connectBuilder
.brokerProps(brokerProps)
.numWorkers(1)
.numBrokers(1)
.build();
connect.start();

connect.assertions().assertAtLeastNumWorkersAreUp(1, "Initial group of workers did not start in time.");

// and when the connector is not configured to create topics
Map<String, String> props = defaultSourceConnectorProps("nonexistenttopic");
Comment thread
C0urante marked this conversation as resolved.
Outdated
props.remove(DEFAULT_TOPIC_CREATION_PREFIX + REPLICATION_FACTOR_CONFIG);
props.remove(DEFAULT_TOPIC_CREATION_PREFIX + PARTITIONS_CONFIG);
props.put("throughput", "-1");

ConnectorHandle connector = RuntimeHandles.get().connectorHandle(CONNECTOR_NAME);
connector.expectedRecords(NUM_TASKS * MESSAGES_PER_POLL);
connect.configureConnector(CONNECTOR_NAME, props);
connect.assertions().assertConnectorAndExactlyNumTasksAreRunning(CONNECTOR_NAME,
NUM_TASKS, "Connector tasks did not start in time");
connector.awaitRecords(TimeUnit.MINUTES.toMillis(1));

// Then if we delete the connector, it and each of its tasks should be stopped by the framework
// even though the producer is blocked because there is no topic
StartAndStopLatch stopCounter = connector.expectedStops(1);
Comment thread
C0urante marked this conversation as resolved.
Outdated
connect.deleteConnector(CONNECTOR_NAME);

assertTrue("Connector and all tasks were not stopped in time", stopCounter.await(1, TimeUnit.MINUTES));
}

private Map<String, String> defaultSourceConnectorProps(String topic) {
// setup up props for the source connector
Map<String, String> props = new HashMap<>();
props.put(CONNECTOR_CLASS_CONFIG, MonitorableSourceConnector.class.getSimpleName());
props.put(TASKS_MAX_CONFIG, String.valueOf(NUM_TASKS));
props.put(TOPIC_CONFIG, topic);
props.put("throughput", String.valueOf(10));
props.put("messages.per.poll", String.valueOf(10));
props.put("throughput", "10");
props.put("messages.per.poll", String.valueOf(MESSAGES_PER_POLL));
props.put(KEY_CONVERTER_CLASS_CONFIG, StringConverter.class.getName());
props.put(VALUE_CONVERTER_CLASS_CONFIG, StringConverter.class.getName());
props.put(DEFAULT_TOPIC_CREATION_PREFIX + REPLICATION_FACTOR_CONFIG, String.valueOf(1));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ public StartAndStopLatch expectedStarts(int expectedStarts, boolean includeTasks
* {@link StartAndStopLatch#await(long, TimeUnit)} to wait up to a specified duration for the
* connector and all tasks to be started at least the specified number of times.
*
* <p>This method does not track the number of times the connector and tasks are stopped, and
* only tracks the number of times the connector and tasks are <em>started</em>.
* <p>This method does not track the number of times the connector and tasks are started, and
* only tracks the number of times the connector and tasks are <em>stopped</em>.
*
* @param expectedStops the minimum number of starts that are expected once this method is
* called
Expand All @@ -315,8 +315,8 @@ public StartAndStopLatch expectedStops(int expectedStops) {
* {@link StartAndStopLatch#await(long, TimeUnit)} to wait up to a specified duration for the
* connector and all tasks to be started at least the specified number of times.
*
* <p>This method does not track the number of times the connector and tasks are stopped, and
* only tracks the number of times the connector and tasks are <em>started</em>.
* <p>This method does not track the number of times the connector and tasks are started, and
* only tracks the number of times the connector and tasks are <em>stopped</em>.
*
* @param expectedStops the minimum number of starts that are expected once this method is
* called
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public List<SourceRecord> poll() {
throttler.throttle();
}
taskHandle.record(batchSize);
log.info("Returning batch of {} records", batchSize);
return LongStream.range(0, batchSize)
.mapToObj(i -> new SourceRecord(
Collections.singletonMap("task.id", taskId),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;

import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
Expand Down Expand Up @@ -563,7 +564,7 @@ private void createSourceTask(TargetState initialState, RetryWithToleranceOperat
producer, admin, null,
offsetReader, offsetWriter, workerConfig,
ClusterConfigState.EMPTY, metrics, pluginLoader, time, retryWithToleranceOperator,
statusBackingStore);
statusBackingStore, (Executor) Runnable::run);
}

private ConsumerRecords<byte[], byte[]> records(ConsumerRecord<byte[], byte[]> record) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;

import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
Expand Down Expand Up @@ -581,7 +582,7 @@ private void createSourceTask(TargetState initialState, RetryWithToleranceOperat
producer, admin, TopicCreationGroup.configuredGroups(sourceConfig),
offsetReader, offsetWriter, workerConfig,
ClusterConfigState.EMPTY, metrics, pluginLoader, time, retryWithToleranceOperator,
statusBackingStore);
statusBackingStore, (Executor) Runnable::run);
Comment thread
C0urante marked this conversation as resolved.
Outdated
}

private ConsumerRecords<byte[], byte[]> records(ConsumerRecord<byte[], byte[]> record) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ private void createWorkerTask(TargetState initialState, Converter keyConverter,
workerTask = new WorkerSourceTask(taskId, sourceTask, statusListener, initialState, keyConverter, valueConverter, headerConverter,
transformationChain, producer, admin, null,
offsetReader, offsetWriter, config, clusterConfigState, metrics, plugins.delegatingLoader(), Time.SYSTEM,
RetryWithToleranceOperatorTest.NOOP_OPERATOR, statusBackingStore);
RetryWithToleranceOperatorTest.NOOP_OPERATOR, statusBackingStore, Runnable::run);
}

@Test
Expand Down Expand Up @@ -710,6 +710,9 @@ public void testCancel() {
offsetReader.close();
PowerMock.expectLastCall();

producer.close(Duration.ZERO);
PowerMock.expectLastCall();

PowerMock.replayAll();

workerTask.cancel();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private void createWorkerTask(TargetState initialState, Converter keyConverter,
workerTask = new WorkerSourceTask(taskId, sourceTask, statusListener, initialState, keyConverter, valueConverter, headerConverter,
transformationChain, producer, admin, TopicCreationGroup.configuredGroups(sourceConfig),
offsetReader, offsetWriter, config, clusterConfigState, metrics, plugins.delegatingLoader(), Time.SYSTEM,
RetryWithToleranceOperatorTest.NOOP_OPERATOR, statusBackingStore);
RetryWithToleranceOperatorTest.NOOP_OPERATOR, statusBackingStore, Runnable::run);
}

@Test
Expand Down Expand Up @@ -754,6 +754,9 @@ public void testCancel() {
offsetReader.close();
PowerMock.expectLastCall();

producer.close(Duration.ZERO);
PowerMock.expectLastCall();

PowerMock.replayAll();

workerTask.cancel();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -1481,7 +1482,8 @@ private void expectNewWorkerTask() throws Exception {
EasyMock.eq(pluginLoader),
anyObject(Time.class),
anyObject(RetryWithToleranceOperator.class),
anyObject(StatusBackingStore.class))
anyObject(StatusBackingStore.class),
anyObject(Executor.class))
.andReturn(workerTask);
}
/* Name here needs to be unique as we are testing the aliasing mechanism */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -1389,7 +1390,8 @@ private void expectNewWorkerTask() throws Exception {
EasyMock.eq(pluginLoader),
anyObject(Time.class),
anyObject(RetryWithToleranceOperator.class),
anyObject(StatusBackingStore.class))
anyObject(StatusBackingStore.class),
anyObject(Executor.class))
.andReturn(workerTask);
}

Expand Down