-
Notifications
You must be signed in to change notification settings - Fork 2.5k
fix: initialize lastCompletedTxnAndMetadata before recommitInstant in Flink StreamWriteOperatorCoordinator #19023
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
base: master
Are you sure you want to change the base?
Changes from all commits
7f6b79b
7ed75d5
25dfa28
03d4810
d25fd02
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 |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.hudi.util; | ||
|
|
||
| import org.apache.hudi.common.table.timeline.HoodieInstant; | ||
| import org.apache.hudi.common.util.Option; | ||
| import org.apache.hudi.common.util.collection.Pair; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * Memorizes transaction states by instant for Flink streaming writes. | ||
| */ | ||
| public class TxnStateMemo { | ||
|
|
||
| private final Map<String, TxnState> memo = new HashMap<>(); | ||
|
|
||
| public void memo(String instant, | ||
| Option<Pair<HoodieInstant, Map<String, String>>> lastCompletedTxnAndMetadata, | ||
| Set<String> conflictResolutionExclusionInstants, | ||
| Set<String> pendingInflightAndRequestedInstants) { | ||
| memo.put(instant, new TxnState( | ||
| lastCompletedTxnAndMetadata, | ||
| new HashSet<>(conflictResolutionExclusionInstants), | ||
| new HashSet<>(pendingInflightAndRequestedInstants))); | ||
| } | ||
|
|
||
| public Option<TxnState> get(String instant) { | ||
| return Option.ofNullable(memo.get(instant)); | ||
| } | ||
|
|
||
| public void slip(String instant) { | ||
|
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. 🤖 nit: - AI-generated; verify before applying. React 👍/👎 to flag quality. |
||
| memo.remove(instant); | ||
| } | ||
|
|
||
| public boolean contains(String currentInstant) { | ||
| return memo.containsKey(currentInstant); | ||
| } | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public static class TxnState { | ||
| private final Option<Pair<HoodieInstant, Map<String, String>>> lastCompletedTxnAndMetadata; | ||
| private final Set<String> conflictResolutionExclusionInstants; | ||
| private final Set<String> pendingInflightAndRequestedInstants; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -435,10 +435,9 @@ private CompletableFuture<CoordinationResponse> handleInFlightInstantsRequest(Co | |
|
|
||
| private void restoreEvents() { | ||
| if (this.eventBuffers.nonEmpty()) { | ||
| final HoodieTimeline completedTimeline = this.metaClient.getActiveTimeline().filterCompletedInstants(); | ||
| final HoodieTimeline completedTimeline = this.metaClient.reloadActiveTimeline().filterCompletedInstants(); | ||
| this.eventBuffers.getEventBufferStream() | ||
| .forEach(entry -> recommitInstant(completedTimeline, entry.getKey(), entry.getValue().getLeft(), entry.getValue().getRight())); | ||
| this.metaClient.reloadActiveTimeline(); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -510,8 +509,6 @@ private void initEventBufferIfNecessary() { | |
| private String startInstant() { | ||
| // refresh the meta client which is reused | ||
| metaClient.reloadActiveTimeline(); | ||
| // refresh the last txn metadata | ||
| this.writeClient.preTxn(tableState.operationType, this.metaClient); | ||
| // put the assignment in front of metadata generation, | ||
| // because the instant request from write task is asynchronous. | ||
| this.instant = this.writeClient.startCommit(tableState.commitAction, this.metaClient); | ||
|
|
@@ -523,6 +520,8 @@ private String startInstant() { | |
| this.writeClient.setWriteTimer(tableState.commitAction); | ||
| log.info("Create instant [{}] for table [{}] with type [{}]", this.instant, | ||
| this.conf.get(FlinkOptions.TABLE_NAME), conf.get(FlinkOptions.TABLE_TYPE)); | ||
| // refresh the last txn metadata | ||
| this.writeClient.preTxn(tableState.operationType, this.metaClient, this.instant, this.eventBuffers.getAllInstants()); | ||
| return this.instant; | ||
| } | ||
|
|
||
|
|
@@ -547,10 +546,14 @@ private boolean recommitInstant(HoodieTimeline completedTimeline, long checkpoin | |
| if (writeClient.getConfig().getFailedWritesCleanPolicy().isLazy()) { | ||
| writeClient.getHeartbeatClient().start(instant); | ||
| } | ||
| // Initialize the transaction state so same-writer instants can be excluded | ||
| // during OCC conflict resolution. | ||
| writeClient.preTxn(tableState.operationType, this.metaClient, instant, this.eventBuffers.getAllInstants()); | ||
|
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. 🤖 I think the multi-pending recommit case is still broken — could you double-check? With A and B both inflight on restart, after A's recommit succeeds, - AI-generated; verify before applying. React 👍/👎 to flag quality. |
||
| return commitInstant(checkpointId, instant, bootstrapBuffer); | ||
| } else { | ||
| // clean the corresponding event buffer if the instant is already committed. | ||
| eventBuffers.reset(checkpointId); | ||
| writeClient.cleanResources(instant); | ||
| return false; | ||
| } | ||
| } | ||
|
|
@@ -614,24 +617,26 @@ private boolean commitInstants(long checkpointId) { | |
| * @return true if the write statuses are committed successfully. | ||
| */ | ||
| private boolean commitInstant(long checkpointId, String instant, EventBuffer eventBuffer) { | ||
| if (eventBuffer.isEmptyDataWriteBuffer()) { | ||
| // all the data write tasks are reset by failover, reset the while buffer and returns early. | ||
| this.eventBuffers.reset(checkpointId); | ||
| // stop the heart beat for lazy cleaning | ||
| writeClient.cleanResources(instant); | ||
| return false; | ||
| } | ||
| try { | ||
| if (eventBuffer.isEmptyDataWriteBuffer()) { | ||
| // all the data write tasks are reset by failover, reset the while buffer and returns early. | ||
| this.eventBuffers.reset(checkpointId); | ||
| return false; | ||
| } | ||
|
|
||
| List<WriteStatus> dataWriteResults = eventBuffer.collectDataWriteStatuses(); | ||
| if (dataWriteResults.isEmpty() && !OptionsResolver.allowCommitOnEmptyBatch(conf)) { | ||
| // No data has written, reset the buffer and returns early | ||
| this.eventBuffers.reset(checkpointId); | ||
| // stop the heart beat for lazy cleaning | ||
| List<WriteStatus> dataWriteResults = eventBuffer.collectDataWriteStatuses(); | ||
| if (dataWriteResults.isEmpty() && !OptionsResolver.allowCommitOnEmptyBatch(conf)) { | ||
| // No data has written, reset the buffer and returns early | ||
| this.eventBuffers.reset(checkpointId); | ||
| return false; | ||
| } | ||
| doCommit(checkpointId, instant, dataWriteResults, eventBuffer.collectIndexWriteStatuses()); | ||
| return true; | ||
| } finally { | ||
| // Stop the heartbeat and remove the memoized transaction state regardless of | ||
| // whether commit succeeds or fails before the coordinator restarts. | ||
| writeClient.cleanResources(instant); | ||
| return false; | ||
| } | ||
| doCommit(checkpointId, instant, dataWriteResults, eventBuffer.collectIndexWriteStatuses()); | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -652,6 +657,7 @@ private void doCommit(long checkpointId, String instant, List<WriteStatus> dataW | |
| FlinkValidatorUtils.runValidators(conf, instant, allWriteStatus, | ||
| checkpointCommitMetadata, () -> StreamerUtil.getPreviousCommitMetadata(this.metaClient)); | ||
|
|
||
| this.writeClient.loadTxn(instant); | ||
| boolean success = writeClient.commit(instant, allWriteStatus, Option.of(checkpointCommitMetadata), | ||
| tableState.commitAction, partitionToReplacedFileIds); | ||
| if (success) { | ||
|
|
||
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.
🤖 nit: could you rename this to
recordorput? Using a noun as a verb (txnStateMemo.memo(...)at the call site) is unusual in Java APIs and reads a bit awkwardly.- AI-generated; verify before applying. React 👍/👎 to flag quality.