-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-6897: Prevent producer from blocking indefinitely after close #5027
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 |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| package org.apache.kafka.clients; | ||
|
|
||
| import org.apache.kafka.common.Cluster; | ||
| import org.apache.kafka.common.KafkaException; | ||
| import org.apache.kafka.common.internals.ClusterResourceListeners; | ||
| import org.apache.kafka.common.Node; | ||
| import org.apache.kafka.common.PartitionInfo; | ||
|
|
@@ -25,6 +26,7 @@ | |
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import java.io.Closeable; | ||
| import java.util.ArrayList; | ||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
|
|
@@ -48,7 +50,7 @@ | |
| * is removed from the metadata refresh set after an update. Consumers disable topic expiry since they explicitly | ||
| * manage topics while producers rely on topic expiry to limit the refresh set. | ||
| */ | ||
| public final class Metadata { | ||
| public final class Metadata implements Closeable { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(Metadata.class); | ||
|
|
||
|
|
@@ -70,6 +72,7 @@ public final class Metadata { | |
| private boolean needMetadataForAllTopics; | ||
| private final boolean allowAutoTopicCreation; | ||
| private final boolean topicExpiryEnabled; | ||
| private boolean isClosed; | ||
|
|
||
| public Metadata(long refreshBackoffMs, long metadataExpireMs, boolean allowAutoTopicCreation) { | ||
| this(refreshBackoffMs, metadataExpireMs, allowAutoTopicCreation, false, new ClusterResourceListeners()); | ||
|
|
@@ -100,6 +103,7 @@ public Metadata(long refreshBackoffMs, long metadataExpireMs, boolean allowAutoT | |
| this.listeners = new ArrayList<>(); | ||
| this.clusterResourceListeners = clusterResourceListeners; | ||
| this.needMetadataForAllTopics = false; | ||
| this.isClosed = false; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -164,12 +168,12 @@ public synchronized AuthenticationException getAndClearAuthenticationException() | |
| * Wait for metadata update until the current version is larger than the last version we know of | ||
| */ | ||
| public synchronized void awaitUpdate(final int lastVersion, final long maxWaitMs) throws InterruptedException { | ||
| if (maxWaitMs < 0) { | ||
| if (maxWaitMs < 0) | ||
| throw new IllegalArgumentException("Max time to wait for metadata updates should not be < 0 milliseconds"); | ||
| } | ||
|
|
||
| long begin = System.currentTimeMillis(); | ||
| long remainingWaitMs = maxWaitMs; | ||
| while (this.version <= lastVersion) { | ||
| while ((this.version <= lastVersion) && !isClosed()) { | ||
| AuthenticationException ex = getAndClearAuthenticationException(); | ||
| if (ex != null) | ||
| throw ex; | ||
|
|
@@ -180,6 +184,8 @@ public synchronized void awaitUpdate(final int lastVersion, final long maxWaitMs | |
| throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms."); | ||
| remainingWaitMs = maxWaitMs - elapsed; | ||
| } | ||
| if (isClosed()) | ||
| throw new KafkaException("Requested metadata update after close"); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -224,6 +230,8 @@ public synchronized boolean containsTopic(String topic) { | |
| */ | ||
| public synchronized void update(Cluster newCluster, Set<String> unavailableTopics, long now) { | ||
| Objects.requireNonNull(newCluster, "cluster should not be null"); | ||
| if (isClosed()) | ||
| throw new IllegalStateException("Update requested after metadata close"); | ||
|
|
||
| this.needUpdate = false; | ||
| this.lastRefreshMs = now; | ||
|
|
@@ -331,6 +339,25 @@ public synchronized void removeListener(Listener listener) { | |
| this.listeners.remove(listener); | ||
| } | ||
|
|
||
| /** | ||
| * "Close" this metadata instance to indicate that metadata updates are no longer possible. This is typically used | ||
| * when the thread responsible for performing metadata updates is exiting and needs a way to relay this information | ||
| * to any other thread(s) that could potentially wait on metadata update to come through. | ||
| */ | ||
| @Override | ||
| public synchronized void close() { | ||
| this.isClosed = true; | ||
| this.notifyAll(); | ||
| } | ||
|
|
||
| /** | ||
| * Check if this metadata instance has been closed. See {@link #close()} for more information. | ||
| * @return True if this instance has been closed; false otherwise | ||
| */ | ||
| public synchronized boolean isClosed() { | ||
|
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. Does this need to be public?
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 have |
||
| return this.isClosed; | ||
| } | ||
|
|
||
| /** | ||
| * MetadataUpdate Listener | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
| import java.util.concurrent.atomic.AtomicReference; | ||
|
|
||
| import org.apache.kafka.common.Cluster; | ||
| import org.apache.kafka.common.KafkaException; | ||
| import org.apache.kafka.common.internals.ClusterResourceListeners; | ||
| import org.apache.kafka.common.Node; | ||
| import org.apache.kafka.common.PartitionInfo; | ||
|
|
@@ -46,7 +47,7 @@ public class MetadataTest { | |
| private long refreshBackoffMs = 100; | ||
| private long metadataExpireMs = 1000; | ||
| private Metadata metadata = new Metadata(refreshBackoffMs, metadataExpireMs, true); | ||
| private AtomicReference<String> backgroundError = new AtomicReference<>(); | ||
| private AtomicReference<Exception> backgroundError = new AtomicReference<>(); | ||
|
|
||
| @After | ||
| public void tearDown() { | ||
|
|
@@ -83,6 +84,30 @@ public void testMetadata() throws Exception { | |
| assertTrue("Update needed due to stale metadata.", metadata.timeToNextUpdate(time) == 0); | ||
| } | ||
|
|
||
| @Test | ||
| public void testMetadataAwaitAfterClose() throws InterruptedException { | ||
| long time = 0; | ||
| metadata.update(Cluster.empty(), Collections.<String>emptySet(), time); | ||
| assertFalse("No update needed.", metadata.timeToNextUpdate(time) == 0); | ||
| metadata.requestUpdate(); | ||
| assertFalse("Still no updated needed due to backoff", metadata.timeToNextUpdate(time) == 0); | ||
| time += refreshBackoffMs; | ||
| assertTrue("Update needed now that backoff time expired", metadata.timeToNextUpdate(time) == 0); | ||
| String topic = "my-topic"; | ||
| metadata.close(); | ||
| Thread t1 = asyncFetch(topic, 500); | ||
| t1.join(); | ||
| assertTrue(backgroundError.get().getClass() == KafkaException.class); | ||
| assertTrue(backgroundError.get().toString().contains("Requested metadata update after close")); | ||
| clearBackgroundError(); | ||
| } | ||
|
|
||
| @Test(expected = IllegalStateException.class) | ||
|
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. Don't we need to update this?
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. Related to the other comment - |
||
| public void testMetadataUpdateAfterClose() { | ||
| metadata.close(); | ||
| metadata.update(Cluster.empty(), Collections.<String>emptySet(), 1000); | ||
| } | ||
|
|
||
| private static void checkTimeToNextUpdate(long refreshBackoffMs, long metadataExpireMs) { | ||
| long now = 10000; | ||
|
|
||
|
|
@@ -409,15 +434,18 @@ public void testNonExpiringMetadata() throws Exception { | |
| assertTrue("Unused topic expired when expiry disabled", metadata.containsTopic("topic4")); | ||
| } | ||
|
|
||
| private void clearBackgroundError() { | ||
| backgroundError.set(null); | ||
| } | ||
|
|
||
| private Thread asyncFetch(final String topic, final long maxWaitMs) { | ||
| Thread thread = new Thread() { | ||
| public void run() { | ||
| while (metadata.fetch().partitionsForTopic(topic).isEmpty()) { | ||
| try { | ||
| try { | ||
|
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. Hmm.. The old logic would let us continue fetching in the case of a timeout. Do you think that was not intentional?
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. Even if we continued fetching, we would have failed the test at the end. |
||
| while (metadata.fetch().partitionsForTopic(topic).isEmpty()) | ||
| metadata.awaitUpdate(metadata.requestUpdate(), maxWaitMs); | ||
| } catch (Exception e) { | ||
| backgroundError.set(e.toString()); | ||
| } | ||
| } catch (Exception e) { | ||
| backgroundError.set(e); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
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 use
KafkaExceptionhere as well?Uh oh!
There was an error while loading. Please reload this page.
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.
Hmm, I feel
IllegalStateExceptionis more appropriate in this case. We expectNetworkClientto not invokeMetadata#updateafter it has calledMetadata#close().