-
Notifications
You must be signed in to change notification settings - Fork 2.5k
[HUDI-3478] Support CDC for Spark in Hudi #6476
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 14 commits
3a7ecca
d0903c6
399b3f5
0c987c5
adb1a8d
bcdba4a
d28c42e
9e4a620
9c4b0b7
0286c11
f303178
853a59b
ff8f82f
34088ae
dfe4136
0947d8c
a274deb
c24f150
eef1ba6
5e320dd
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,229 @@ | ||
| /* | ||
| * 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.io; | ||
|
|
||
| import org.apache.avro.generic.GenericData; | ||
| import org.apache.avro.generic.GenericRecord; | ||
| import org.apache.avro.generic.IndexedRecord; | ||
|
|
||
| import org.apache.hudi.avro.HoodieAvroUtils; | ||
| import org.apache.hudi.avro.SerializableRecord; | ||
| import org.apache.hudi.common.model.HoodieRecord; | ||
| import org.apache.hudi.common.model.HoodieRecordPayload; | ||
| import org.apache.hudi.common.table.HoodieTableConfig; | ||
| import org.apache.hudi.common.table.cdc.HoodieCDCOperation; | ||
| import org.apache.hudi.common.table.cdc.HoodieCDCUtils; | ||
| import org.apache.hudi.common.table.log.AppendResult; | ||
| import org.apache.hudi.common.table.log.HoodieLogFormat; | ||
| import org.apache.hudi.common.table.log.block.HoodieCDCDataBlock; | ||
| import org.apache.hudi.common.table.log.block.HoodieLogBlock; | ||
| import org.apache.hudi.common.util.DefaultSizeEstimator; | ||
| import org.apache.hudi.common.util.Option; | ||
| import org.apache.hudi.common.util.StringUtils; | ||
| import org.apache.hudi.common.util.collection.ExternalSpillableMap; | ||
| import org.apache.hudi.config.HoodieWriteConfig; | ||
| import org.apache.hudi.exception.HoodieException; | ||
| import org.apache.hudi.exception.HoodieIOException; | ||
| import org.apache.hudi.exception.HoodieUpsertException; | ||
|
|
||
| import java.io.Closeable; | ||
| import java.io.IOException; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.atomic.AtomicLong; | ||
| import java.util.function.Function; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| public class HoodieCDCLogger<T extends HoodieRecordPayload> implements Closeable { | ||
|
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 don't think we need to parameterize this by
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. yes. updated. |
||
|
|
||
| private final String partitionPath; | ||
|
|
||
| private final String fileName; | ||
|
|
||
| private final String commitTime; | ||
|
|
||
| private final List<String> keyFields; | ||
|
|
||
| private final int taskPartitionId; | ||
|
|
||
| private final boolean populateMetaFields; | ||
|
|
||
| // writer for cdc data | ||
| private final HoodieLogFormat.Writer cdcWriter; | ||
|
|
||
| private final boolean cdcEnabled; | ||
|
|
||
| private final String cdcSupplementalLoggingMode; | ||
|
|
||
| // the cdc data | ||
| private final Map<String, SerializableRecord> cdcData; | ||
|
|
||
| private final Function<GenericRecord, GenericRecord> rewriteRecordFunc; | ||
|
|
||
| // the count of records currently being written, used to generate the same seqno for the cdc data | ||
| private final AtomicLong writtenRecordCount = new AtomicLong(-1); | ||
|
|
||
| public HoodieCDCLogger( | ||
| String partitionPath, | ||
| String fileName, | ||
| String commitTime, | ||
| HoodieWriteConfig config, | ||
| List<String> keyFields, | ||
| int taskPartitionId, | ||
| HoodieLogFormat.Writer cdcWriter, | ||
| long maxInMemorySizeInBytes, | ||
| Function<GenericRecord, GenericRecord> rewriteRecordFunc) { | ||
| try { | ||
| this.partitionPath = partitionPath; | ||
| this.fileName = fileName; | ||
| this.commitTime = commitTime; | ||
| this.keyFields = keyFields; | ||
| this.taskPartitionId = taskPartitionId; | ||
| this.populateMetaFields = config.populateMetaFields(); | ||
| this.cdcWriter = cdcWriter; | ||
| this.rewriteRecordFunc = rewriteRecordFunc; | ||
|
|
||
| this.cdcEnabled = config.getBooleanOrDefault(HoodieTableConfig.CDC_ENABLED); | ||
| this.cdcSupplementalLoggingMode = config.getStringOrDefault(HoodieTableConfig.CDC_SUPPLEMENTAL_LOGGING_MODE); | ||
| this.cdcData = new ExternalSpillableMap<>( | ||
| maxInMemorySizeInBytes, | ||
| config.getSpillableMapBasePath(), | ||
| new DefaultSizeEstimator<>(), | ||
| new DefaultSizeEstimator<>(), | ||
| config.getCommonConfig().getSpillableDiskMapType(), | ||
| config.getCommonConfig().isBitCaskDiskMapCompressionEnabled() | ||
| ); | ||
| } catch (IOException e) { | ||
| throw new HoodieUpsertException("Failed to initialize HoodieCDCLogger", e); | ||
| } | ||
| } | ||
|
|
||
| public void put(HoodieRecord<T> hoodieRecord, GenericRecord oldRecord, Option<IndexedRecord> indexedRecord) { | ||
| if (cdcEnabled) { | ||
| String recordKey; | ||
| if (oldRecord == null) { | ||
| recordKey = hoodieRecord.getRecordKey(); | ||
| } else { | ||
| recordKey = StringUtils.join( | ||
|
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. Please check my previous comment that we have to use proper KeyGen here
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. has fixed this. use |
||
| keyFields.stream().map(keyField -> oldRecord.get(keyField).toString()).toArray(String[]::new), | ||
| ":"); | ||
| } | ||
| if (indexedRecord.isPresent()) { | ||
| GenericRecord record = (GenericRecord) indexedRecord.get(); | ||
| if (oldRecord == null) { | ||
| // inserted cdc record | ||
| cdcData.put(recordKey, createCDCRecord(HoodieCDCOperation.INSERT, recordKey, partitionPath, | ||
| null, record)); | ||
| } else { | ||
| // updated cdc record | ||
| cdcData.put(recordKey, createCDCRecord(HoodieCDCOperation.UPDATE, recordKey, partitionPath, | ||
| oldRecord, record)); | ||
| } | ||
| } else { | ||
| // deleted cdc record | ||
| cdcData.put(recordKey, createCDCRecord(HoodieCDCOperation.DELETE, recordKey, partitionPath, | ||
| oldRecord, null)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private SerializableRecord createCDCRecord(HoodieCDCOperation operation, | ||
| String recordKey, | ||
| String partitionPath, | ||
| GenericRecord oldRecord, | ||
| GenericRecord newRecord) { | ||
| GenericData.Record record; | ||
| if (cdcSupplementalLoggingMode.equals(HoodieTableConfig.CDC_SUPPLEMENTAL_LOGGING_MODE_WITH_BEFORE_AFTER)) { | ||
| record = HoodieCDCUtils.cdcRecord(operation.getValue(), commitTime, | ||
| oldRecord, addCommitMetadata(newRecord, recordKey, partitionPath)); | ||
| } else if (cdcSupplementalLoggingMode.equals(HoodieTableConfig.CDC_SUPPLEMENTAL_LOGGING_MODE_WITH_BEFORE)) { | ||
| record = HoodieCDCUtils.cdcRecord(operation.getValue(), recordKey, oldRecord); | ||
| } else { | ||
| record = HoodieCDCUtils.cdcRecord(operation.getValue(), recordKey); | ||
| } | ||
| return new SerializableRecord(record); | ||
| } | ||
|
|
||
| private GenericRecord addCommitMetadata(GenericRecord record, String recordKey, String partitionPath) { | ||
| if (record != null && populateMetaFields) { | ||
| GenericRecord rewriteRecord = rewriteRecordFunc.apply(record); | ||
| String seqId = HoodieRecord.generateSequenceId(commitTime, taskPartitionId, writtenRecordCount.get()); | ||
| HoodieAvroUtils.addCommitMetadataToRecord(rewriteRecord, commitTime, seqId); | ||
| HoodieAvroUtils.addHoodieKeyToRecord(rewriteRecord, recordKey, partitionPath, fileName); | ||
| return rewriteRecord; | ||
| } | ||
| return record; | ||
| } | ||
|
|
||
| public long getAndIncrement() { | ||
| return writtenRecordCount.getAndIncrement(); | ||
| } | ||
|
|
||
| public boolean isEmpty() { | ||
| return !this.cdcEnabled || this.cdcData.isEmpty(); | ||
| } | ||
|
|
||
| public Option<AppendResult> writeCDCData() { | ||
| if (isEmpty()) { | ||
| return Option.empty(); | ||
| } | ||
| try { | ||
| Map<HoodieLogBlock.HeaderMetadataType, String> header = buildCDCBlockHeader(); | ||
| List<IndexedRecord> records = cdcData.values().stream() | ||
| .map(SerializableRecord::getRecord).collect(Collectors.toList()); | ||
| HoodieLogBlock block = new HoodieCDCDataBlock(records, header, | ||
| StringUtils.join(keyFields.toArray(new String[0]), ",")); | ||
| AppendResult result = cdcWriter.appendBlocks(Collections.singletonList(block)); | ||
|
|
||
| // call close to trigger the data flush. | ||
| this.close(); | ||
|
|
||
| return Option.of(result); | ||
| } catch (Exception e) { | ||
| throw new HoodieException("Failed to write the cdc data to " + cdcWriter.getLogFile().getPath(), e); | ||
| } | ||
| } | ||
|
|
||
| private Map<HoodieLogBlock.HeaderMetadataType, String> buildCDCBlockHeader() { | ||
| Map<HoodieLogBlock.HeaderMetadataType, String> header = new HashMap<>(); | ||
| header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, commitTime); | ||
| if (cdcSupplementalLoggingMode.equals(HoodieTableConfig.CDC_SUPPLEMENTAL_LOGGING_MODE_WITH_BEFORE_AFTER)) { | ||
| header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, HoodieCDCUtils.CDC_SCHEMA_STRING); | ||
| } else if (cdcSupplementalLoggingMode.equals(HoodieTableConfig.CDC_SUPPLEMENTAL_LOGGING_MODE_WITH_BEFORE)) { | ||
| header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, HoodieCDCUtils.CDC_SCHEMA_OP_RECORDKEY_BEFORE_STRING); | ||
| } else { | ||
| header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, HoodieCDCUtils.CDC_SCHEMA_OP_AND_RECORDKEY_STRING); | ||
| } | ||
| return header; | ||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
| if (cdcWriter != null) { | ||
| try { | ||
| cdcWriter.close(); | ||
| } catch (IOException e) { | ||
| throw new HoodieIOException("Failed to close HoodieCDCLogger", e); | ||
| } | ||
| } | ||
| cdcData.clear(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,9 +31,12 @@ | |
| import org.apache.hudi.common.model.HoodieWriteStat; | ||
| import org.apache.hudi.common.model.HoodieWriteStat.RuntimeStats; | ||
| import org.apache.hudi.common.model.IOType; | ||
| import org.apache.hudi.common.table.HoodieTableConfig; | ||
| import org.apache.hudi.common.table.log.AppendResult; | ||
| import org.apache.hudi.common.util.DefaultSizeEstimator; | ||
| import org.apache.hudi.common.util.HoodieRecordSizeEstimator; | ||
| import org.apache.hudi.common.util.Option; | ||
| import org.apache.hudi.common.util.StringUtils; | ||
| import org.apache.hudi.common.util.ValidationUtils; | ||
| import org.apache.hudi.common.util.collection.ExternalSpillableMap; | ||
| import org.apache.hudi.config.HoodieWriteConfig; | ||
|
|
@@ -102,6 +105,10 @@ public class HoodieMergeHandle<T extends HoodieRecordPayload, I, K, O> extends H | |
| protected Map<String, HoodieRecord<T>> keyToNewRecords; | ||
| protected Set<String> writtenRecordKeys; | ||
| protected HoodieFileWriter<IndexedRecord> fileWriter; | ||
| // a flag that indicate whether allow the change data to write out a cdc log file. | ||
| protected boolean cdcEnabled = false; | ||
| // used to write cdc data | ||
| protected HoodieCDCLogger<T> cdcLogger; | ||
| private boolean preserveMetadata = false; | ||
|
|
||
| protected Path newFilePath; | ||
|
|
@@ -203,6 +210,21 @@ private void init(String fileId, String partitionPath, HoodieBaseFile baseFileTo | |
| // Create the writer for writing the new version file | ||
| fileWriter = createNewFileWriter(instantTime, newFilePath, hoodieTable, config, | ||
| writeSchemaWithMetaFields, taskContextSupplier); | ||
|
|
||
| // init the cdc logger | ||
| this.cdcEnabled = config.getBooleanOrDefault(HoodieTableConfig.CDC_ENABLED); | ||
| if (cdcEnabled) { | ||
| this.cdcLogger = new HoodieCDCLogger<>( | ||
| partitionPath, | ||
| newFilePath.getName(), | ||
| instantTime, | ||
| config, | ||
| keyFields, | ||
| getPartitionId(), | ||
| createLogWriter(Option.empty(), instantTime), | ||
| IOUtils.getMaxMemoryPerPartitionMerge(taskContextSupplier, config), | ||
| this::rewriteRecord); | ||
| } | ||
| } catch (IOException io) { | ||
| LOG.error("Error in update task at commit " + instantTime, io); | ||
| writeStatus.setGlobalError(io); | ||
|
|
@@ -281,7 +303,11 @@ private boolean writeUpdateRecord(HoodieRecord<T> hoodieRecord, GenericRecord ol | |
| return false; | ||
| } | ||
| } | ||
| return writeRecord(hoodieRecord, indexedRecord, isDelete); | ||
| boolean result = writeRecord(hoodieRecord, indexedRecord, isDelete); | ||
| if (result && cdcEnabled) { | ||
| cdcLogger.put(hoodieRecord, oldRecord, indexedRecord); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| protected void writeInsertRecord(HoodieRecord<T> hoodieRecord) throws IOException { | ||
|
|
@@ -292,6 +318,9 @@ protected void writeInsertRecord(HoodieRecord<T> hoodieRecord) throws IOExceptio | |
| return; | ||
| } | ||
| if (writeRecord(hoodieRecord, insertRecord, HoodieOperation.isDelete(hoodieRecord.getOperation()))) { | ||
| if (cdcEnabled) { | ||
| cdcLogger.put(hoodieRecord, null, insertRecord); | ||
| } | ||
| insertRecordsWritten++; | ||
| } | ||
| } | ||
|
|
@@ -385,6 +414,9 @@ protected void writeToFile(HoodieKey key, GenericRecord avroRecord, boolean shou | |
| } else { | ||
| fileWriter.writeAvroWithMetadata(key, rewriteRecord(avroRecord)); | ||
| } | ||
| if (cdcEnabled) { | ||
| cdcLogger.getAndIncrement(); | ||
|
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. This should not be needed |
||
| } | ||
| } | ||
|
|
||
| protected void writeIncomingRecords() throws IOException { | ||
|
|
@@ -399,9 +431,36 @@ protected void writeIncomingRecords() throws IOException { | |
| } | ||
| } | ||
|
|
||
| private void setCDCStatIfNeeded(HoodieWriteStat stat) { | ||
| try { | ||
| Option<AppendResult> cdcResult; | ||
| if (cdcLogger == null || recordsWritten == 0L || (recordsWritten == insertRecordsWritten)) { | ||
| // the following cases where we do not need to write out the cdc file: | ||
| // case 1: all the data from the previous file slice are deleted. and no new data is inserted; | ||
| // case 2: all the data are new-coming, | ||
| cdcResult = Option.empty(); | ||
| } else { | ||
| cdcResult = cdcLogger.writeCDCData(); | ||
|
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. Flushing CDC logger should be occurring w/in the Handles' |
||
| } | ||
|
|
||
| if (cdcResult.isPresent()) { | ||
| Path cdcLogFile = cdcResult.get().logFile().getPath(); | ||
| String cdcFileName = cdcLogFile.getName(); | ||
| String cdcPath = StringUtils.isNullOrEmpty(partitionPath) ? cdcFileName : partitionPath + "/" + cdcFileName; | ||
| long cdcFileSizeInBytes = FSUtils.getFileSize(fs, cdcLogFile); | ||
| stat.setCdcPath(cdcPath); | ||
| stat.setCdcWriteBytes(cdcFileSizeInBytes); | ||
| } | ||
| } catch (IOException e) { | ||
| throw new HoodieUpsertException("Failed to set cdc write stat", e); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public List<WriteStatus> close() { | ||
| try { | ||
| HoodieWriteStat stat = writeStatus.getStat(); | ||
|
|
||
| writeIncomingRecords(); | ||
|
|
||
| if (keyToNewRecords instanceof ExternalSpillableMap) { | ||
|
|
@@ -416,9 +475,10 @@ public List<WriteStatus> close() { | |
| fileWriter = null; | ||
| } | ||
|
|
||
| long fileSizeInBytes = FSUtils.getFileSize(fs, newFilePath); | ||
| HoodieWriteStat stat = writeStatus.getStat(); | ||
| // if there are cdc data written, set the CDC-related information. | ||
| setCDCStatIfNeeded(stat); | ||
|
|
||
| long fileSizeInBytes = FSUtils.getFileSize(fs, newFilePath); | ||
| stat.setTotalWriteBytes(fileSizeInBytes); | ||
| stat.setFileSizeInBytes(fileSizeInBytes); | ||
| stat.setNumWrites(recordsWritten); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.