-
Notifications
You must be signed in to change notification settings - Fork 3k
Kafka Connect: Handle no coordinator and data loss in ICR mode #12372
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
Kafka Connect: Handle no coordinator and data loss in ICR mode #12372
Conversation
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/IcebergSinkTask.java
Outdated
Show resolved
Hide resolved
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/ResourceType.java
Outdated
Show resolved
Hide resolved
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/channel/CommitterImpl.java
Outdated
Show resolved
Hide resolved
| coordinatorThread.terminate(); | ||
| coordinatorThread = null; | ||
| @Override | ||
| public void stop(ResourceType resourceType) { |
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.
Likewise, we can just call the type-specific methods directly instead.
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/channel/CommitterImpl.java
Outdated
Show resolved
Hide resolved
|
We should add some comments, and also tests if feasible. Also looks like there are some code formatting issues. |
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/Committer.java
Outdated
Show resolved
Hide resolved
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/Committer.java
Outdated
Show resolved
Hide resolved
| committer = CommitterFactory.createCommitter(config); | ||
| committer.start(catalog, config, context); | ||
| // We should be starting co-ordinator only the list of partitions has the zeroth partition. | ||
| if(committer.isCoordinator(partitions)) { |
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.
We should move this logic into the committer implementation.
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.
If we want to move this logic to the committer then we need to somehow pass the the partitions information and that would require modification to the commuter method signatures
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/IcebergSinkTask.java
Outdated
Show resolved
Hide resolved
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/IcebergSinkTask.java
Outdated
Show resolved
Hide resolved
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/IcebergSinkTask.java
Outdated
Show resolved
Hide resolved
…handle backward compatibility
…handle backward compatibility
… owned offsets and this avoida extra work
…_data_loss_in_current_design_in_ICR_mode_test_latest Handling no coordinator and data loss in current design in icr mode test latest
|
|
||
| class CommitterFactory { | ||
| static Committer createCommitter(IcebergSinkConfig config) { | ||
| static Committer createCommitter() { |
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.
We should leave this as-is for now and address changes in the PR to support different committers.
| @Override | ||
| public void start(Map<String, String> props) { | ||
| this.config = new IcebergSinkConfig(props); | ||
| // Catalog and committer are global resources and do not depend on the topic partition; |
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 don't feel this comment is necessary
| private Collection<MemberDescription> membersWhenWorkerIsCoordinator; | ||
| private final AtomicBoolean isCommitterInitialized = new AtomicBoolean(false); | ||
|
|
||
| void initializeCommitter(Catalog catalog, IcebergSinkConfig config, SinkTaskContext context) { |
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.
This should be private.
| private final AtomicBoolean isCommitterInitialized = new AtomicBoolean(false); | ||
|
|
||
| void initializeCommitter(Catalog catalog, IcebergSinkConfig config, SinkTaskContext context) { | ||
| if (isCommitterInitialized.compareAndSet(false, true)) { |
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 feel checking for initialization is being overly conservative, given this would only happen in cases where Kafka Connect isn't following the sink task contract. We can add a precondition check instead, in methods that require initialization. Also, we shouldn't need an atomic as methods should be called from the main task thread.
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.
This check is kind of required as the objects being created here are passed as reference to the worker and coordinator and every open call is changing that especially the KafkaClient. Also this is not a lock and this will be a synchronous call for all the task. This also prevents redundant assignment at every open call.
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.
Also the atomic ds are very efficient and this is just a check and set and will happen only once in the lifetime of a task. Also all task are independent of each other so this will not block anything as this is not a lock just a compare and set.
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 see, it might be a bit confusing calling committer start from the task open. What do you think of naming the new committer methods open and close instead, to align with the task API?
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.
(BTW the atomic is fine, that's safer anyway, thanks)
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.
Yeah, I was thinking that initially but did not made change. Should I rename the new methods as open and close for committer interface.
| private SinkTaskContext context; | ||
| private KafkaClientFactory clientFactory; | ||
| private Collection<MemberDescription> membersWhenWorkerIsCoordinator; | ||
| private final AtomicBoolean isCommitterInitialized = new AtomicBoolean(false); |
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.
We can name this initialized or isInitialized.
| private Collection<MemberDescription> membersWhenWorkerIsCoordinator; | ||
| private final AtomicBoolean isCommitterInitialized = new AtomicBoolean(false); | ||
|
|
||
| private void initializeCommitter(Catalog catalog, IcebergSinkConfig config, SinkTaskContext context) { |
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.
Let's name this just initialize
| public void start(Catalog catalog, IcebergSinkConfig config, SinkTaskContext context) { | ||
| KafkaClientFactory clientFactory = new KafkaClientFactory(config.kafkaProps()); | ||
|
|
||
| public boolean hasLeaderPartition(Collection<TopicPartition> currentAssignedPartitions) { |
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.
This should be private
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.
Also, nitpick, I feel we should remove isLeader and fold the logic in here, having the two methods with similar names is somewhat confusing.
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 left that just for better readability of the code. I can make that change.
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.
Also that method is being used in one of the tests, and did not wanted to make extra changes.
| } | ||
| } | ||
|
|
||
| public void startWorker() { |
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 following new methods should be private
| public void start(Map<String, String> props) { | ||
| this.config = new IcebergSinkConfig(props); | ||
| catalog = CatalogUtils.loadCatalog(config); | ||
| committer = CommitterFactory.createCommitter(config); |
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.
When assigning member variables, you should use the this. prefix.
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.
Ohh, sure, missed this.
| if (isInitialized.compareAndSet(false, true)) { | ||
| this.icebergCatalog = catalog; | ||
| this.icebergSinkConfig = config; | ||
| this.sinkTaskContext = context; |
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.
We should revert the variable names to what they were.
| } | ||
|
|
||
| private void startCoordinator() { | ||
| LOG.info("Task elected leader, starting commit coordinator"); |
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.
We should protect against multiple coordinator threads here
|
Thanks @kumarpritam863 for the research on this and the contribution! |
|
Thanks @bryanck for all the support, insights and reviews. |
|
@kumarpritam863, can you please check: #13593 |
Few Observations:
In the ICR mode "open" receives only the "newly added partitions", and "open" will "not be called" by connect framework if there are No New Partitions Assigned to the Task.
Similarly in case of "Close" the Task receives only the "removed partitions" but we blindly close the "Co-ordinator".
The coordinator is created only in case "open" is called but in case when a partition is revoked and no partition is added on the task then only close will be called with that revoked partition without any open call.
How this is leading to NO-Coordinator Scenario:
Let's see this with the below example:
Initially we had one worker "W0" with two tasks "T0" and "T1" consuming from two partitions of one topic namely "P0" and "P1", so the initial configuration is:
W0 -> [{T0,P0}, {T1, P1}] -> this will elect "T0" as the co-ordinator as it has "P0"
Now another worker "W1" joins:-> this will lead to rebalancing on both tasks as well as topic partitions within those tasks.
State at this point of time:
W0 -> [{T0,[P0, P1]}]
W1 -> []
Now,
Assume P1 is removed from T0:
Hence this leads to a No-Coordinator scenario.
Data Loss Scenario:
In Incremental Cooperative Rebalancing (ICR) mode, when rebalance happens, consumers do not stop consuming as their is no stop the world like in "Eager" mode of rebalancing.
In case a partition is removed from a task, Consumer co-ordinator calls "close(Collection()) of the sink Task. In this call since we are blindly dumping all the files, this will dump the records also for the partitions still retained by this task. Moreover close call will make the committer null and since we have not null check for commiter in the put(Collection) , this will silently ignore the records. Once we get an open(Collection) call from Kafka this will start the commiter without resetting the offsets which leads to data loss.
Document explaining both the scenario: https://docs.google.com/document/d/1okqGq1HXu2rDnq88wIlVDv0EmNFZYB1PhgwyAzWIKT8/edit?tab=t.0#heading=h.51qcys2ewbsa