-
Notifications
You must be signed in to change notification settings - Fork 2.5k
[HUDI-6784] Support deletion logic in merger #9593
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 |
|---|---|---|
|
|
@@ -22,7 +22,9 @@ | |
| import org.apache.hudi.common.config.TypedProperties; | ||
| import org.apache.hudi.common.engine.TaskContextSupplier; | ||
| import org.apache.hudi.common.fs.FSUtils; | ||
| import org.apache.hudi.common.model.EmptyHoodieRecordPayload; | ||
| import org.apache.hudi.common.model.HoodieBaseFile; | ||
| import org.apache.hudi.common.model.HoodieEmptyRecord; | ||
| import org.apache.hudi.common.model.HoodieKey; | ||
| import org.apache.hudi.common.model.HoodieOperation; | ||
| import org.apache.hudi.common.model.HoodiePartitionMetadata; | ||
|
|
@@ -267,26 +269,82 @@ protected void init(String fileId, Iterator<HoodieRecord<T>> newRecordsItr) { | |
|
|
||
| protected boolean writeUpdateRecord(HoodieRecord<T> newRecord, HoodieRecord<T> oldRecord, Option<HoodieRecord> combineRecordOpt, Schema writerSchema) throws IOException { | ||
| boolean isDelete = false; | ||
| // Precondition: combined record exists. | ||
| if (combineRecordOpt.isPresent()) { | ||
| if (oldRecord.getData() != combineRecordOpt.get().getData()) { | ||
| // the incoming record is chosen | ||
| isDelete = HoodieOperation.isDelete(newRecord.getOperation()); | ||
| } else { | ||
| // the incoming record is dropped | ||
| // Save old data. | ||
| if (oldRecord.getData() == combineRecordOpt.get().getData()) { | ||
| return false; | ||
| } | ||
| updatedRecordsWritten++; | ||
|
|
||
| // If the new record is a delete record, we do delete no matter what | ||
| // the combined record is. | ||
| if (isDeleteRecord(newRecord, writerSchema, config.getProps())) { | ||
| doDelete(newRecord); | ||
| return true; | ||
| } | ||
|
|
||
| // Until now we know this is not a delete case, we inject custom logic to decide | ||
| // if the combined record should be written. Three cases here: | ||
| // 1. If the output is empty, the old data is written for safety since empty means unknown. | ||
| // 2. If the output is a delete record, the delete count is increased. | ||
| // 3. Otherwise, the resulting record is passed to the downstream. | ||
| Option<Pair<HoodieRecord, Schema>> processedRecord = recordMerger.insert( | ||
| combineRecordOpt.get(), writerSchema, config.getProps()); | ||
| if (!processedRecord.isPresent()) { | ||
| // Save old data. | ||
| return false; | ||
| } else if (isDeleteRecord(processedRecord.get().getLeft(), processedRecord.get().getRight(), config.getProps())) { | ||
| doDelete(newRecord); | ||
| return true; | ||
| } else { | ||
| updatedRecordsWritten++; | ||
| combineRecordOpt = Option.of(processedRecord.get().getLeft()); | ||
| writerSchema = processedRecord.get().getRight(); | ||
| } | ||
| } | ||
|
|
||
| // TODO: clean logic in `writeRecord` function. | ||
| // Write the record finally. | ||
| return writeRecord(newRecord, combineRecordOpt, writerSchema, config.getPayloadConfig().getProps(), isDelete); | ||
| } | ||
|
|
||
| protected void doDelete(HoodieRecord record) { | ||
| recordsDeleted++; | ||
| updatedRecordsWritten++; | ||
| record.unseal(); | ||
| record.clearNewLocation(); | ||
| record.seal(); | ||
| record.deflate(); | ||
| writeStatus.markSuccess(record, record.getMetadata()); | ||
| } | ||
|
|
||
| protected boolean isDeleteRecord(HoodieRecord record, Schema schema, TypedProperties props) throws IOException { | ||
| return record.isDelete(schema, props) | ||
| || record instanceof HoodieEmptyRecord | ||
| || (record.getData() != null && record.getData() instanceof EmptyHoodieRecordPayload) | ||
| || HoodieOperation.isDelete(record.getOperation()); | ||
|
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. What are the gains we bind all these check together ? Empty record and delete record are meant to be dropped but are they handled in the same logic originally?
Collaborator
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. Based on my understanding, Hudi uses both empty record and delete record interchangeably to mean delete. So I combine here. I will modify this PR based on our discussion so this logic should be gone. But we should find a way to standardize these logics; otherwise, the scattered logic is really confusing.
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. Revert all the changes to /**
* Checks the merged record valility before flushing into dist, if returns false, the given record would be ignored.
* In some scenarios, the bussiness logic needs to check the validity of the merged record, so this interface give
* a chance for the user to do a sanity check.
*
* <p> This interface is experimental and might got evolved in the future.
**/
@Experimental
default boolean isValid(HoodieRecord record, Schema schema) {
return true;
}This interface would be invoked before each merged record flushing. Only merged record needs this check currently. |
||
| } | ||
|
|
||
| protected boolean isValidRecord(HoodieRecord record, Schema schema, TypedProperties props) throws IOException { | ||
| return !isDeleteRecord(record, schema, props) && !record.shouldIgnore(schema, props); | ||
| } | ||
|
|
||
| protected void writeInsertRecord(HoodieRecord<T> newRecord) throws IOException { | ||
| Schema schema = useWriterSchemaForCompaction ? writeSchemaWithMetaFields : writeSchema; | ||
| // just skip the ignored record | ||
| if (newRecord.shouldIgnore(schema, config.getProps())) { | ||
| return; | ||
| } | ||
linliu-code marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| writeInsertRecord(newRecord, schema, config.getProps()); | ||
|
|
||
| // Insert possible insert logic. | ||
| Option<Pair<HoodieRecord, Schema>> processedRecord = recordMerger.insert( | ||
| newRecord, schema, config.getProps()); | ||
| if (!processedRecord.isPresent() | ||
| || !isValidRecord(processedRecord.get().getLeft(), schema, config.getProps())) { | ||
| return; | ||
| } | ||
|
|
||
| writeInsertRecord(processedRecord.get().getLeft(), schema, config.getProps()); | ||
| } | ||
|
|
||
| protected void writeInsertRecord(HoodieRecord<T> newRecord, Schema schema, Properties prop) | ||
|
|
@@ -309,6 +367,10 @@ private boolean writeRecord(HoodieRecord<T> newRecord, Option<HoodieRecord> comb | |
| return false; | ||
| } | ||
| try { | ||
| // Cases for DELETE: | ||
| // (1) The merged record is empty. | ||
| // (2) The merged record is a delete record. | ||
| // (3) The new record is a delete record. | ||
| if (combineRecord.isPresent() && !combineRecord.get().isDelete(schema, config.getProps()) && !isDelete) { | ||
| writeToFile(newRecord.getKey(), combineRecord.get(), schema, prop, preserveMetadata && useWriterSchemaForCompaction); | ||
| recordsWritten++; | ||
|
|
@@ -321,8 +383,7 @@ private boolean writeRecord(HoodieRecord<T> newRecord, Option<HoodieRecord> comb | |
| } | ||
| writeStatus.markSuccess(newRecord, recordMetadata); | ||
| // deflate record payload after recording success. This will help users access payload as a | ||
| // part of marking | ||
| // record successful. | ||
| // part of marking record successful. | ||
| newRecord.deflate(); | ||
| return true; | ||
| } catch (Exception e) { | ||
|
|
@@ -346,9 +407,12 @@ public void write(HoodieRecord<T> oldRecord) { | |
| // writing the first record. So make a copy of the record to be merged | ||
| HoodieRecord<T> newRecord = keyToNewRecords.get(key).newInstance(); | ||
| try { | ||
| Option<Pair<HoodieRecord, Schema>> mergeResult = recordMerger.merge(oldRecord, oldSchema, newRecord, newSchema, props); | ||
| // Inject default/custom merging logic for update. | ||
| Option<Pair<HoodieRecord, Schema>> mergeResult = recordMerger.merge( | ||
linliu-code marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| oldRecord, oldSchema, newRecord, newSchema, props); | ||
| Schema combineRecordSchema = mergeResult.map(Pair::getRight).orElse(null); | ||
| Option<HoodieRecord> combinedRecord = mergeResult.map(Pair::getLeft); | ||
|
|
||
| if (combinedRecord.isPresent() && combinedRecord.get().shouldIgnore(combineRecordSchema, props)) { | ||
| // If it is an IGNORE_RECORD, just copy the old record, and do not update the new record. | ||
| copyOldRecord = true; | ||
|
|
@@ -398,12 +462,17 @@ protected void writeToFile(HoodieKey key, HoodieRecord<T> record, Schema schema, | |
|
|
||
| protected void writeIncomingRecords() throws IOException { | ||
| // write out any pending records (this can happen when inserts are turned into updates) | ||
| Schema oldSchema = config.populateMetaFields() ? writeSchemaWithMetaFields : writeSchema; | ||
| Schema newSchema = useWriterSchemaForCompaction ? writeSchemaWithMetaFields : writeSchema; | ||
| TypedProperties props = config.getPayloadConfig().getProps(); | ||
|
|
||
| Iterator<HoodieRecord<T>> newRecordsItr = (keyToNewRecords instanceof ExternalSpillableMap) | ||
| ? ((ExternalSpillableMap)keyToNewRecords).iterator() : keyToNewRecords.values().iterator(); | ||
|
|
||
| while (newRecordsItr.hasNext()) { | ||
| HoodieRecord<T> hoodieRecord = newRecordsItr.next(); | ||
| if (!writtenRecordKeys.contains(hoodieRecord.getRecordKey())) { | ||
| writeInsertRecord(hoodieRecord); | ||
| HoodieRecord<T> record = newRecordsItr.next(); | ||
| if (!writtenRecordKeys.contains(record.getRecordKey())) { | ||
| writeInsertRecord(record); | ||
| } | ||
| } | ||
| } | ||
|
|
||
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 revert all the changes to create handle, the delete messages already got handled in line 137.
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 allow the custom logic?