-
Notifications
You must be signed in to change notification settings - Fork 15.4k
HOTFIX: skip listOffsets request for newly created changelog topics #8662
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
dff6e11
e0f8577
652ea3a
e8124ae
fbc13b4
df24f6e
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 |
|---|---|---|
|
|
@@ -492,7 +492,7 @@ private Map<TopicPartition, PartitionInfo> prepareRepartitionTopics(final Map<In | |
|
|
||
| // make sure the repartition source topics exist with the right number of partitions, | ||
| // create these topics if necessary | ||
| prepareTopic(repartitionTopicMetadata); | ||
| internalTopicManager.makeReady(repartitionTopicMetadata); | ||
|
|
||
| // augment the metadata with the newly computed number of partitions for all the | ||
| // repartition source topics | ||
|
|
@@ -640,14 +640,12 @@ private void checkAllPartitions(final Set<String> allSourceTopics, | |
| } | ||
|
|
||
| /** | ||
| * Resolve changelog topic metadata and create them if necessary. | ||
| * | ||
| * @return mapping of stateful tasks to their set of changelog topics | ||
| * Resolve changelog topic metadata and create them if necessary. Fills in the changelogsByStatefulTask map | ||
| * and returns the set of changelogs which were newly created. | ||
| */ | ||
| private Map<TaskId, Set<TopicPartition>> prepareChangelogTopics(final Map<Integer, TopicsInfo> topicGroups, | ||
| final Map<Integer, Set<TaskId>> tasksForTopicGroup) { | ||
| final Map<TaskId, Set<TopicPartition>> changelogsByStatefulTask = new HashMap<>(); | ||
|
|
||
| private Set<String> prepareChangelogTopics(final Map<Integer, TopicsInfo> topicGroups, | ||
| final Map<Integer, Set<TaskId>> tasksForTopicGroup, | ||
| final Map<TaskId, Set<TopicPartition>> changelogsByStatefulTask) { | ||
| // add tasks to state change log topic subscribers | ||
| final Map<String, InternalTopicConfig> changelogTopicMetadata = new HashMap<>(); | ||
| for (final Map.Entry<Integer, TopicsInfo> entry : topicGroups.entrySet()) { | ||
|
|
@@ -685,9 +683,9 @@ private Map<TaskId, Set<TopicPartition>> prepareChangelogTopics(final Map<Intege | |
| } | ||
| } | ||
|
|
||
| prepareTopic(changelogTopicMetadata); | ||
| final Set<String> newlyCreatedTopics = internalTopicManager.makeReady(changelogTopicMetadata); | ||
| log.debug("Created state changelog topics {} from the parsed topology.", changelogTopicMetadata.values()); | ||
| return changelogsByStatefulTask; | ||
| return newlyCreatedTopics; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -703,12 +701,12 @@ private boolean assignTasksToClients(final Set<String> allSourceTopics, | |
| final Map<Integer, Set<TaskId>> tasksForTopicGroup = new HashMap<>(); | ||
| populateTasksForMaps(taskForPartition, tasksForTopicGroup, allSourceTopics, partitionsForTask, fullMetadata); | ||
|
|
||
| final Map<TaskId, Set<TopicPartition>> changelogsByStatefulTask = | ||
| prepareChangelogTopics(topicGroups, tasksForTopicGroup); | ||
| final Map<TaskId, Set<TopicPartition>> changelogsByStatefulTask = new HashMap<>(); | ||
| final Set<String> newlyCreatedChangelogs = prepareChangelogTopics(topicGroups, tasksForTopicGroup, changelogsByStatefulTask); | ||
|
|
||
| final Map<UUID, ClientState> clientStates = new HashMap<>(); | ||
| final boolean lagComputationSuccessful = | ||
| populateClientStatesMap(clientStates, clientMetadataMap, taskForPartition, changelogsByStatefulTask); | ||
| populateClientStatesMap(clientStates, clientMetadataMap, taskForPartition, changelogsByStatefulTask, newlyCreatedChangelogs); | ||
|
|
||
| final Set<TaskId> allTasks = partitionsForTask.keySet(); | ||
| final Set<TaskId> statefulTasks = changelogsByStatefulTask.keySet(); | ||
|
|
@@ -758,16 +756,26 @@ private TaskAssignor createTaskAssignor(final boolean lagComputationSuccessful) | |
| private boolean populateClientStatesMap(final Map<UUID, ClientState> clientStates, | ||
| final Map<UUID, ClientMetadata> clientMetadataMap, | ||
| final Map<TopicPartition, TaskId> taskForPartition, | ||
| final Map<TaskId, Set<TopicPartition>> changelogsByStatefulTask) { | ||
| final Map<TaskId, Set<TopicPartition>> changelogsByStatefulTask, | ||
| final Set<String> newlyCreatedChangelogs) { | ||
| boolean fetchEndOffsetsSuccessful; | ||
| Map<TaskId, Long> allTaskEndOffsetSums; | ||
| try { | ||
| final Collection<TopicPartition> allChangelogPartitions = changelogsByStatefulTask.values().stream() | ||
| .flatMap(Collection::stream) | ||
| .collect(Collectors.toList()); | ||
| final Collection<TopicPartition> allChangelogPartitions = | ||
| changelogsByStatefulTask.values().stream() | ||
| .flatMap(Collection::stream) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| final Collection<TopicPartition> allPreexistingChangelogPartitions = new ArrayList<>(allChangelogPartitions); | ||
| allPreexistingChangelogPartitions.removeIf(partition -> newlyCreatedChangelogs.contains(partition.topic())); | ||
|
|
||
| final Collection<TopicPartition> allNewlyCreatedChangelogPartitions = new ArrayList<>(allChangelogPartitions); | ||
| allNewlyCreatedChangelogPartitions.removeAll(allPreexistingChangelogPartitions); | ||
|
|
||
| final Map<TopicPartition, ListOffsetsResultInfo> endOffsets = | ||
| fetchEndOffsets(allChangelogPartitions, adminClient, Duration.ofMillis(adminClientTimeout)); | ||
| allTaskEndOffsetSums = computeEndOffsetSumsByTask(endOffsets, changelogsByStatefulTask); | ||
| fetchEndOffsets(allPreexistingChangelogPartitions, adminClient, Duration.ofMillis(adminClientTimeout)); | ||
|
|
||
| allTaskEndOffsetSums = computeEndOffsetSumsByTask(endOffsets, changelogsByStatefulTask, allNewlyCreatedChangelogPartitions); | ||
| fetchEndOffsetsSuccessful = true; | ||
| } catch (final StreamsException e) { | ||
| allTaskEndOffsetSums = null; | ||
|
|
@@ -794,20 +802,27 @@ private boolean populateClientStatesMap(final Map<UUID, ClientState> clientState | |
| * @return Map from stateful task to its total end offset summed across all changelog partitions | ||
| */ | ||
| private Map<TaskId, Long> computeEndOffsetSumsByTask(final Map<TopicPartition, ListOffsetsResultInfo> endOffsets, | ||
| final Map<TaskId, Set<TopicPartition>> changelogsByStatefulTask) { | ||
| final Map<TaskId, Set<TopicPartition>> changelogsByStatefulTask, | ||
| final Collection<TopicPartition> newlyCreatedChangelogPartitions) { | ||
| final Map<TaskId, Long> taskEndOffsetSums = new HashMap<>(); | ||
| for (final Map.Entry<TaskId, Set<TopicPartition>> taskEntry : changelogsByStatefulTask.entrySet()) { | ||
| final TaskId task = taskEntry.getKey(); | ||
| final Set<TopicPartition> changelogs = taskEntry.getValue(); | ||
|
|
||
| taskEndOffsetSums.put(task, 0L); | ||
| for (final TopicPartition changelog : changelogs) { | ||
| final ListOffsetsResultInfo offsetResult = endOffsets.get(changelog); | ||
| if (offsetResult == null) { | ||
| log.debug("Fetched end offsets did not contain the changelog {} of task {}", changelog, task); | ||
| throw new IllegalStateException("Could not get end offset for " + changelog); | ||
| final long changelogEndOffset; | ||
| if (newlyCreatedChangelogPartitions.contains(changelog)) { | ||
| changelogEndOffset = 0L; | ||
| } else { | ||
| final ListOffsetsResultInfo offsetResult = endOffsets.get(changelog); | ||
| if (offsetResult == null) { | ||
| log.debug("Fetched end offsets did not contain the changelog {} of task {}", changelog, task); | ||
| throw new IllegalStateException("Could not get end offset for " + changelog); | ||
| } | ||
| changelogEndOffset = offsetResult.offset(); | ||
| } | ||
| final long newEndOffsetSum = taskEndOffsetSums.get(task) + offsetResult.offset(); | ||
| final long newEndOffsetSum = taskEndOffsetSums.get(task) + changelogEndOffset; | ||
| if (newEndOffsetSum < 0) { | ||
| taskEndOffsetSums.put(task, Long.MAX_VALUE); | ||
| break; | ||
|
|
@@ -1525,38 +1540,6 @@ private static void validateActiveTaskEncoding(final List<TopicPartition> partit | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Internal helper function that creates a Kafka topic | ||
| * | ||
| * @param topicPartitions Map that contains the topic names to be created with the number of partitions | ||
| */ | ||
| private void prepareTopic(final Map<String, InternalTopicConfig> topicPartitions) { | ||
| log.debug("Starting to validate internal topics {} in partition assignor.", topicPartitions); | ||
|
|
||
| // first construct the topics to make ready | ||
| final Map<String, InternalTopicConfig> topicsToMakeReady = new HashMap<>(); | ||
|
|
||
| for (final InternalTopicConfig topic : topicPartitions.values()) { | ||
| final Optional<Integer> numPartitions = topic.numberOfPartitions(); | ||
| if (!numPartitions.isPresent()) { | ||
| throw new StreamsException( | ||
| String.format("%sTopic [%s] number of partitions not defined", | ||
| logPrefix, topic.name()) | ||
| ); | ||
| } | ||
| if (!topic.hasEnforcedNumberOfPartitions()) { | ||
| topic.setNumberOfPartitions(numPartitions.get()); | ||
| } | ||
| topicsToMakeReady.put(topic.name(), topic); | ||
| } | ||
|
Comment on lines
1536
to
1551
Member
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. Am I crazy or is this entire block not actually doing anything?
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. It should do some sanity checks -- but I agree it's not easy to grok... Maybe we also incrementally refactored the code and this method become useless? All tests passed?
Member
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 guess to be fair it was still validating that every topic had the
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. The logic indeed seem redundant to me. |
||
|
|
||
| if (!topicsToMakeReady.isEmpty()) { | ||
| internalTopicManager.makeReady(topicsToMakeReady); | ||
| } | ||
|
|
||
| log.debug("Completed validating internal topics {} in partition assignor.", topicPartitions); | ||
| } | ||
|
|
||
| private void ensureCopartitioning(final Collection<Set<String>> copartitionGroups, | ||
| final Map<String, InternalTopicConfig> allRepartitionTopicsNumPartitions, | ||
| final Cluster metadata) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -235,7 +235,7 @@ private MockInternalTopicManager configurePartitionAssignorWith(final Map<String | |
| partitionAssignor.configure(configMap); | ||
| EasyMock.replay(taskManager, adminClient); | ||
|
|
||
| return overwriteInternalTopicManagerWithMock(); | ||
| return overwriteInternalTopicManagerWithMock(false); | ||
| } | ||
|
|
||
| private void createDefaultMockTaskManager() { | ||
|
|
@@ -281,9 +281,13 @@ private void createMockAdminClient(final Map<TopicPartition, Long> changelogEndO | |
| EasyMock.replay(result); | ||
| } | ||
|
|
||
| private MockInternalTopicManager overwriteInternalTopicManagerWithMock() { | ||
| final MockInternalTopicManager mockInternalTopicManager = | ||
| new MockInternalTopicManager(new StreamsConfig(configProps()), mockClientSupplier.restoreConsumer); | ||
| // If mockCreateInternalTopics is true the internal topic manager will report that it had to create all internal | ||
| // topics and we will skip the listOffsets request for these changelogs | ||
| private MockInternalTopicManager overwriteInternalTopicManagerWithMock(final boolean mockCreateInternalTopics) { | ||
| final MockInternalTopicManager mockInternalTopicManager = new MockInternalTopicManager( | ||
| new StreamsConfig(configProps()), | ||
| mockClientSupplier.restoreConsumer, | ||
| mockCreateInternalTopics); | ||
| partitionAssignor.setInternalTopicManager(mockInternalTopicManager); | ||
| return mockInternalTopicManager; | ||
| } | ||
|
|
@@ -1863,6 +1867,35 @@ public void shouldThrowIllegalStateExceptionIfAnyTopicsMissingFromChangelogEndOf | |
| assertThrows(IllegalStateException.class, () -> partitionAssignor.assign(metadata, new GroupSubscription(subscriptions))); | ||
| } | ||
|
|
||
| @Test | ||
| public void shouldSkipListOffsetsRequestForNewlyCreatedChangelogTopics() { | ||
| adminClient = EasyMock.createMock(AdminClient.class); | ||
| final ListOffsetsResult result = EasyMock.createNiceMock(ListOffsetsResult.class); | ||
| final KafkaFutureImpl<Map<TopicPartition, ListOffsetsResultInfo>> allFuture = new KafkaFutureImpl<>(); | ||
| allFuture.complete(emptyMap()); | ||
|
|
||
| expect(adminClient.listOffsets(emptyMap())).andStubReturn(result); | ||
| expect(result.all()).andReturn(allFuture); | ||
|
|
||
| builder.addSource(null, "source1", null, null, null, "topic1"); | ||
| builder.addProcessor("processor1", new MockProcessorSupplier(), "source1"); | ||
| builder.addStateStore(new MockKeyValueStoreBuilder("store1", false), "processor1"); | ||
|
|
||
| subscriptions.put("consumer10", | ||
| new Subscription( | ||
| singletonList("topic1"), | ||
| defaultSubscriptionInfo.encode() | ||
| )); | ||
|
|
||
| EasyMock.replay(result); | ||
|
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. Don't we need to reply
Member
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 gets replayed during configuration (at the end of |
||
| configureDefault(); | ||
| overwriteInternalTopicManagerWithMock(true); | ||
|
|
||
| partitionAssignor.assign(metadata, new GroupSubscription(subscriptions)); | ||
|
|
||
| EasyMock.verify(adminClient); | ||
| } | ||
|
|
||
| private static ByteBuffer encodeFutureSubscription() { | ||
| final ByteBuffer buf = ByteBuffer.allocate(4 /* used version */ + 4 /* supported version */); | ||
| buf.putInt(LATEST_SUPPORTED_VERSION + 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.
It's not what you signed up for, but I'm wondering if we should at least submit a Jira to give some of these AdminClient methods a "full consistency" mode. In other words, since the command returns a future anyway, it would be nice to be able to tell it not to return until it can guarantee the topic will appear to be fully created on all brokers.
I'm mildly concerned that we're just kicking the can down the road a little ways with this PR. I.e., we let the assignment happen, but then some other metadata (or data) operation for that topic will just fail shortly thereafter.
More generally, we jump through a lot of hoops in our own tests to try and make sure that the topics are really, actually created (or deleted) before proceeding with the test, and I'm sure that our users also suffer from the same problem in their testing and production code.
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.
I think this race condition was particularly severe since we do the listOffsets request pretty much immediately after creating the topics, whereas whatever we're doing with that topic next will not be until the rebalance was completed.
AFAIK we've never had any users report subsequent operations failing after the first rebalance due to not-yet-fully-created topics, but it could have just slipped past us
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.
I do agree it would be useful though. Feel free to create a ticket :P
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.
Thanks! Will do. I just wanted to bounce the idea off you first, in case it was stupid.