Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
import org.apache.hudi.common.model.HoodieWriteStat.RuntimeStats;
import org.apache.hudi.common.model.IOType;
import org.apache.hudi.common.table.log.AppendResult;
import org.apache.hudi.common.table.log.HoodieLogFormat;
import org.apache.hudi.common.table.log.HoodieLogFormat.Writer;
import org.apache.hudi.common.table.log.block.HoodieAvroDataBlock;
import org.apache.hudi.common.table.log.block.HoodieDeleteBlock;
Expand Down Expand Up @@ -471,23 +470,6 @@ public List<WriteStatus> writeStatuses() {
return statuses;
}

private Writer createLogWriter(Option<FileSlice> fileSlice, String baseCommitTime)
Comment thread
xushiyan marked this conversation as resolved.
throws IOException {
Option<HoodieLogFile> latestLogFile = fileSlice.get().getLatestLogFile();

return HoodieLogFormat.newWriterBuilder()
.onParentPath(FSUtils.getPartitionPath(hoodieTable.getMetaClient().getBasePath(), partitionPath))
.withFileId(fileId)
.overBaseCommit(baseCommitTime)
.withLogVersion(latestLogFile.map(HoodieLogFile::getLogVersion).orElse(HoodieLogFile.LOGFILE_BASE_VERSION))
.withFileSize(latestLogFile.map(HoodieLogFile::getFileSize).orElse(0L))
.withSizeThreshold(config.getLogFileMaxSize())
.withFs(fs)
.withRolloverLogWriteToken(writeToken)
.withLogWriteToken(latestLogFile.map(x -> FSUtils.getWriteTokenFromLogPath(x.getPath())).orElse(writeToken))
.withFileExtension(HoodieLogFile.DELTA_EXTENSION).build();
}

/**
* Whether there is need to update the record location.
*/
Expand Down
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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to parameterize this by HoodieRecordPayload

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has fixed this. use KeyGen.

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
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -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++;
}
}
Expand Down Expand Up @@ -385,6 +414,9 @@ protected void writeToFile(HoodieKey key, GenericRecord avroRecord, boolean shou
} else {
fileWriter.writeAvroWithMetadata(key, rewriteRecord(avroRecord));
}
if (cdcEnabled) {
cdcLogger.getAndIncrement();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed

}
}

protected void writeIncomingRecords() throws IOException {
Expand All @@ -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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flushing CDC logger should be occurring w/in the Handles' close method not in setCDCStatIfNeeded (setter should just be tasked w/ setting up the WriteStat)

}

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) {
Expand All @@ -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);
Expand Down
Loading