From 5405a6287bca1f9b3692316367c0806ed21e5ba5 Mon Sep 17 00:00:00 2001 From: Nishith Agarwal Date: Thu, 15 Feb 2018 11:01:25 -0800 Subject: [PATCH] Introducing HoodieLogFormat V2 with versioning support - HoodieLogFormat V2 has support for LogFormat evolution through versioning - LogVersion is associated with a LogBlock not a LogFile - Based on a version for a LogBlock, approporiate code path is executed - Implemented LazyReading of Hoodie Log Blocks with Memory / IO tradeoff - Implemented Reverse pointer to be able to traverse the log in reverse - Introduce new MAGIC for backwards compatibility with logs without versions --- docs/_data/topnav.yml | 2 +- docs/code_and_design.md | 38 + docs/code_structure.md | 16 - docs/configurations.md | 8 + docs/images/hoodie_log_format_v2.png | Bin 0 -> 223676 bytes .../cli/commands/ArchivedCommitsCommand.java | 12 +- .../hoodie/config/HoodieCompactionConfig.java | 25 + .../uber/hoodie/config/HoodieWriteConfig.java | 12 +- .../uber/hoodie/io/HoodieAppendHandle.java | 17 +- .../hoodie/io/HoodieCommitArchiveLog.java | 8 +- .../compact/HoodieRealtimeTableCompactor.java | 3 +- .../hoodie/table/HoodieMergeOnReadTable.java | 11 +- .../hoodie/io/TestHoodieCommitArchiveLog.java | 2 +- .../hoodie/common/model/HoodieLogFile.java | 13 + .../log/HoodieCompactedLogRecordScanner.java | 261 ++++--- .../common/table/log/HoodieLogFileReader.java | 410 +++++++++++ .../common/table/log/HoodieLogFormat.java | 30 +- .../table/log/HoodieLogFormatReader.java | 201 ++--- .../table/log/HoodieLogFormatWriter.java | 70 +- .../common/table/log/LogFormatVersion.java | 131 ++++ .../table/log/block/HoodieAvroDataBlock.java | 266 +++++-- .../table/log/block/HoodieCommandBlock.java | 63 +- .../table/log/block/HoodieCorruptBlock.java | 65 +- .../table/log/block/HoodieDeleteBlock.java | 84 ++- .../table/log/block/HoodieLogBlock.java | 189 ++++- .../table/log/block/LogBlockVersion.java | 79 ++ .../hoodie/common/model/HoodieTestUtils.java | 7 +- .../common/table/log/HoodieLogFormatTest.java | 693 +++++++++++++----- .../realtime/HoodieRealtimeRecordReader.java | 10 +- .../HoodieRealtimeRecordReaderTest.java | 7 +- .../uber/hoodie/hive/HoodieHiveClient.java | 3 +- .../java/com/uber/hoodie/hive/TestUtil.java | 7 +- 32 files changed, 2066 insertions(+), 677 deletions(-) create mode 100644 docs/code_and_design.md delete mode 100644 docs/code_structure.md create mode 100644 docs/images/hoodie_log_format_v2.png create mode 100644 hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFileReader.java create mode 100644 hoodie-common/src/main/java/com/uber/hoodie/common/table/log/LogFormatVersion.java create mode 100644 hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/LogBlockVersion.java diff --git a/docs/_data/topnav.yml b/docs/_data/topnav.yml index b144d9a83..190573a3e 100644 --- a/docs/_data/topnav.yml +++ b/docs/_data/topnav.yml @@ -23,7 +23,7 @@ topnav_dropdowns: url: /api_docs.html output: web - title: Code Structure - url: /code_structure.html + url: /code_and_design.html output: web - title: Roadmap url: /roadmap.html diff --git a/docs/code_and_design.md b/docs/code_and_design.md new file mode 100644 index 000000000..3baaa97b4 --- /dev/null +++ b/docs/code_and_design.md @@ -0,0 +1,38 @@ +--- +title: Code Structure +keywords: usecases +sidebar: mydoc_sidebar +permalink: code_and_design.html +--- + +## Code & Project Structure + + * hoodie-client : Spark client library to take a bunch of inserts + updates and apply them to a Hoodie table + * hoodie-common : Common code shared between different artifacts of Hoodie + + ## HoodieLogFormat + + The following diagram depicts the LogFormat for Hoodie MergeOnRead. Each logfile consists of one or more log blocks. + Each logblock follows the format shown below. + + | Field | Description | + |-------------- |------------------| + | MAGIC | A magic header that marks the start of a block | + | VERSION | The version of the LogFormat, this helps define how to switch between different log format as it evolves | + | TYPE | The type of the log block | + | HEADER LENGTH | The length of the headers, 0 if no headers | + | HEADER | Metadata needed for a log block. For eg. INSTANT_TIME, TARGET_INSTANT_TIME, SCHEMA etc. | + | CONTENT LENGTH | The length of the content of the log block | + | CONTENT | The content of the log block, for example, for a DATA_BLOCK, the content is (number of records + actual records) in byte [] | + | FOOTER LENGTH | The length of the footers, 0 if no footers | + | FOOTER | Metadata needed for a log block. For eg. index entries, a bloom filter for records in a DATA_BLOCK etc. | + | LOGBLOCK LENGTH | The total number of bytes written for a log block, typically the SUM(everything_above). This is a LONG. This acts as a reverse pointer to be able to traverse the log in reverse.| + + + {% include image.html file="hoodie_log_format_v2.png" alt="hoodie_log_format_v2.png" %} + + + + + + diff --git a/docs/code_structure.md b/docs/code_structure.md deleted file mode 100644 index 2550c905e..000000000 --- a/docs/code_structure.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Code Structure -keywords: usecases -sidebar: mydoc_sidebar -permalink: code_structure.html ---- - -## Code & Project Structure - - * hoodie-client : Spark client library to take a bunch of inserts + updates and apply them to a Hoodie table - * hoodie-common : Common code shared between different artifacts of Hoodie - - - - - diff --git a/docs/configurations.md b/docs/configurations.md index 328c5a6e7..2ff607707 100644 --- a/docs/configurations.md +++ b/docs/configurations.md @@ -74,6 +74,14 @@ summary: "Here we list all possible configurations and what they mean" Should hoodie dynamically compute the insertSplitSize based on the last 24 commit's metadata. Turned off by default. - [approxRecordSize](#approxRecordSize) ()
The average record size. If specified, hoodie will use this and not compute dynamically based on the last 24 commit's metadata. No value set as default. This is critical in computing the insert parallelism and bin-packing inserts into small files. See above. + - [withCompactionLazyBlockReadEnabled](#withCompactionLazyBlockReadEnabled) (true)
+ When a CompactedLogScanner merges all log files, this config helps to choose whether the logblocks should be read lazily or not. Choose true to use I/O intensive lazy block reading (low memory usage) or false for Memory intensive immediate block read (high memory usage) + - [withMaxNumDeltaCommitsBeforeCompaction](#withMaxNumDeltaCommitsBeforeCompaction) (maxNumDeltaCommitsBeforeCompaction = 10)
+ Number of max delta commits to keep before triggering an inline compaction + - [withCompactionReverseLogReadEnabled](#withCompactionReverseLogReadEnabled) (false)
+ HoodieLogFormatReader reads a logfile in the forward direction starting from pos=0 to pos=file_length. If this config is set to true, the Reader reads the logfile in reverse direction, from pos=file_length to pos=0 + - [withMaxMemorySizePerCompactionInBytes](#withMaxMemorySizePerCompactionInBytes) (maxMemorySizePerCompactionInBytes = 1GB)
+ HoodieCompactedLogScanner reads logblocks, converts records to HoodieRecords and then merges these log blocks and records. At any point, the number of entries in a log block can be less than or equal to the number of entries in the corresponding parquet file. This can lead to OOM in the Scanner. Hence, a spillable map helps alleviate the memory pressure. Use this config to set the max allowable inMemory footprint of the spillable map. - [withMetricsConfig](#withMetricsConfig) (HoodieMetricsConfig)
Hoodie publishes metrics on every commit, clean, rollback etc. diff --git a/docs/images/hoodie_log_format_v2.png b/docs/images/hoodie_log_format_v2.png new file mode 100644 index 0000000000000000000000000000000000000000..c392e2677d54a93776cb06d7623cb4c93bdfb45e GIT binary patch literal 223676 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%0Ag0f>V6lT_7G=l97 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

!po}?ZRTHFmHaiKOExVfo>t(&BPu6L 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

gH;O8v3yIPNXH%q#a?#J>@=z(m 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 L>S_`XT)6%p4&u8a literal 0 HcmV?d00001 diff --git a/hoodie-cli/src/main/java/com/uber/hoodie/cli/commands/ArchivedCommitsCommand.java b/hoodie-cli/src/main/java/com/uber/hoodie/cli/commands/ArchivedCommitsCommand.java index d4945c012..50fb6a565 100644 --- a/hoodie-cli/src/main/java/com/uber/hoodie/cli/commands/ArchivedCommitsCommand.java +++ b/hoodie-cli/src/main/java/com/uber/hoodie/cli/commands/ArchivedCommitsCommand.java @@ -58,11 +58,12 @@ public class ArchivedCommitsCommand implements CommandMarker { FileStatus[] fsStatuses = FSUtils.getFs(basePath, HoodieCLI.conf) .globStatus(new Path(basePath + "/.hoodie/.commits_.archive*")); List allCommits = new ArrayList<>(); + int commits = 0; for (FileStatus fs : fsStatuses) { //read the archived file HoodieLogFormat.Reader reader = HoodieLogFormat .newReader(FSUtils.getFs(basePath, HoodieCLI.conf), - new HoodieLogFile(fs.getPath()), HoodieArchivedMetaEntry.getClassSchema(), false); + new HoodieLogFile(fs.getPath()), HoodieArchivedMetaEntry.getClassSchema()); List readRecords = new ArrayList<>(); //read the avro blocks @@ -70,10 +71,17 @@ public class ArchivedCommitsCommand implements CommandMarker { HoodieAvroDataBlock blk = (HoodieAvroDataBlock) reader.next(); List records = blk.getRecords(); readRecords.addAll(records); + if(commits == limit) { + break; + } + commits++; } List readCommits = readRecords.stream().map(r -> (GenericRecord) r) - .map(r -> readCommit(r)).limit(limit).collect(Collectors.toList()); + .map(r -> readCommit(r)).collect(Collectors.toList()); allCommits.addAll(readCommits); + if(commits == limit) { + break; + } } return HoodiePrintHelper.print( new String[]{"CommitTime", "CommitType", "CommitDetails"}, diff --git a/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieCompactionConfig.java b/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieCompactionConfig.java index 75dd49d6c..6579ccf9f 100644 --- a/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieCompactionConfig.java +++ b/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieCompactionConfig.java @@ -105,6 +105,15 @@ public class HoodieCompactionConfig extends DefaultHoodieConfig { // Default memory size per compaction, excess spills to disk public static final String DEFAULT_MAX_SIZE_IN_MEMORY_PER_COMPACTION_IN_BYTES = String.valueOf(1024*1024*1024L); //1GB + // used to choose a trade off between IO vs Memory when performing compaction process + // Depending on outputfile_size and memory provided, choose true to avoid OOM for large file size + small memory + public static final String COMPACTION_LAZY_BLOCK_READ_ENABLED_PROP = "hoodie.compaction.lazy.block.read"; + public static final String DEFAULT_COMPACTION_LAZY_BLOCK_READ_ENABLED = "false"; + + // used to choose whether to enable reverse log reading (reverse log traversal) + public static final String COMPACTION_REVERSE_LOG_READ_ENABLED_PROP = "hoodie.compaction.reverse.log.read"; + public static final String DEFAULT_COMPACTION_REVERSE_LOG_READ_ENABLED = "false"; + private HoodieCompactionConfig(Properties props) { super(props); } @@ -225,6 +234,18 @@ public class HoodieCompactionConfig extends DefaultHoodieConfig { return this; } + public Builder withCompactionLazyBlockReadEnabled(Boolean compactionLazyBlockReadEnabled) { + props.setProperty(COMPACTION_LAZY_BLOCK_READ_ENABLED_PROP, + String.valueOf(compactionLazyBlockReadEnabled)); + return this; + } + + public Builder withCompactionReverseLogReadEnabled(Boolean compactionReverseLogReadEnabled) { + props.setProperty(COMPACTION_REVERSE_LOG_READ_ENABLED_PROP, + String.valueOf(compactionReverseLogReadEnabled)); + return this; + } + public HoodieCompactionConfig build() { HoodieCompactionConfig config = new HoodieCompactionConfig(props); setDefaultOnCondition(props, !props.containsKey(AUTO_CLEAN_PROP), @@ -262,6 +283,10 @@ public class HoodieCompactionConfig extends DefaultHoodieConfig { TARGET_IO_PER_COMPACTION_IN_MB_PROP, DEFAULT_TARGET_IO_PER_COMPACTION_IN_MB); setDefaultOnCondition(props, !props.containsKey(MAX_SIZE_IN_MEMORY_PER_COMPACTION_IN_BYTES_PROP), MAX_SIZE_IN_MEMORY_PER_COMPACTION_IN_BYTES_PROP, DEFAULT_MAX_SIZE_IN_MEMORY_PER_COMPACTION_IN_BYTES); + setDefaultOnCondition(props, !props.containsKey(COMPACTION_LAZY_BLOCK_READ_ENABLED_PROP), + COMPACTION_LAZY_BLOCK_READ_ENABLED_PROP, DEFAULT_COMPACTION_LAZY_BLOCK_READ_ENABLED); + setDefaultOnCondition(props, !props.containsKey(COMPACTION_REVERSE_LOG_READ_ENABLED_PROP), + COMPACTION_REVERSE_LOG_READ_ENABLED_PROP, DEFAULT_COMPACTION_REVERSE_LOG_READ_ENABLED); HoodieCleaningPolicy.valueOf(props.getProperty(CLEANER_POLICY_PROP)); Preconditions.checkArgument( diff --git a/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieWriteConfig.java b/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieWriteConfig.java index 9933f3c9c..dfae2a082 100644 --- a/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieWriteConfig.java +++ b/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieWriteConfig.java @@ -24,14 +24,14 @@ import com.uber.hoodie.common.util.ReflectionUtils; import com.uber.hoodie.index.HoodieIndex; import com.uber.hoodie.io.compact.strategy.CompactionStrategy; import com.uber.hoodie.metrics.MetricsReporterType; +import org.apache.spark.storage.StorageLevel; +import javax.annotation.concurrent.Immutable; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.util.Map; import java.util.Properties; -import javax.annotation.concurrent.Immutable; -import org.apache.spark.storage.StorageLevel; /** * Class storing configs for the {@link com.uber.hoodie.HoodieWriteClient} @@ -215,6 +215,14 @@ public class HoodieWriteConfig extends DefaultHoodieConfig { .parseLong(props.getProperty(HoodieCompactionConfig.MAX_SIZE_IN_MEMORY_PER_COMPACTION_IN_BYTES_PROP)); } + public Boolean getCompactionLazyBlockReadEnabled() { + return Boolean.valueOf(props.getProperty(HoodieCompactionConfig.COMPACTION_LAZY_BLOCK_READ_ENABLED_PROP)); + } + + public Boolean getCompactionReverseLogReadEnabled() { + return Boolean.valueOf(props.getProperty(HoodieCompactionConfig.COMPACTION_REVERSE_LOG_READ_ENABLED_PROP)); + } + /** * index properties **/ diff --git a/hoodie-client/src/main/java/com/uber/hoodie/io/HoodieAppendHandle.java b/hoodie-client/src/main/java/com/uber/hoodie/io/HoodieAppendHandle.java index 0720b133f..7f00961f2 100644 --- a/hoodie-client/src/main/java/com/uber/hoodie/io/HoodieAppendHandle.java +++ b/hoodie-client/src/main/java/com/uber/hoodie/io/HoodieAppendHandle.java @@ -159,11 +159,14 @@ public class HoodieAppendHandle extends HoodieIOH return Optional.empty(); } + // TODO (NA) - Perform a schema check of current input record with the last schema on log file + // to make sure we don't append records with older (shorter) schema than already appended public void doAppend() { int maxBlockSize = config.getLogFileDataBlockMaxSize(); int numberOfRecords = 0; - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, commitTime); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, commitTime); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); while (recordItr.hasNext()) { HoodieRecord record = recordItr.next(); // update the new location of the record, so we know where to find it next @@ -178,7 +181,7 @@ public class HoodieAppendHandle extends HoodieIOH // Recompute averageRecordSize before writing a new block and update existing value with avg of new and old logger.info("AvgRecordSize => " + averageRecordSize); averageRecordSize = (averageRecordSize + SizeEstimator.estimate(record))/2; - doAppend(metadata); + doAppend(header); numberOfRecords = 0; } Optional indexedRecord = getIndexedRecord(record); @@ -189,18 +192,18 @@ public class HoodieAppendHandle extends HoodieIOH } numberOfRecords++; } - doAppend(metadata); + doAppend(header); } - private void doAppend(Map metadata) { + private void doAppend(Map header) { try { if (recordList.size() > 0) { - writer = writer.appendBlock(new HoodieAvroDataBlock(recordList, schema, metadata)); + writer = writer.appendBlock(new HoodieAvroDataBlock(recordList, header)); recordList.clear(); } if (keysToDelete.size() > 0) { writer = writer.appendBlock( - new HoodieDeleteBlock(keysToDelete.stream().toArray(String[]::new), metadata)); + new HoodieDeleteBlock(keysToDelete.stream().toArray(String[]::new), header)); keysToDelete.clear(); } } catch (Exception e) { diff --git a/hoodie-client/src/main/java/com/uber/hoodie/io/HoodieCommitArchiveLog.java b/hoodie-client/src/main/java/com/uber/hoodie/io/HoodieCommitArchiveLog.java index 6e1f14ab7..d0454e3d2 100644 --- a/hoodie-client/src/main/java/com/uber/hoodie/io/HoodieCommitArchiveLog.java +++ b/hoodie-client/src/main/java/com/uber/hoodie/io/HoodieCommitArchiveLog.java @@ -18,6 +18,7 @@ package com.uber.hoodie.io; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.uber.hoodie.avro.model.HoodieArchivedMetaEntry; import com.uber.hoodie.avro.model.HoodieCleanMetadata; @@ -30,6 +31,7 @@ import com.uber.hoodie.common.table.HoodieTableMetaClient; import com.uber.hoodie.common.table.HoodieTimeline; import com.uber.hoodie.common.table.log.HoodieLogFormat; import com.uber.hoodie.common.table.log.block.HoodieAvroDataBlock; +import com.uber.hoodie.common.table.log.block.HoodieLogBlock; import com.uber.hoodie.common.table.timeline.HoodieArchivedTimeline; import com.uber.hoodie.common.table.timeline.HoodieInstant; import com.uber.hoodie.common.util.AvroUtils; @@ -39,6 +41,7 @@ import com.uber.hoodie.exception.HoodieException; import com.uber.hoodie.exception.HoodieIOException; import com.uber.hoodie.table.HoodieTable; import org.apache.avro.Schema; +import org.apache.avro.file.DataFileStream; import org.apache.avro.generic.IndexedRecord; import org.apache.hadoop.fs.Path; import org.apache.log4j.LogManager; @@ -47,6 +50,7 @@ import org.apache.log4j.Logger; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -190,7 +194,9 @@ public class HoodieCommitArchiveLog { for (HoodieInstant hoodieInstant : instants) { records.add(convertToAvroRecord(commitTimeline, hoodieInstant)); } - HoodieAvroDataBlock block = new HoodieAvroDataBlock(records, wrapperSchema); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, wrapperSchema.toString()); + HoodieAvroDataBlock block = new HoodieAvroDataBlock(records, header); this.writer = writer.appendBlock(block); } catch (Exception e) { throw new HoodieCommitException("Failed to archive commits", e); diff --git a/hoodie-client/src/main/java/com/uber/hoodie/io/compact/HoodieRealtimeTableCompactor.java b/hoodie-client/src/main/java/com/uber/hoodie/io/compact/HoodieRealtimeTableCompactor.java index c72406d12..709c0e9fe 100644 --- a/hoodie-client/src/main/java/com/uber/hoodie/io/compact/HoodieRealtimeTableCompactor.java +++ b/hoodie-client/src/main/java/com/uber/hoodie/io/compact/HoodieRealtimeTableCompactor.java @@ -154,7 +154,8 @@ public class HoodieRealtimeTableCompactor implements HoodieCompactor { HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, metaClient.getBasePath(), - operation.getDeltaFilePaths(), readerSchema, maxInstantTime, config.getMaxMemorySizePerCompactionInBytes()); + operation.getDeltaFilePaths(), readerSchema, maxInstantTime, config.getMaxMemorySizePerCompactionInBytes(), + config.getCompactionLazyBlockReadEnabled(), config.getCompactionReverseLogReadEnabled()); if (!scanner.iterator().hasNext()) { return Lists.newArrayList(); } diff --git a/hoodie-client/src/main/java/com/uber/hoodie/table/HoodieMergeOnReadTable.java b/hoodie-client/src/main/java/com/uber/hoodie/table/HoodieMergeOnReadTable.java index 88d7b8d8a..9cca2ea90 100644 --- a/hoodie-client/src/main/java/com/uber/hoodie/table/HoodieMergeOnReadTable.java +++ b/hoodie-client/src/main/java/com/uber/hoodie/table/HoodieMergeOnReadTable.java @@ -265,14 +265,15 @@ public class HoodieMergeOnReadTable extends .withFileExtension(HoodieLogFile.DELTA_EXTENSION).build(); Long numRollbackBlocks = 0L; // generate metadata - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, metaClient.getActiveTimeline().lastInstant().get().getTimestamp()); - metadata.put(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME, commit); + header.put(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME, commit); + header.put(HoodieLogBlock.HeaderMetadataType.COMMAND_BLOCK_TYPE, + String.valueOf(HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK.ordinal())); // if update belongs to an existing log file writer = writer.appendBlock(new HoodieCommandBlock( - HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK, - metadata)); + header)); numRollbackBlocks++; filesToNumBlocksRollback .put(this.getMetaClient().getFs().getFileStatus(writer.getLogFile().getPath()), diff --git a/hoodie-client/src/test/java/com/uber/hoodie/io/TestHoodieCommitArchiveLog.java b/hoodie-client/src/test/java/com/uber/hoodie/io/TestHoodieCommitArchiveLog.java index 3add748c9..1e5d015d5 100644 --- a/hoodie-client/src/test/java/com/uber/hoodie/io/TestHoodieCommitArchiveLog.java +++ b/hoodie-client/src/test/java/com/uber/hoodie/io/TestHoodieCommitArchiveLog.java @@ -122,7 +122,7 @@ public class TestHoodieCommitArchiveLog { //read the file HoodieLogFormat.Reader reader = HoodieLogFormat .newReader(fs, new HoodieLogFile(new Path(basePath + "/.hoodie/.commits_.archive.1")), - HoodieArchivedMetaEntry.getClassSchema(), false); + HoodieArchivedMetaEntry.getClassSchema()); int archivedRecordsCount = 0; List readRecords = new ArrayList<>(); diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/model/HoodieLogFile.java b/hoodie-common/src/main/java/com/uber/hoodie/common/model/HoodieLogFile.java index c4a527914..0c587c671 100644 --- a/hoodie-common/src/main/java/com/uber/hoodie/common/model/HoodieLogFile.java +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/model/HoodieLogFile.java @@ -101,6 +101,19 @@ public class HoodieLogFile implements Serializable { }; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HoodieLogFile that = (HoodieLogFile) o; + return path != null ? path.equals(that.path) : that.path == null; + } + + @Override + public int hashCode() { + return path != null ? path.hashCode() : 0; + } + @Override public String toString() { return "HoodieLogFile {" + path + '}'; diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieCompactedLogRecordScanner.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieCompactedLogRecordScanner.java index 465b53867..98dd29b78 100644 --- a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieCompactedLogRecordScanner.java +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieCompactedLogRecordScanner.java @@ -30,6 +30,7 @@ import com.uber.hoodie.common.table.log.block.HoodieLogBlock; import com.uber.hoodie.common.util.SpillableMapUtils; import com.uber.hoodie.common.util.collection.ExternalSpillableMap; import com.uber.hoodie.exception.HoodieIOException; +import java.util.stream.Collectors; import org.apache.avro.Schema; import org.apache.avro.generic.GenericRecord; import org.apache.avro.generic.IndexedRecord; @@ -48,14 +49,25 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; +import static com.uber.hoodie.common.table.log.block.HoodieLogBlock.HeaderMetadataType.INSTANT_TIME; import static com.uber.hoodie.common.table.log.block.HoodieLogBlock.HoodieLogBlockType.CORRUPT_BLOCK; -import static com.uber.hoodie.common.table.log.block.HoodieLogBlock.LogMetadataType.INSTANT_TIME; /** * Scans through all the blocks in a list of HoodieLogFile and builds up a compacted/merged list of * records which will be used as a lookup table when merging the base columnar file with the redo * log file. + * NOTE: If readBlockLazily is turned on, does not merge, instead keeps reading log blocks and merges everything at once + * This is an optimization to avoid seek() back and forth to read new block (forward seek()) + * and lazily read content of seen block (reverse and forward seek()) during merge + * | | Read Block 1 Metadata | | Read Block 1 Data | + * | | Read Block 2 Metadata | | Read Block 2 Data | + * | I/O Pass 1 | ..................... | I/O Pass 2 | ................. | + * | | Read Block N Metadata | | Read Block N Data | + * + * This results in two I/O passes over the log file. + * */ + public class HoodieCompactedLogRecordScanner implements Iterable> { @@ -77,10 +89,11 @@ public class HoodieCompactedLogRecordScanner implements // Merge strategy to use when combining records from log private String payloadClassFQN; // Store the last instant log blocks (needed to implement rollback) - Deque currentInstantLogBlocks = new ArrayDeque<>(); + private Deque currentInstantLogBlocks = new ArrayDeque<>(); public HoodieCompactedLogRecordScanner(FileSystem fs, String basePath, List logFilePaths, - Schema readerSchema, String latestInstantTime, Long maxMemorySizeInBytes) { + Schema readerSchema, String latestInstantTime, Long maxMemorySizeInBytes, + boolean readBlocksLazily, boolean reverseReader) { this.readerSchema = readerSchema; this.latestInstantTime = latestInstantTime; this.hoodieTableMetaClient = new HoodieTableMetaClient(fs.getConf(), basePath); @@ -88,137 +101,141 @@ public class HoodieCompactedLogRecordScanner implements this.payloadClassFQN = this.hoodieTableMetaClient.getTableConfig().getPayloadClass(); try { - // Store merged records for all versions for this log file, set the maxInMemoryMapSize to half, - // assign other half to the temporary map needed to read next block - records = new ExternalSpillableMap<>(maxMemorySizeInBytes, readerSchema, + // Store merged records for all versions for this log file, set the in-memory footprint to maxInMemoryMapSize + this.records = new ExternalSpillableMap<>(maxMemorySizeInBytes, readerSchema, payloadClassFQN, Optional.empty()); // iterate over the paths - Iterator logFilePathsItr = logFilePaths.iterator(); - while (logFilePathsItr.hasNext()) { - HoodieLogFile logFile = new HoodieLogFile(new Path(logFilePathsItr.next())); - log.info("Scanning log file " + logFile.getPath()); + HoodieLogFormatReader logFormatReaderWrapper = + new HoodieLogFormatReader(fs, + logFilePaths.stream().map(logFile -> new HoodieLogFile(new Path(logFile))) + .collect(Collectors.toList()), readerSchema, readBlocksLazily, reverseReader); + while (logFormatReaderWrapper.hasNext()) { + HoodieLogFile logFile = logFormatReaderWrapper.getLogFile(); + log.info("Scanning log file " + logFile); totalLogFiles.incrementAndGet(); - // Use the HoodieLogFormatReader to iterate through the blocks in the log file - HoodieLogFormatReader reader = new HoodieLogFormatReader(fs, logFile, readerSchema, true); - while (reader.hasNext()) { - HoodieLogBlock r = reader.next(); - if (r.getBlockType() != CORRUPT_BLOCK && - !HoodieTimeline.compareTimestamps(r.getLogMetadata().get(INSTANT_TIME), this.latestInstantTime, - HoodieTimeline.LESSER_OR_EQUAL)) { - //hit a block with instant time greater than should be processed, stop processing further - break; - } - switch (r.getBlockType()) { - case AVRO_DATA_BLOCK: - log.info("Reading a data block from file " + logFile.getPath()); - // Consider the following scenario - // (Time 0, C1, Task T1) -> Running - // (Time 1, C1, Task T1) -> Failed (Wrote either a corrupt block or a correct DataBlock (B1) with commitTime C1 - // (Time 2, C1, Task T1.2) -> Running (Task T1 was retried and the attempt number is 2) - // (Time 3, C1, Task T1.2) -> Finished (Wrote a correct DataBlock B2) - // Now a logFile L1 can have 2 correct Datablocks (B1 and B2) which are the same. - // Say, commit C1 eventually failed and a rollback is triggered. - // Rollback will write only 1 rollback block (R1) since it assumes one block is written per ingestion batch for a file, - // but in reality we need to rollback (B1 & B2) - // The following code ensures the same rollback block (R1) is used to rollback both B1 & B2 - if(isNewInstantBlock(r)) { - // If this is a avro data block, then merge the last block records into the main result - merge(records, currentInstantLogBlocks); - } - // store the current block - currentInstantLogBlocks.push(r); - break; - case DELETE_BLOCK: - log.info("Reading a delete block from file " + logFile.getPath()); - if (isNewInstantBlock(r)) { - // Block with the keys listed as to be deleted, data and delete blocks written in different batches - // so it is safe to merge - // This is a delete block, so lets merge any records from previous data block - merge(records, currentInstantLogBlocks); - } - // store deletes so can be rolled back - currentInstantLogBlocks.push(r); - break; - case COMMAND_BLOCK: - log.info("Reading a command block from file " + logFile.getPath()); - // This is a command block - take appropriate action based on the command - HoodieCommandBlock commandBlock = (HoodieCommandBlock) r; - String targetInstantForCommandBlock = r.getLogMetadata() - .get(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME); - switch (commandBlock.getType()) { // there can be different types of command blocks - case ROLLBACK_PREVIOUS_BLOCK: - // Rollback the last read log block - // Get commit time from last record block, compare with targetCommitTime, rollback only if equal, - // this is required in scenarios of invalid/extra rollback blocks written due to failures during - // the rollback operation itself and ensures the same rollback block (R1) is used to rollback - // both B1 & B2 with same instant_time - int numBlocksRolledBack = 0; - while(!currentInstantLogBlocks.isEmpty()) { - HoodieLogBlock lastBlock = currentInstantLogBlocks.peek(); - // handle corrupt blocks separately since they may not have metadata - if (lastBlock.getBlockType() == CORRUPT_BLOCK) { - log.info( - "Rolling back the last corrupted log block read in " + logFile.getPath()); - currentInstantLogBlocks.pop(); - numBlocksRolledBack++; - } - // rollback last data block or delete block - else if (lastBlock.getBlockType() != CORRUPT_BLOCK && - targetInstantForCommandBlock - .contentEquals(lastBlock.getLogMetadata().get(INSTANT_TIME))) { - log.info("Rolling back the last log block read in " + logFile.getPath()); - currentInstantLogBlocks.pop(); - numBlocksRolledBack++; - } - // invalid or extra rollback block - else if(!targetInstantForCommandBlock - .contentEquals(currentInstantLogBlocks.peek().getLogMetadata().get(INSTANT_TIME))) { - log.warn("Invalid or extra rollback command block in " + logFile.getPath()); - break; - } - // this should not happen ideally - else { - log.warn("Unable to apply rollback command block in " + logFile.getPath()); - } - } - log.info("Number of applied rollback blocks " + numBlocksRolledBack); - break; - - } - break; - case CORRUPT_BLOCK: - log.info("Found a corrupt block in " + logFile.getPath()); - // If there is a corrupt block - we will assume that this was the next data block - currentInstantLogBlocks.push(r); - break; - } + // Use the HoodieLogFileReader to iterate through the blocks in the log file + HoodieLogBlock r = logFormatReaderWrapper.next(); + if (r.getBlockType() != CORRUPT_BLOCK && + !HoodieTimeline.compareTimestamps(r.getLogBlockHeader().get(INSTANT_TIME), + this.latestInstantTime, + HoodieTimeline.LESSER_OR_EQUAL)) { + //hit a block with instant time greater than should be processed, stop processing further + break; } - // merge the last read block when all the blocks are done reading - if (!currentInstantLogBlocks.isEmpty()) { - log.info("Merging the final blocks in " + logFile.getPath()); - merge(records, currentInstantLogBlocks); + switch (r.getBlockType()) { + case AVRO_DATA_BLOCK: + log.info("Reading a data block from file " + logFile.getPath()); + if (isNewInstantBlock(r) && !readBlocksLazily) { + // If this is an avro data block belonging to a different commit/instant, + // then merge the last blocks and records into the main result + merge(records, currentInstantLogBlocks); + } + // store the current block + currentInstantLogBlocks.push(r); + break; + case DELETE_BLOCK: + log.info("Reading a delete block from file " + logFile.getPath()); + if (isNewInstantBlock(r) && !readBlocksLazily) { + // If this is a delete data block belonging to a different commit/instant, + // then merge the last blocks and records into the main result + merge(records, currentInstantLogBlocks); + } + // store deletes so can be rolled back + currentInstantLogBlocks.push(r); + break; + case COMMAND_BLOCK: + // Consider the following scenario + // (Time 0, C1, Task T1) -> Running + // (Time 1, C1, Task T1) -> Failed (Wrote either a corrupt block or a correct DataBlock (B1) with commitTime C1 + // (Time 2, C1, Task T1.2) -> Running (Task T1 was retried and the attempt number is 2) + // (Time 3, C1, Task T1.2) -> Finished (Wrote a correct DataBlock B2) + // Now a logFile L1 can have 2 correct Datablocks (B1 and B2) which are the same. + // Say, commit C1 eventually failed and a rollback is triggered. + // Rollback will write only 1 rollback block (R1) since it assumes one block is written per ingestion batch for a file, + // but in reality we need to rollback (B1 & B2) + // The following code ensures the same rollback block (R1) is used to rollback both B1 & B2 + log.info("Reading a command block from file " + logFile.getPath()); + // This is a command block - take appropriate action based on the command + HoodieCommandBlock commandBlock = (HoodieCommandBlock) r; + String targetInstantForCommandBlock = r.getLogBlockHeader() + .get(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME); + switch (commandBlock.getType()) { // there can be different types of command blocks + case ROLLBACK_PREVIOUS_BLOCK: + // Rollback the last read log block + // Get commit time from last record block, compare with targetCommitTime, rollback only if equal, + // this is required in scenarios of invalid/extra rollback blocks written due to failures during + // the rollback operation itself and ensures the same rollback block (R1) is used to rollback + // both B1 & B2 with same instant_time + int numBlocksRolledBack = 0; + while (!currentInstantLogBlocks.isEmpty()) { + HoodieLogBlock lastBlock = currentInstantLogBlocks.peek(); + // handle corrupt blocks separately since they may not have metadata + if (lastBlock.getBlockType() == CORRUPT_BLOCK) { + log.info( + "Rolling back the last corrupted log block read in " + logFile.getPath()); + currentInstantLogBlocks.pop(); + numBlocksRolledBack++; + } + // rollback last data block or delete block + else if (lastBlock.getBlockType() != CORRUPT_BLOCK && + targetInstantForCommandBlock + .contentEquals(lastBlock.getLogBlockHeader().get(INSTANT_TIME))) { + log.info("Rolling back the last log block read in " + logFile.getPath()); + currentInstantLogBlocks.pop(); + numBlocksRolledBack++; + } + // invalid or extra rollback block + else if (!targetInstantForCommandBlock + .contentEquals( + currentInstantLogBlocks.peek().getLogBlockHeader().get(INSTANT_TIME))) { + log.warn("TargetInstantTime " + targetInstantForCommandBlock + + " invalid or extra rollback command block in " + logFile.getPath()); + break; + } + // this should not happen ideally + else { + log.warn("Unable to apply rollback command block in " + logFile.getPath()); + } + } + log.info("Number of applied rollback blocks " + numBlocksRolledBack); + break; + + } + break; + case CORRUPT_BLOCK: + log.info("Found a corrupt block in " + logFile.getPath()); + // If there is a corrupt block - we will assume that this was the next data block + currentInstantLogBlocks.push(r); + break; } } + // merge the last read block when all the blocks are done reading + if (!currentInstantLogBlocks.isEmpty()) { + log.info("Merging the final data blocks"); + merge(records, currentInstantLogBlocks); + } } catch (IOException e) { - throw new HoodieIOException("IOException when reading compacting log files"); + throw new HoodieIOException("IOException when reading log file "); } this.totalRecordsToUpdate = records.size(); log.info("MaxMemoryInBytes allowed for compaction => " + maxMemorySizeInBytes); - log.info("Number of entries in MemoryBasedMap in ExternalSpillableMap => " + records.getInMemoryMapNumEntries()); - log.info("Total size in bytes of MemoryBasedMap in ExternalSpillableMap => " + records.getCurrentInMemoryMapSize()); - log.info("Number of entries in DiskBasedMap in ExternalSpillableMap => " + records.getDiskBasedMapNumEntries()); + log.info("Number of entries in MemoryBasedMap in ExternalSpillableMap => " + records + .getInMemoryMapNumEntries()); + log.info("Total size in bytes of MemoryBasedMap in ExternalSpillableMap => " + records + .getCurrentInMemoryMapSize()); + log.info("Number of entries in DiskBasedMap in ExternalSpillableMap => " + records + .getDiskBasedMapNumEntries()); log.info("Size of file spilled to disk => " + records.getSizeOfFileOnDiskInBytes()); } /** * Checks if the current logblock belongs to a later instant - * @param logBlock - * @return */ private boolean isNewInstantBlock(HoodieLogBlock logBlock) { - return currentInstantLogBlocks.size() > 0 && currentInstantLogBlocks.peek().getBlockType() != CORRUPT_BLOCK - && !logBlock.getLogMetadata().get(INSTANT_TIME) - .contentEquals(currentInstantLogBlocks.peek().getLogMetadata().get(INSTANT_TIME)); + return currentInstantLogBlocks.size() > 0 + && currentInstantLogBlocks.peek().getBlockType() != CORRUPT_BLOCK + && !logBlock.getLogBlockHeader().get(INSTANT_TIME) + .contentEquals(currentInstantLogBlocks.peek().getLogBlockHeader().get(INSTANT_TIME)); } /** @@ -228,7 +245,10 @@ public class HoodieCompactedLogRecordScanner implements */ private Map> loadRecordsFromBlock( HoodieAvroDataBlock dataBlock) throws IOException { - Map> recordsFromLastBlock = Maps.newHashMap(); + // TODO (NA) - Instead of creating a new HashMap use the spillable map + Map> recordsFromLastBlock = Maps + .newHashMap(); + // TODO (NA) - Implemnt getRecordItr() in HoodieAvroDataBlock and use that here List recs = dataBlock.getRecords(); totalLogRecords.addAndGet(recs.size()); recs.forEach(rec -> { @@ -255,7 +275,7 @@ public class HoodieCompactedLogRecordScanner implements * Merge the last seen log blocks with the accumulated records */ private void merge(Map> records, - Deque lastBlocks) throws IOException { + Deque lastBlocks) throws IOException { while (!lastBlocks.isEmpty()) { // poll the element at the bottom of the stack since that's the order it was inserted HoodieLogBlock lastBlock = lastBlocks.pollLast(); @@ -265,6 +285,7 @@ public class HoodieCompactedLogRecordScanner implements break; case DELETE_BLOCK: // TODO : If delete is the only block written and/or records are present in parquet file + // TODO : Mark as tombstone (optional.empty()) for data instead of deleting the entry Arrays.stream(((HoodieDeleteBlock) lastBlock).getKeysToDelete()).forEach(records::remove); break; case CORRUPT_BLOCK: @@ -278,7 +299,7 @@ public class HoodieCompactedLogRecordScanner implements * Merge the records read from a single data block with the accumulated records */ private void merge(Map> records, - Map> recordsFromLastBlock) { + Map> recordsFromLastBlock) { recordsFromLastBlock.forEach((key, hoodieRecord) -> { if (records.containsKey(key)) { // Merge and store the merged record diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFileReader.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFileReader.java new file mode 100644 index 000000000..f9a01c8c8 --- /dev/null +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFileReader.java @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2016 Uber Technologies, Inc. (hoodie-dev-group@uber.com) + * + * Licensed 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 com.uber.hoodie.common.table.log; + +import com.google.common.base.Preconditions; +import com.uber.hoodie.common.model.HoodieLogFile; +import com.uber.hoodie.common.table.log.block.HoodieAvroDataBlock; +import com.uber.hoodie.common.table.log.block.HoodieCommandBlock; +import com.uber.hoodie.common.table.log.block.HoodieCorruptBlock; +import com.uber.hoodie.common.table.log.block.HoodieDeleteBlock; +import com.uber.hoodie.common.table.log.block.HoodieLogBlock; +import com.uber.hoodie.common.table.log.block.HoodieLogBlock.HeaderMetadataType; +import com.uber.hoodie.common.table.log.block.HoodieLogBlock.HoodieLogBlockType; +import com.uber.hoodie.exception.CorruptedLogFileException; +import com.uber.hoodie.exception.HoodieIOException; +import com.uber.hoodie.exception.HoodieNotSupportedException; +import org.apache.avro.Schema; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FileSystem; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Scans a log file and provides block level iterator on the log file Loads the entire block + * contents in memory Can emit either a DataBlock, CommandBlock, DeleteBlock or CorruptBlock (if one + * is found) + */ +class HoodieLogFileReader implements HoodieLogFormat.Reader { + + private static final int DEFAULT_BUFFER_SIZE = 4096; + private final static Logger log = LogManager.getLogger(HoodieLogFileReader.class); + + private final FSDataInputStream inputStream; + private final HoodieLogFile logFile; + private static final byte[] oldMagicBuffer = new byte[4]; + private static final byte[] magicBuffer = new byte[6]; + private final Schema readerSchema; + private HoodieLogBlock nextBlock = null; + private LogFormatVersion nextBlockVersion; + private boolean readBlockLazily; + private long reverseLogFilePosition; + private long lastReverseLogFilePosition; + private boolean reverseReader; + + HoodieLogFileReader(FileSystem fs, HoodieLogFile logFile, Schema readerSchema, int bufferSize, + boolean readBlockLazily, boolean reverseReader) throws IOException { + this.inputStream = fs.open(logFile.getPath(), bufferSize); + this.logFile = logFile; + this.readerSchema = readerSchema; + this.readBlockLazily = readBlockLazily; + this.reverseReader = reverseReader; + if(this.reverseReader) { + this.reverseLogFilePosition = this.lastReverseLogFilePosition = fs.getFileStatus(logFile.getPath()).getLen(); + } + addShutDownHook(); + } + + HoodieLogFileReader(FileSystem fs, HoodieLogFile logFile, Schema readerSchema, + boolean readBlockLazily, boolean reverseReader) throws IOException { + this(fs, logFile, readerSchema, DEFAULT_BUFFER_SIZE, readBlockLazily, reverseReader); + } + + HoodieLogFileReader(FileSystem fs, HoodieLogFile logFile, Schema readerSchema) throws IOException { + this(fs, logFile, readerSchema, DEFAULT_BUFFER_SIZE, false, false); + } + + @Override + public HoodieLogFile getLogFile() { + return logFile; + } + + /** + * Close the inputstream when the JVM exits + */ + private void addShutDownHook() { + Runtime.getRuntime().addShutdownHook(new Thread() { + public void run() { + try { + inputStream.close(); + } catch (Exception e) { + log.warn("unable to close input stream for log file " + logFile, e); + // fail silently for any sort of exception + } + } + }); + } + + // TODO : convert content and block length to long by using ByteBuffer, raw byte [] allows for max of Integer size + private HoodieLogBlock readBlock() throws IOException { + + int blocksize = -1; + int type = -1; + HoodieLogBlockType blockType = null; + Map header = null; + + try { + + if (isOldMagic()) { + // 1 Read the block type for a log block + type = inputStream.readInt(); + + Preconditions.checkArgument(type < HoodieLogBlockType.values().length, + "Invalid block byte type found " + type); + blockType = HoodieLogBlockType.values()[type]; + + // 2 Read the total size of the block + blocksize = inputStream.readInt(); + } else { + // 1 Read the total size of the block + blocksize = (int) inputStream.readLong(); + } + + } catch (Exception e) { + // An exception reading any of the above indicates a corrupt block + // Create a corrupt block by finding the next OLD_MAGIC marker or EOF + return createCorruptBlock(); + } + + // We may have had a crash which could have written this block partially + // Skip blocksize in the stream and we should either find a sync marker (start of the next block) or EOF + // If we did not find either of it, then this block is a corrupted block. + boolean isCorrupted = isBlockCorrupt(blocksize); + if (isCorrupted) { + return createCorruptBlock(); + } + + // 2. Read the version for this log format + this.nextBlockVersion = readVersion(); + + // 3. Read the block type for a log block + if (nextBlockVersion.getVersion() != HoodieLogFormatVersion.DEFAULT_VERSION) { + type = inputStream.readInt(); + + Preconditions.checkArgument(type < HoodieLogBlockType.values().length, + "Invalid block byte type found " + type); + blockType = HoodieLogBlockType.values()[type]; + } + + // 4. Read the header for a log block, if present + if (nextBlockVersion.hasHeader()) { + header = HoodieLogBlock.getLogMetadata(inputStream); + } + + int contentLength = blocksize; + // 5. Read the content length for the content + if (nextBlockVersion.getVersion() != HoodieLogFormatVersion.DEFAULT_VERSION) { + contentLength = (int) inputStream.readLong(); + } + + // 6. Read the content or skip content based on IO vs Memory trade-off by client + // TODO - have a max block size and reuse this buffer in the ByteBuffer (hard to guess max block size for now) + long contentPosition = inputStream.getPos(); + byte[] content = HoodieLogBlock.readOrSkipContent(inputStream, contentLength, readBlockLazily); + + // 7. Read footer if any + Map footer = null; + if (nextBlockVersion.hasFooter()) { + footer = HoodieLogBlock.getLogMetadata(inputStream); + } + + // 8. Read log block length, if present. This acts as a reverse pointer when traversing a log file in reverse + long logBlockLength = 0; + if (nextBlockVersion.hasLogBlockLength()) { + logBlockLength = inputStream.readLong(); + } + + // 9. Read the log block end position in the log file + long blockEndPos = inputStream.getPos(); + + switch (blockType) { + // based on type read the block + case AVRO_DATA_BLOCK: + if (nextBlockVersion.getVersion() == HoodieLogFormatVersion.DEFAULT_VERSION) { + return HoodieAvroDataBlock.getBlock(content, readerSchema); + } else { + return HoodieAvroDataBlock.getBlock(logFile, inputStream, Optional.ofNullable(content), readBlockLazily, + contentPosition, contentLength, blockEndPos, readerSchema, header, footer); + } + case DELETE_BLOCK: + return HoodieDeleteBlock.getBlock(logFile, inputStream, Optional.ofNullable(content), readBlockLazily, + contentPosition, contentLength, blockEndPos, header, footer); + case COMMAND_BLOCK: + return HoodieCommandBlock.getBlock(logFile, inputStream, Optional.ofNullable(content), readBlockLazily, + contentPosition, contentLength, blockEndPos, header, footer); + default: + throw new HoodieNotSupportedException("Unsupported Block " + blockType); + } + } + + private HoodieLogBlock createCorruptBlock() throws IOException { + log.info("Log " + logFile + " has a corrupted block at " + inputStream.getPos()); + long currentPos = inputStream.getPos(); + long nextBlockOffset = scanForNextAvailableBlockOffset(); + // Rewind to the initial start and read corrupted bytes till the nextBlockOffset + inputStream.seek(currentPos); + log.info("Next available block in " + logFile + " starts at " + nextBlockOffset); + int corruptedBlockSize = (int) (nextBlockOffset - currentPos); + long contentPosition = inputStream.getPos(); + byte[] corruptedBytes = HoodieLogBlock.readOrSkipContent(inputStream, corruptedBlockSize, readBlockLazily); + return HoodieCorruptBlock.getBlock(logFile, inputStream, Optional.ofNullable(corruptedBytes), readBlockLazily, + contentPosition, corruptedBlockSize, corruptedBlockSize, new HashMap<>(), new HashMap<>()); + } + + private boolean isBlockCorrupt(int blocksize) throws IOException { + long currentPos = inputStream.getPos(); + try { + inputStream.seek(currentPos + blocksize); + } catch (EOFException e) { + // this is corrupt + return true; + } + + try { + readMagic(); + // all good - either we found the sync marker or EOF. Reset position and continue + return false; + } catch (CorruptedLogFileException e) { + // This is a corrupted block + return true; + } finally { + inputStream.seek(currentPos); + } + } + + private long scanForNextAvailableBlockOffset() throws IOException { + while (true) { + long currentPos = inputStream.getPos(); + try { + boolean isEOF = readMagic(); + return isEOF ? inputStream.getPos() : currentPos; + } catch (CorruptedLogFileException e) { + // No luck - advance and try again + inputStream.seek(currentPos + 1); + } + } + } + + @Override + public void close() throws IOException { + this.inputStream.close(); + } + + @Override + /** + * hasNext is not idempotent. TODO - Fix this. It is okay for now - PR + */ + public boolean hasNext() { + try { + boolean isEOF = readMagic(); + if (isEOF) { + return false; + } + this.nextBlock = readBlock(); + return nextBlock != null; + } catch (IOException e) { + throw new HoodieIOException("IOException when reading logfile " + logFile, e); + } + } + + /** + * Read log format version from log file, if present + * For old log files written with Magic header OLD_MAGIC and without version, return DEFAULT_VERSION + * + * @throws IOException + */ + private LogFormatVersion readVersion() throws IOException { + // If not old log file format (written with Magic header OLD_MAGIC), then read log version + if (Arrays.equals(oldMagicBuffer, HoodieLogFormat.OLD_MAGIC)) { + Arrays.fill(oldMagicBuffer, (byte) 0); + return new HoodieLogFormatVersion(HoodieLogFormatVersion.DEFAULT_VERSION); + } + return new HoodieLogFormatVersion(inputStream.readInt()); + } + + private boolean isOldMagic() { + return Arrays.equals(oldMagicBuffer, HoodieLogFormat.OLD_MAGIC); + } + + + private boolean readMagic() throws IOException { + try { + long pos = inputStream.getPos(); + // 1. Read magic header from the start of the block + inputStream.readFully(magicBuffer, 0, 6); + if (!Arrays.equals(magicBuffer, HoodieLogFormat.MAGIC)) { + inputStream.seek(pos); + // 1. Read old magic header from the start of the block + // (for backwards compatibility of older log files written without log version) + inputStream.readFully(oldMagicBuffer, 0, 4); + if (!Arrays.equals(oldMagicBuffer, HoodieLogFormat.OLD_MAGIC)) { + throw new CorruptedLogFileException( + logFile + "could not be read. Did not find the magic bytes at the start of the block"); + } + } + return false; + } catch (EOFException e) { + // We have reached the EOF + return true; + } + } + + @Override + public HoodieLogBlock next() { + if (nextBlock == null) { + // may be hasNext is not called + hasNext(); + } + return nextBlock; + } + + /** + * hasPrev is not idempotent + * + * @return + */ + public boolean hasPrev() { + try { + if(!this.reverseReader) { + throw new HoodieNotSupportedException("Reverse log reader has not been enabled"); + } + reverseLogFilePosition = lastReverseLogFilePosition; + reverseLogFilePosition -= Long.BYTES; + lastReverseLogFilePosition = reverseLogFilePosition; + inputStream.seek(reverseLogFilePosition); + } catch (Exception e) { + // Either reached EOF while reading backwards or an exception + return false; + } + return true; + } + + /** + * This is a reverse iterator + * Note: At any point, an instance of HoodieLogFileReader should either iterate reverse (prev) + * or forward (next). Doing both in the same instance is not supported + * WARNING : Every call to prev() should be preceded with hasPrev() + * + * @return + * @throws IOException + */ + public HoodieLogBlock prev() throws IOException { + + if(!this.reverseReader) { + throw new HoodieNotSupportedException("Reverse log reader has not been enabled"); + } + long blockSize = inputStream.readLong(); + long blockEndPos = inputStream.getPos(); + // blocksize should read everything about a block including the length as well + try { + inputStream.seek(reverseLogFilePosition - blockSize); + } catch (Exception e) { + // this could be a corrupt block + inputStream.seek(blockEndPos); + throw new CorruptedLogFileException("Found possible corrupted block, cannot read log file in reverse, " + + "fallback to forward reading of logfile"); + } + boolean hasNext = hasNext(); + reverseLogFilePosition -= blockSize; + lastReverseLogFilePosition = reverseLogFilePosition; + return this.nextBlock; + } + + /** + * Reverse pointer, does not read the block. Return the current position of the log file (in reverse) + * If the pointer (inputstream) is moved in any way, it is the job of the client of this class to + * seek/reset it back to the file position returned from the method to expect correct results + * + * @return + * @throws IOException + */ + public long moveToPrev() throws IOException { + + if(!this.reverseReader) { + throw new HoodieNotSupportedException("Reverse log reader has not been enabled"); + } + inputStream.seek(lastReverseLogFilePosition); + long blockSize = inputStream.readLong(); + // blocksize should be everything about a block including the length as well + inputStream.seek(reverseLogFilePosition - blockSize); + reverseLogFilePosition -= blockSize; + lastReverseLogFilePosition = reverseLogFilePosition; + return reverseLogFilePosition; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported for HoodieLogFileReader"); + } +} diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormat.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormat.java index 58734b165..d5deb9d03 100644 --- a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormat.java +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormat.java @@ -19,17 +19,18 @@ package com.uber.hoodie.common.table.log; import com.uber.hoodie.common.model.HoodieLogFile; import com.uber.hoodie.common.table.log.block.HoodieLogBlock; import com.uber.hoodie.common.util.FSUtils; -import java.io.Closeable; -import java.io.IOException; -import java.util.Iterator; import org.apache.avro.Schema; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; +import java.io.Closeable; +import java.io.IOException; +import java.util.Iterator; + /** - * File Format for Hoodie Log Files. The File Format consists of blocks each seperated with a MAGIC + * File Format for Hoodie Log Files. The File Format consists of blocks each seperated with a OLD_MAGIC * sync marker. A Block can either be a Data block, Command block or Delete Block. Data Block - * Contains log records serialized as Avro Binary Format Command Block - Specific commands like * RoLLBACK_PREVIOUS-BLOCK - Tombstone for the previously written block Delete Block - List of keys @@ -42,7 +43,21 @@ public interface HoodieLogFormat { * this file specific (generate a random 4 byte magic and stick it in the file header), but this I * think is suffice for now - PR */ - byte[] MAGIC = new byte[]{'H', 'U', 'D', 'I'}; + byte[] OLD_MAGIC = new byte[]{'H', 'U', 'D', 'I'}; + + /** + * Magic 6 bytes we put at the start of every block in the log file. + * This is added to maintain backwards compatiblity due to lack of log format/block + * version in older log files. All new log block will now write this OLD_MAGIC value + */ + byte[] MAGIC = new byte[]{'#', 'H', 'U', 'D', 'I', '#'}; + + /** + * The current version of the log format. Anytime the log format changes + * this version needs to be bumped and corresponding changes need to be made to + * {@link HoodieLogFormatVersion} + */ + int currentVersion = 1; /** * Writer interface to allow appending block to this file format @@ -196,9 +211,8 @@ public interface HoodieLogFormat { return new WriterBuilder(); } - static HoodieLogFormat.Reader newReader(FileSystem fs, HoodieLogFile logFile, Schema readerSchema, - boolean readMetadata) + static HoodieLogFormat.Reader newReader(FileSystem fs, HoodieLogFile logFile, Schema readerSchema) throws IOException { - return new HoodieLogFormatReader(fs, logFile, readerSchema, readMetadata); + return new HoodieLogFileReader(fs, logFile, readerSchema, false, false); } } diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormatReader.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormatReader.java index 4168e27cf..f62c9f0e5 100644 --- a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormatReader.java +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormatReader.java @@ -16,192 +16,85 @@ package com.uber.hoodie.common.table.log; -import com.google.common.base.Preconditions; import com.uber.hoodie.common.model.HoodieLogFile; -import com.uber.hoodie.common.table.log.block.HoodieAvroDataBlock; -import com.uber.hoodie.common.table.log.block.HoodieCommandBlock; -import com.uber.hoodie.common.table.log.block.HoodieCorruptBlock; -import com.uber.hoodie.common.table.log.block.HoodieDeleteBlock; import com.uber.hoodie.common.table.log.block.HoodieLogBlock; -import com.uber.hoodie.common.table.log.block.HoodieLogBlock.HoodieLogBlockType; -import com.uber.hoodie.exception.CorruptedLogFileException; import com.uber.hoodie.exception.HoodieIOException; -import com.uber.hoodie.exception.HoodieNotSupportedException; -import java.io.EOFException; -import java.io.IOException; -import java.util.Arrays; import org.apache.avro.Schema; -import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileSystem; -import org.apache.log4j.LogManager; -import org.apache.log4j.Logger; -/** - * Scans a log file and provides block level iterator on the log file Loads the entire block - * contents in memory Can emit either a DataBlock, CommandBlock, DeleteBlock or CorruptBlock (if one - * is found) - */ +import java.io.IOException; +import java.util.List; + public class HoodieLogFormatReader implements HoodieLogFormat.Reader { - private static final int DEFAULT_BUFFER_SIZE = 4096; - private final static Logger log = LogManager.getLogger(HoodieLogFormatReader.class); - - private final FSDataInputStream inputStream; - private final HoodieLogFile logFile; - private static final byte[] magicBuffer = new byte[4]; + private final List logFiles; + private HoodieLogFileReader currentReader; + private final FileSystem fs; private final Schema readerSchema; - private HoodieLogBlock nextBlock = null; - private boolean readMetadata = true; + private final boolean readBlocksLazily; + private final boolean reverseLogReader; - HoodieLogFormatReader(FileSystem fs, HoodieLogFile logFile, Schema readerSchema, int bufferSize, - boolean readMetadata) throws IOException { - this.inputStream = fs.open(logFile.getPath(), bufferSize); - this.logFile = logFile; + HoodieLogFormatReader(FileSystem fs, List logFiles, + Schema readerSchema, boolean readBlocksLazily, boolean reverseLogReader) throws IOException { + this.logFiles = logFiles; + this.fs = fs; this.readerSchema = readerSchema; - this.readMetadata = readMetadata; - } - - HoodieLogFormatReader(FileSystem fs, HoodieLogFile logFile, Schema readerSchema, - boolean readMetadata) throws IOException { - this(fs, logFile, readerSchema, DEFAULT_BUFFER_SIZE, readMetadata); - } - - @Override - public HoodieLogFile getLogFile() { - return logFile; - } - - private HoodieLogBlock readBlock() throws IOException { - // 2. Read the block type - int ordinal = inputStream.readInt(); - Preconditions.checkArgument(ordinal < HoodieLogBlockType.values().length, - "Invalid block byte ordinal found " + ordinal); - HoodieLogBlockType blockType = HoodieLogBlockType.values()[ordinal]; - - // 3. Read the size of the block - int blocksize = inputStream.readInt(); - - // We may have had a crash which could have written this block partially - // Skip blocksize in the stream and we should either find a sync marker (start of the next block) or EOF - // If we did not find either of it, then this block is a corrupted block. - boolean isCorrupted = isBlockCorrupt(blocksize); - if (isCorrupted) { - return createCorruptBlock(); - } - - // 4. Read the content - // TODO - have a max block size and reuse this buffer in the ByteBuffer (hard to guess max block size for now) - byte[] content = new byte[blocksize]; - inputStream.readFully(content, 0, blocksize); - - switch (blockType) { - // based on type read the block - case AVRO_DATA_BLOCK: - return HoodieAvroDataBlock.fromBytes(content, readerSchema, readMetadata); - case DELETE_BLOCK: - return HoodieDeleteBlock.fromBytes(content, readMetadata); - case COMMAND_BLOCK: - return HoodieCommandBlock.fromBytes(content, readMetadata); - default: - throw new HoodieNotSupportedException("Unsupported Block " + blockType); + this.readBlocksLazily = readBlocksLazily; + this.reverseLogReader = reverseLogReader; + if(logFiles.size() > 0) { + HoodieLogFile nextLogFile = logFiles.remove(0); + this.currentReader = new HoodieLogFileReader(fs, nextLogFile, readerSchema, readBlocksLazily, + false); } } - private HoodieLogBlock createCorruptBlock() throws IOException { - log.info("Log " + logFile + " has a corrupted block at " + inputStream.getPos()); - long currentPos = inputStream.getPos(); - long nextBlockOffset = scanForNextAvailableBlockOffset(); - // Rewind to the initial start and read corrupted bytes till the nextBlockOffset - inputStream.seek(currentPos); - log.info("Next available block in " + logFile + " starts at " + nextBlockOffset); - int corruptedBlockSize = (int) (nextBlockOffset - currentPos); - byte[] content = new byte[corruptedBlockSize]; - inputStream.readFully(content, 0, corruptedBlockSize); - return HoodieCorruptBlock.fromBytes(content, corruptedBlockSize, true); - } - - private boolean isBlockCorrupt(int blocksize) throws IOException { - long currentPos = inputStream.getPos(); - try { - inputStream.seek(currentPos + blocksize); - } catch (EOFException e) { - // this is corrupt - return true; - } - - try { - readMagic(); - // all good - either we found the sync marker or EOF. Reset position and continue - return false; - } catch (CorruptedLogFileException e) { - // This is a corrupted block - return true; - } finally { - inputStream.seek(currentPos); - } - } - - private long scanForNextAvailableBlockOffset() throws IOException { - while (true) { - long currentPos = inputStream.getPos(); - try { - boolean isEOF = readMagic(); - return isEOF ? inputStream.getPos() : currentPos; - } catch (CorruptedLogFileException e) { - // No luck - advance and try again - inputStream.seek(currentPos + 1); - } - } + HoodieLogFormatReader(FileSystem fs, List logFiles, + Schema readerSchema) throws IOException { + this(fs, logFiles, readerSchema, false, false); } @Override public void close() throws IOException { - this.inputStream.close(); + if (currentReader != null) { + currentReader.close(); + } } @Override - /** - * hasNext is not idempotent. TODO - Fix this. It is okay for now - PR - */ public boolean hasNext() { - try { - boolean isEOF = readMagic(); - if (isEOF) { - return false; - } - this.nextBlock = readBlock(); - return nextBlock != null; - } catch (IOException e) { - throw new HoodieIOException("IOException when reading logfile " + logFile, e); - } - } - private boolean readMagic() throws IOException { - try { - // 1. Read magic header from the start of the block - inputStream.readFully(magicBuffer, 0, 4); - if (!Arrays.equals(magicBuffer, HoodieLogFormat.MAGIC)) { - throw new CorruptedLogFileException( - logFile + "could not be read. Did not find the magic bytes at the start of the block"); - } + if(currentReader == null) { return false; - } catch (EOFException e) { - // We have reached the EOF + } + else if (currentReader.hasNext()) { return true; } + else if (logFiles.size() > 0) { + try { + HoodieLogFile nextLogFile = logFiles.remove(0); + this.currentReader = new HoodieLogFileReader(fs, nextLogFile, readerSchema, readBlocksLazily, + false); + } catch (IOException io) { + throw new HoodieIOException("unable to initialize read with log file ", io); + } + return this.currentReader.hasNext(); + } + return false; } @Override public HoodieLogBlock next() { - if (nextBlock == null) { - // may be hasNext is not called - hasNext(); - } - return nextBlock; + HoodieLogBlock block = currentReader.next(); + return block; + } + + @Override + public HoodieLogFile getLogFile() { + return currentReader.getLogFile(); } @Override public void remove() { - throw new UnsupportedOperationException("Remove not supported for HoodieLogFormatReader"); } -} + +} \ No newline at end of file diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormatWriter.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormatWriter.java index f32629571..9ea4600a5 100644 --- a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormatWriter.java +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/HoodieLogFormatWriter.java @@ -22,7 +22,6 @@ import com.uber.hoodie.common.table.log.HoodieLogFormat.WriterBuilder; import com.uber.hoodie.common.table.log.block.HoodieLogBlock; import com.uber.hoodie.common.util.FSUtils; import com.uber.hoodie.exception.HoodieException; -import java.io.IOException; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; @@ -32,6 +31,8 @@ import org.apache.hadoop.ipc.RemoteException; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; +import java.io.IOException; + /** * HoodieLogFormatWriter can be used to append blocks to a log file Use * HoodieLogFormat.WriterBuilder to construct @@ -117,16 +118,39 @@ public class HoodieLogFormatWriter implements HoodieLogFormat.Writer { @Override public Writer appendBlock(HoodieLogBlock block) throws IOException, InterruptedException { - byte[] content = block.getBytes(); - // 1. write the magic header for the start of the block - this.output.write(HoodieLogFormat.MAGIC); - // 2. Write the block type - this.output.writeInt(block.getBlockType().ordinal()); - // 3. Write the size of the block - this.output.writeInt(content.length); - // 4. Write the contents of the block - this.output.write(content); + // Find current version + LogFormatVersion currentLogFormatVersion = new HoodieLogFormatVersion(HoodieLogFormat.currentVersion); + long currentSize = this.output.size(); + + // 1. Write the magic header for the start of the block + this.output.write(HoodieLogFormat.MAGIC); + + // bytes for header + byte [] headerBytes = HoodieLogBlock.getLogMetadataBytes(block.getLogBlockHeader()); + // content bytes + byte [] content = block.getContentBytes(); + // bytes for footer + byte [] footerBytes = HoodieLogBlock.getLogMetadataBytes(block.getLogBlockFooter()); + + // 2. Write the total size of the block (excluding Magic) + this.output.writeLong(getLogBlockLength(content.length, headerBytes.length, footerBytes.length)); + + // 3. Write the version of this log block + this.output.writeInt(currentLogFormatVersion.getVersion()); + // 4. Write the block type + this.output.writeInt(block.getBlockType().ordinal()); + + // 5. Write the headers for the log block + this.output.write(headerBytes); + // 6. Write the size of the content block + this.output.writeLong(content.length); + // 7. Write the contents of the data block + this.output.write(content); + // 8. Write the footers for the log block + this.output.write(footerBytes); + // 9. Write the total size of the log block (including magic) which is everything written until now (for reverse pointer) + this.output.writeLong(this.output.size() - currentSize); // Flush every block to disk flush(); @@ -134,6 +158,32 @@ public class HoodieLogFormatWriter implements HoodieLogFormat.Writer { return rolloverIfNeeded(); } + /** + * + * This method returns the total LogBlock Length which is the sum of + * 1. Number of bytes to write version + * 2. Number of bytes to write ordinal + * 3. Length of the headers + * 4. Number of bytes used to write content length + * 5. Length of the content + * 6. Length of the footers + * 7. Number of bytes to write totalLogBlockLength + * @param contentLength + * @param headerLength + * @param footerLength + * @return + */ + private int getLogBlockLength(int contentLength, int headerLength, int footerLength) { + return + Integer.BYTES + // Number of bytes to write version + Integer.BYTES + // Number of bytes to write ordinal + headerLength + // Length of the headers + Long.BYTES + // Number of bytes used to write content length + contentLength + // Length of the content + footerLength + // Length of the footers + Long.BYTES; // Number of bytes to write totalLogBlockLength at end of block (for reverse pointer) + } + private Writer rolloverIfNeeded() throws IOException, InterruptedException { // Roll over if the size is past the threshold if (getCurrentSize() > sizeThreshold) { diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/LogFormatVersion.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/LogFormatVersion.java new file mode 100644 index 000000000..8bba078c7 --- /dev/null +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/LogFormatVersion.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016 Uber Technologies, Inc. (hoodie-dev-group@uber.com) + * + * Licensed 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 com.uber.hoodie.common.table.log; + +/** + * A set of feature flags associated with a log format. + * Versions are changed when the log format changes. + * TODO(na) - Implement policies around major/minor versions + */ +abstract class LogFormatVersion { + private final int version; + + LogFormatVersion(int version) { + this.version = version; + } + + public int getVersion() { + return version; + } + + public abstract boolean hasMagicHeader(); + + public abstract boolean hasContent(); + + public abstract boolean hasContentLength(); + + public abstract boolean hasOrdinal(); + + public abstract boolean hasHeader(); + + public abstract boolean hasFooter(); + + public abstract boolean hasLogBlockLength(); +} + +/** + * Implements logic to determine behavior for feature flags for {@link LogFormatVersion} + */ +final class HoodieLogFormatVersion extends LogFormatVersion { + + public final static int DEFAULT_VERSION = 0; + + HoodieLogFormatVersion(int version) { + super(version); + } + @Override + public boolean hasMagicHeader() { + switch (super.getVersion()) { + case DEFAULT_VERSION: + return true; + default: + return true; + } + } + + @Override + public boolean hasContent() { + switch (super.getVersion()) { + case DEFAULT_VERSION: + return true; + default: + return true; + } + } + + @Override + public boolean hasContentLength() { + switch (super.getVersion()) { + case DEFAULT_VERSION: + return true; + default: + return true; + } + } + + @Override + public boolean hasOrdinal() { + switch (super.getVersion()) { + case DEFAULT_VERSION: + return true; + default: + return true; + } + } + + @Override + public boolean hasHeader() { + switch (super.getVersion()) { + case DEFAULT_VERSION: + return false; + default: + return true; + } + } + + @Override + public boolean hasFooter() { + switch (super.getVersion()) { + case DEFAULT_VERSION: + return false; + case 1: + return true; + } + return false; + } + + @Override + public boolean hasLogBlockLength() { + switch (super.getVersion()) { + case DEFAULT_VERSION: + return false; + case 1: + return true; + } + return false; + } +} \ No newline at end of file diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieAvroDataBlock.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieAvroDataBlock.java index ebb72bb4b..94e89e793 100644 --- a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieAvroDataBlock.java +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieAvroDataBlock.java @@ -16,6 +16,8 @@ package com.uber.hoodie.common.table.log.block; +import com.google.common.annotations.VisibleForTesting; +import com.uber.hoodie.common.model.HoodieLogFile; import com.uber.hoodie.common.storage.SizeAwareDataInputStream; import com.uber.hoodie.common.util.HoodieAvroUtils; import com.uber.hoodie.exception.HoodieIOException; @@ -27,43 +29,134 @@ import org.apache.avro.io.Decoder; import org.apache.avro.io.DecoderFactory; import org.apache.avro.io.Encoder; import org.apache.avro.io.EncoderFactory; +import org.apache.hadoop.fs.FSDataInputStream; +import javax.annotation.Nonnull; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; /** * DataBlock contains a list of records serialized using Avro. * The Datablock contains - * 1. Compressed Writer Schema length - * 2. Compressed Writer Schema content - * 3. Total number of records in the block - * 4. Size of a record - * 5. Actual avro serialized content of the record + * 1. Data Block version + * 2. Total number of records in the block + * 3. Size of a record + * 4. Actual avro serialized content of the record */ public class HoodieAvroDataBlock extends HoodieLogBlock { private List records; private Schema schema; - public HoodieAvroDataBlock(List records, Schema schema, Map metadata) { - super(metadata); + public HoodieAvroDataBlock(@Nonnull List records, + @Nonnull Map header, + @Nonnull Map footer) { + super(header, footer, Optional.empty(), Optional.empty(), null, false); this.records = records; - this.schema = schema; + this.schema = Schema.parse(super.getLogBlockHeader().get(HeaderMetadataType.SCHEMA)); } - public HoodieAvroDataBlock(List records, Schema schema) { - this(records, schema, null); + public HoodieAvroDataBlock(@Nonnull List records, + @Nonnull Map header) { + this(records, header, new HashMap<>()); + } + + private HoodieAvroDataBlock(Optional content, @Nonnull FSDataInputStream inputStream, + boolean readBlockLazily, Optional blockContentLocation, + Schema readerSchema, @Nonnull Map headers, + @Nonnull Map footer) { + super(headers, footer, blockContentLocation, content, inputStream, readBlockLazily); + this.schema = readerSchema; + } + + public static HoodieLogBlock getBlock(HoodieLogFile logFile, + FSDataInputStream inputStream, + Optional content, + boolean readBlockLazily, + long position, + long blockSize, + long blockEndpos, + Schema readerSchema, + Map header, + Map footer) { + + return new HoodieAvroDataBlock(content, inputStream, readBlockLazily, + Optional.of(new HoodieLogBlockContentLocation(logFile, position, blockSize, blockEndpos)), + readerSchema, header, footer); + + } + + @Override + public byte[] getContentBytes() throws IOException { + + // In case this method is called before realizing records from content + if (getContent().isPresent()) { + return getContent().get(); + } else if (readBlockLazily && !getContent().isPresent() && records == null) { + // read block lazily + createRecordsFromContentBytes(); + } + + Schema schema = Schema.parse(super.getLogBlockHeader().get(HeaderMetadataType.SCHEMA)); + GenericDatumWriter writer = new GenericDatumWriter<>(schema); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(baos); + + // 1. Write out the log block version + output.writeInt(HoodieLogBlock.version); + + // 2. Write total number of records + output.writeInt(records.size()); + + // 3. Write the records + Iterator itr = records.iterator(); + while (itr.hasNext()) { + IndexedRecord s = itr.next(); + ByteArrayOutputStream temp = new ByteArrayOutputStream(); + Encoder encoder = EncoderFactory.get().binaryEncoder(temp, null); + try { + // Encode the record into bytes + writer.write(s, encoder); + encoder.flush(); + + // Get the size of the bytes + int size = temp.toByteArray().length; + // Write the record size + output.writeInt(size); + // Write the content + output.write(temp.toByteArray()); + itr.remove(); + } catch (IOException e) { + throw new HoodieIOException("IOException converting HoodieAvroDataBlock to bytes", e); + } + } + output.close(); + return baos.toByteArray(); + } + + @Override + public HoodieLogBlockType getBlockType() { + return HoodieLogBlockType.AVRO_DATA_BLOCK; } - //TODO : (na) lazily create IndexedRecords only when required public List getRecords() { + if (records == null) { + try { + // in case records are absent, read content lazily and then convert to IndexedRecords + createRecordsFromContentBytes(); + } catch (IOException io) { + throw new HoodieIOException("Unable to convert content bytes to records", io); + } + } return records; } @@ -71,18 +164,114 @@ public class HoodieAvroDataBlock extends HoodieLogBlock { return schema; } - @Override - public byte[] getBytes() throws IOException { + //TODO (na) - Break down content into smaller chunks of byte [] to be GC as they are used + //TODO (na) - Implement a recordItr instead of recordList + private void createRecordsFromContentBytes() throws IOException { + + if (readBlockLazily && !getContent().isPresent()) { + // read log block contents from disk + inflate(); + } + + SizeAwareDataInputStream dis = + new SizeAwareDataInputStream( + new DataInputStream(new ByteArrayInputStream(getContent().get()))); + + // 1. Read version for this data block + int version = dis.readInt(); + HoodieAvroDataBlockVersion logBlockVersion = new HoodieAvroDataBlockVersion(version); + + // Get schema from the header + Schema writerSchema = new Schema.Parser() + .parse(super.getLogBlockHeader().get(HeaderMetadataType.SCHEMA)); + + // If readerSchema was not present, use writerSchema + if (schema == null) { + schema = writerSchema; + } + + GenericDatumReader reader = new GenericDatumReader<>(writerSchema, schema); + // 2. Get the total records + int totalRecords = 0; + if (logBlockVersion.hasRecordCount()) { + totalRecords = dis.readInt(); + } + List records = new ArrayList<>(totalRecords); + + // 3. Read the content + for (int i = 0; i < totalRecords; i++) { + int recordLength = dis.readInt(); + Decoder decoder = DecoderFactory.get() + .binaryDecoder(getContent().get(), dis.getNumberOfBytesRead(), recordLength, null); + IndexedRecord record = reader.read(null, decoder); + records.add(record); + dis.skipBytes(recordLength); + } + dis.close(); + this.records = records; + // Free up content to be GC'd, deflate + deflate(); + } + + /*****************************************************DEPRECATED METHODS**********************************************/ + + @Deprecated + @VisibleForTesting + /** + * This constructor is retained to provide backwards compatibility to HoodieArchivedLogs + * which were written using HoodieLogFormat V1 + */ + public HoodieAvroDataBlock(List records, Schema schema) { + super(new HashMap<>(), new HashMap<>(), Optional.empty(), Optional.empty(), null, false); + this.records = records; + this.schema = schema; + } + + @Deprecated + /** + * This method is retained to provide backwards compatibility to HoodieArchivedLogs which were written using HoodieLogFormat V1 + */ + public static HoodieLogBlock getBlock(byte[] content, Schema readerSchema) throws IOException { + + SizeAwareDataInputStream dis = new SizeAwareDataInputStream( + new DataInputStream(new ByteArrayInputStream(content))); + + // 1. Read the schema written out + int schemaLength = dis.readInt(); + byte[] compressedSchema = new byte[schemaLength]; + dis.readFully(compressedSchema, 0, schemaLength); + Schema writerSchema = new Schema.Parser().parse(HoodieAvroUtils.decompress(compressedSchema)); + + if (readerSchema == null) { + readerSchema = writerSchema; + } + + GenericDatumReader reader = new GenericDatumReader<>(writerSchema, readerSchema); + // 2. Get the total records + int totalRecords = dis.readInt(); + List records = new ArrayList<>(totalRecords); + + // 3. Read the content + for (int i = 0; i < totalRecords; i++) { + int recordLength = dis.readInt(); + Decoder decoder = DecoderFactory.get() + .binaryDecoder(content, dis.getNumberOfBytesRead(), recordLength, null); + IndexedRecord record = reader.read(null, decoder); + records.add(record); + dis.skipBytes(recordLength); + } + dis.close(); + return new HoodieAvroDataBlock(records, readerSchema); + } + + @Deprecated + @VisibleForTesting + public byte[] getBytes(Schema schema) throws IOException { GenericDatumWriter writer = new GenericDatumWriter<>(schema); ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(baos); - // 1. Write out metadata - if (super.getLogMetadata() != null) { - output.write(HoodieLogBlock.getLogMetadataBytes(super.getLogMetadata())); - } - // 2. Compress and Write schema out byte[] schemaContent = HoodieAvroUtils.compress(schema.toString()); output.writeInt(schemaContent.length); @@ -118,45 +307,4 @@ public class HoodieAvroDataBlock extends HoodieLogBlock { return baos.toByteArray(); } - @Override - public HoodieLogBlockType getBlockType() { - return HoodieLogBlockType.AVRO_DATA_BLOCK; - } - - //TODO (na) - Break down content into smaller chunks of byte [] to be GC as they are used - public static HoodieLogBlock fromBytes(byte[] content, Schema readerSchema, boolean readMetadata) throws IOException { - - SizeAwareDataInputStream dis = new SizeAwareDataInputStream(new DataInputStream(new ByteArrayInputStream(content))); - Map metadata = null; - // 1. Read the metadata written out, if applicable - if (readMetadata) { - metadata = HoodieLogBlock.getLogMetadata(dis); - } - // 1. Read the schema written out - int schemaLength = dis.readInt(); - byte[] compressedSchema = new byte[schemaLength]; - dis.readFully(compressedSchema, 0, schemaLength); - Schema writerSchema = new Schema.Parser().parse(HoodieAvroUtils.decompress(compressedSchema)); - - if (readerSchema == null) { - readerSchema = writerSchema; - } - - //TODO : (na) lazily create IndexedRecords only when required - GenericDatumReader reader = new GenericDatumReader<>(writerSchema, readerSchema); - // 2. Get the total records - int totalRecords = dis.readInt(); - List records = new ArrayList<>(totalRecords); - - // 3. Read the content - for (int i=0;i metadata) { - super(metadata); - this.type = type; + public HoodieCommandBlock(Map header) { + this(Optional.empty(), null, false, Optional.empty(), header, new HashMap<>()); } - public HoodieCommandBlock(HoodieCommandBlockTypeEnum type) { - this(type, null); - } - - @Override - public byte[] getBytes() throws IOException { - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DataOutputStream output = new DataOutputStream(baos); - if (super.getLogMetadata() != null) { - output.write(HoodieLogBlock.getLogMetadataBytes(super.getLogMetadata())); - } - output.writeInt(type.ordinal()); - output.close(); - return baos.toByteArray(); + private HoodieCommandBlock(Optional content, FSDataInputStream inputStream, + boolean readBlockLazily, Optional blockContentLocation, + Map header, Map footer) { + super(header, footer, blockContentLocation, content, inputStream, readBlockLazily); + this.type = HoodieCommandBlockTypeEnum.values()[Integer + .parseInt(header.get(HeaderMetadataType.COMMAND_BLOCK_TYPE))]; } public HoodieCommandBlockTypeEnum getType() { @@ -66,13 +53,23 @@ public class HoodieCommandBlock extends HoodieLogBlock { return HoodieLogBlockType.COMMAND_BLOCK; } - public static HoodieLogBlock fromBytes(byte[] content, boolean readMetadata) throws IOException { - SizeAwareDataInputStream dis = new SizeAwareDataInputStream(new DataInputStream(new ByteArrayInputStream(content))); - Map metadata = null; - if (readMetadata) { - metadata = HoodieLogBlock.getLogMetadata(dis); - } - int ordinal = dis.readInt(); - return new HoodieCommandBlock(HoodieCommandBlockTypeEnum.values()[ordinal], metadata); + @Override + public byte[] getContentBytes() { + return new byte[0]; + } + + public static HoodieLogBlock getBlock(HoodieLogFile logFile, + FSDataInputStream inputStream, + Optional content, + boolean readBlockLazily, + long position, + long blockSize, + long blockEndpos, + Map header, + Map footer) { + + return new HoodieCommandBlock(content, inputStream, readBlockLazily, + Optional.of(new HoodieLogBlockContentLocation(logFile, position, blockSize, blockEndpos)), + header, footer); } } diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieCorruptBlock.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieCorruptBlock.java index 5819c99e1..c75c8ea62 100644 --- a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieCorruptBlock.java +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieCorruptBlock.java @@ -16,14 +16,12 @@ package com.uber.hoodie.common.table.log.block; -import com.uber.hoodie.common.storage.SizeAwareDataInputStream; +import com.uber.hoodie.common.model.HoodieLogFile; +import org.apache.hadoop.fs.FSDataInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.IOException; import java.util.Map; +import java.util.Optional; /** * Corrupt block is emitted whenever the scanner finds the length of the block written at the @@ -31,26 +29,20 @@ import java.util.Map; */ public class HoodieCorruptBlock extends HoodieLogBlock { - private final byte[] corruptedBytes; - - private HoodieCorruptBlock(byte[] corruptedBytes, Map metadata) { - super(metadata); - this.corruptedBytes = corruptedBytes; - } - - private HoodieCorruptBlock(byte[] corruptedBytes) { - this(corruptedBytes, null); + private HoodieCorruptBlock(Optional corruptedBytes, FSDataInputStream inputStream, + boolean readBlockLazily, Optional blockContentLocation, + Map header, Map footer) { + super(header, footer, blockContentLocation, corruptedBytes, inputStream, readBlockLazily); } @Override - public byte[] getBytes() throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DataOutputStream output = new DataOutputStream(baos); - if (super.getLogMetadata() != null) { - output.write(HoodieLogBlock.getLogMetadataBytes(super.getLogMetadata())); + public byte[] getContentBytes() throws IOException { + + if (!getContent().isPresent() && readBlockLazily) { + // read content from disk + inflate(); } - output.write(corruptedBytes); - return baos.toByteArray(); + return getContent().get(); } @Override @@ -58,26 +50,17 @@ public class HoodieCorruptBlock extends HoodieLogBlock { return HoodieLogBlockType.CORRUPT_BLOCK; } - public byte[] getCorruptedBytes() { - return corruptedBytes; - } + public static HoodieLogBlock getBlock(HoodieLogFile logFile, + FSDataInputStream inputStream, + Optional corruptedBytes, + boolean readBlockLazily, + long position, + long blockSize, + long blockEndPos, + Map header, + Map footer) throws IOException { - public static HoodieLogBlock fromBytes(byte[] content, int blockSize, boolean readMetadata) - throws IOException { - SizeAwareDataInputStream dis = new SizeAwareDataInputStream(new DataInputStream(new ByteArrayInputStream(content))); - Map metadata = null; - int bytesRemaining = blockSize; - if (readMetadata) { - try { //attempt to read metadata - metadata = HoodieLogBlock.getLogMetadata(dis); - bytesRemaining = blockSize - HoodieLogBlock.getLogMetadataBytes(metadata).length; - } catch (IOException e) { - // unable to read metadata, possibly corrupted - metadata = null; - } - } - byte[] corruptedBytes = new byte[bytesRemaining]; - dis.readFully(corruptedBytes); - return new HoodieCorruptBlock(corruptedBytes, metadata); + return new HoodieCorruptBlock(corruptedBytes, inputStream, readBlockLazily, + Optional.of(new HoodieLogBlockContentLocation(logFile, position, blockSize, blockEndPos)), header, footer); } } diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieDeleteBlock.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieDeleteBlock.java index 3751124fe..4de25b5da 100644 --- a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieDeleteBlock.java +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieDeleteBlock.java @@ -16,48 +16,82 @@ package com.uber.hoodie.common.table.log.block; +import com.uber.hoodie.common.model.HoodieLogFile; +import com.uber.hoodie.common.storage.SizeAwareDataInputStream; +import com.uber.hoodie.exception.HoodieIOException; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.fs.FSDataInputStream; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.Charset; +import java.util.HashMap; import java.util.Map; - -import com.uber.hoodie.common.storage.SizeAwareDataInputStream; -import org.apache.commons.lang3.StringUtils; +import java.util.Optional; /** * Delete block contains a list of keys to be deleted from scanning the blocks so far */ public class HoodieDeleteBlock extends HoodieLogBlock { - private final String[] keysToDelete; + private String[] keysToDelete; - public HoodieDeleteBlock(String[] keysToDelete, Map metadata) { - super(metadata); + public HoodieDeleteBlock(String[] keysToDelete, + Map header) { + this(Optional.empty(), null, false, Optional.empty(), header, new HashMap<>()); this.keysToDelete = keysToDelete; } - public HoodieDeleteBlock(String[] keysToDelete) { - this(keysToDelete, null); + + private HoodieDeleteBlock(Optional content, FSDataInputStream inputStream, + boolean readBlockLazily, Optional blockContentLocation, + Map header, Map footer) { + super(header, footer, blockContentLocation, content, inputStream, readBlockLazily); } @Override - public byte[] getBytes() throws IOException { + public byte[] getContentBytes() throws IOException { + + // In case this method is called before realizing keys from content + if (getContent().isPresent()) { + return getContent().get(); + } else if (readBlockLazily && !getContent().isPresent() && keysToDelete == null) { + // read block lazily + getKeysToDelete(); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(baos); - if (super.getLogMetadata() != null) { - output.write(HoodieLogBlock.getLogMetadataBytes(super.getLogMetadata())); - } - byte[] bytesToWrite = StringUtils.join(keysToDelete, ',').getBytes(Charset.forName("utf-8")); + byte[] bytesToWrite = StringUtils.join(getKeysToDelete(), ',').getBytes(Charset.forName("utf-8")); + output.writeInt(HoodieLogBlock.version); output.writeInt(bytesToWrite.length); output.write(bytesToWrite); return baos.toByteArray(); } public String[] getKeysToDelete() { - return keysToDelete; + try { + if (keysToDelete == null) { + if (!getContent().isPresent() && readBlockLazily) { + // read content from disk + inflate(); + } + SizeAwareDataInputStream dis = + new SizeAwareDataInputStream(new DataInputStream(new ByteArrayInputStream(getContent().get()))); + int version = dis.readInt(); + int dataLength = dis.readInt(); + byte[] data = new byte[dataLength]; + dis.readFully(data); + this.keysToDelete = new String(data).split(","); + deflate(); + } + return keysToDelete; + } catch (IOException io) { + throw new HoodieIOException("Unable to generate keys to delete from block content", io); + } } @Override @@ -65,15 +99,17 @@ public class HoodieDeleteBlock extends HoodieLogBlock { return HoodieLogBlockType.DELETE_BLOCK; } - public static HoodieLogBlock fromBytes(byte[] content, boolean readMetadata) throws IOException { - SizeAwareDataInputStream dis = new SizeAwareDataInputStream(new DataInputStream(new ByteArrayInputStream(content))); - Map metadata = null; - if (readMetadata) { - metadata = HoodieLogBlock.getLogMetadata(dis); - } - int dataLength = dis.readInt(); - byte[] data = new byte[dataLength]; - dis.readFully(data); - return new HoodieDeleteBlock(new String(data).split(","), metadata); + public static HoodieLogBlock getBlock(HoodieLogFile logFile, + FSDataInputStream inputStream, + Optional content, + boolean readBlockLazily, + long position, + long blockSize, + long blockEndPos, + Map header, + Map footer) throws IOException { + + return new HoodieDeleteBlock(content, inputStream, readBlockLazily, + Optional.of(new HoodieLogBlockContentLocation(logFile, position, blockSize, blockEndPos)), header, footer); } } diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieLogBlock.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieLogBlock.java index d21332f2f..e7735b0db 100644 --- a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieLogBlock.java +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/HoodieLogBlock.java @@ -17,21 +17,66 @@ package com.uber.hoodie.common.table.log.block; import com.google.common.collect.Maps; -import com.uber.hoodie.common.storage.SizeAwareDataInputStream; +import com.uber.hoodie.common.model.HoodieLogFile; import com.uber.hoodie.exception.HoodieException; +import com.uber.hoodie.exception.HoodieIOException; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.util.Map; +import java.util.Optional; +import javax.annotation.Nonnull; +import org.apache.hadoop.fs.FSDataInputStream; /** * Abstract class defining a block in HoodieLogFile */ public abstract class HoodieLogBlock { - public byte[] getBytes() throws IOException { + /** + * The current version of the log block. Anytime the logBlock format changes + * this version needs to be bumped and corresponding changes need to be made to + * {@link HoodieLogBlockVersion} + * TODO : Change this to a class, something like HoodieLogBlockVersionV1/V2 and implement/override operations there + */ + public static int version = 1; + // Header for each log block + private final Map logBlockHeader; + // Footer for each log block + private final Map logBlockFooter; + // Location of a log block on disk + private final Optional blockContentLocation; + // data for a specific block + private Optional content; + // TODO : change this to just InputStream so this works for any FileSystem + // create handlers to return specific type of inputstream based on FS + // input stream corresponding to the log file where this logBlock belongs + protected FSDataInputStream inputStream; + // Toggle flag, whether to read blocks lazily (I/O intensive) or not (Memory intensive) + protected boolean readBlockLazily; + + public HoodieLogBlock(@Nonnull Map logBlockHeader, + @Nonnull Map logBlockFooter, + @Nonnull Optional blockContentLocation, + @Nonnull Optional content, + FSDataInputStream inputStream, + boolean readBlockLazily) { + this.logBlockHeader = logBlockHeader; + this.logBlockFooter = logBlockFooter; + this.blockContentLocation = blockContentLocation; + this.content = content; + this.inputStream = inputStream; + this.readBlockLazily = readBlockLazily; + } + + // Return the bytes representation of the data belonging to a LogBlock + public byte[] getContentBytes() throws IOException { + throw new HoodieException("No implementation was provided"); + } + + public byte [] getMagic() { throw new HoodieException("No implementation was provided"); } @@ -39,8 +84,25 @@ public abstract class HoodieLogBlock { throw new HoodieException("No implementation was provided"); } - //log metadata for each log block - private Map logMetadata; + public long getLogBlockLength() { + throw new HoodieException("No implementation was provided"); + } + + public Optional getBlockContentLocation() { + return this.blockContentLocation; + } + + public Map getLogBlockHeader() { + return logBlockHeader; + } + + public Map getLogBlockFooter() { + return logBlockFooter; + } + + public Optional getContent() { + return content; + } /** * Type of the log block WARNING: This enum is serialized as the ordinal. Only add new enums at @@ -54,32 +116,71 @@ public abstract class HoodieLogBlock { } /** - * Metadata abstraction for a HoodieLogBlock WARNING : This enum is serialized as the ordinal. + * Log Metadata headers abstraction for a HoodieLogBlock WARNING : This enum is serialized as the ordinal. * Only add new enums at the end. */ - public enum LogMetadataType { + public enum HeaderMetadataType { INSTANT_TIME, - TARGET_INSTANT_TIME + TARGET_INSTANT_TIME, + SCHEMA, + COMMAND_BLOCK_TYPE } - public HoodieLogBlock(Map logMetadata) { - this.logMetadata = logMetadata; + /** + * Log Metadata footers abstraction for a HoodieLogBlock WARNING : This enum is serialized as the ordinal. + * Only add new enums at the end. + */ + public enum FooterMetadataType { } - public Map getLogMetadata() { - return logMetadata; + /** + * This class is used to store the Location of the Content of a Log Block. It's used when a client chooses for a + * IO intensive CompactedScanner, the location helps to lazily read contents from the log file + */ + public static final class HoodieLogBlockContentLocation { + // The logFile that contains this block + private final HoodieLogFile logFile; + // The filePosition in the logFile for the contents of this block + private final long contentPositionInLogFile; + // The number of bytes / size of the contents of this block + private final long blockSize; + // The final position where the complete block ends + private final long blockEndPos; + + HoodieLogBlockContentLocation(HoodieLogFile logFile, long contentPositionInLogFile, long blockSize, long blockEndPos) { + this.logFile = logFile; + this.contentPositionInLogFile = contentPositionInLogFile; + this.blockSize = blockSize; + this.blockEndPos = blockEndPos; + } + + public HoodieLogFile getLogFile() { + return logFile; + } + + public long getContentPositionInLogFile() { + return contentPositionInLogFile; + } + + public long getBlockSize() { + return blockSize; + } + + public long getBlockEndPos() { + return blockEndPos; + } } /** * Convert log metadata to bytes 1. Write size of metadata 2. Write enum ordinal 3. Write actual * bytes */ - public static byte[] getLogMetadataBytes(Map metadata) + public static byte[] getLogMetadataBytes(Map metadata) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(baos); output.writeInt(metadata.size()); - for (Map.Entry entry : metadata.entrySet()) { + for (Map.Entry entry : metadata.entrySet()) { output.writeInt(entry.getKey().ordinal()); byte[] bytes = entry.getValue().getBytes(); output.writeInt(bytes.length); @@ -91,10 +192,10 @@ public abstract class HoodieLogBlock { /** * Convert bytes to LogMetadata, follow the same order as {@link HoodieLogBlock#getLogMetadataBytes} */ - public static Map getLogMetadata(SizeAwareDataInputStream dis) + public static Map getLogMetadata(DataInputStream dis) throws IOException { - Map metadata = Maps.newHashMap(); + Map metadata = Maps.newHashMap(); // 1. Read the metadata written out int metadataCount = dis.readInt(); try { @@ -103,7 +204,7 @@ public abstract class HoodieLogBlock { int metadataEntrySize = dis.readInt(); byte[] metadataEntry = new byte[metadataEntrySize]; dis.readFully(metadataEntry, 0, metadataEntrySize); - metadata.put(LogMetadataType.values()[metadataEntryIndex], new String(metadataEntry)); + metadata.put(HeaderMetadataType.values()[metadataEntryIndex], new String(metadataEntry)); metadataCount--; } return metadata; @@ -111,4 +212,60 @@ public abstract class HoodieLogBlock { throw new IOException("Could not read metadata fields ", eof); } } + + /** + * Read or Skip block content of a log block in the log file. Depends on lazy reading enabled in + * {@link com.uber.hoodie.common.table.log.HoodieCompactedLogRecordScanner} + * + * @param inputStream + * @param contentLength + * @param readBlockLazily + * @return + * @throws IOException + */ + public static byte [] readOrSkipContent(FSDataInputStream inputStream, + Integer contentLength, boolean readBlockLazily) throws IOException { + byte [] content = null; + if (!readBlockLazily) { + // Read the contents in memory + content = new byte[contentLength]; + inputStream.readFully(content, 0, contentLength); + } else { + // Seek to the end of the content block + inputStream.seek(inputStream.getPos() + contentLength); + } + return content; + } + + /** + * When lazyReading of blocks is turned on, inflate the content of a log block from disk + * @throws IOException + */ + protected void inflate() throws IOException { + + try { + content = Optional.of(new byte[(int) this.getBlockContentLocation().get().getBlockSize()]); + inputStream.seek(this.getBlockContentLocation().get().getContentPositionInLogFile()); + inputStream.readFully(content.get(), 0, content.get().length); + inputStream.seek(this.getBlockContentLocation().get().getBlockEndPos()); + } catch(IOException e) { + try { + // TODO : fs.open() and return inputstream again, need to pass FS configuration + // because the inputstream might close/timeout for large number of log blocks to be merged + inflate(); + } catch(IOException io) { + throw new HoodieIOException("unable to lazily read log block from disk", io); + } + } + } + + /** + * After the content bytes is converted into the required DataStructure by a logBlock, deflate the content + * to release byte [] and relieve memory pressure when GC kicks in. + * NOTE: This still leaves the heap fragmented + */ + protected void deflate() { + content = Optional.empty(); + } + } diff --git a/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/LogBlockVersion.java b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/LogBlockVersion.java new file mode 100644 index 000000000..1a9844209 --- /dev/null +++ b/hoodie-common/src/main/java/com/uber/hoodie/common/table/log/block/LogBlockVersion.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016 Uber Technologies, Inc. (hoodie-dev-group@uber.com) + * + * Licensed 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 com.uber.hoodie.common.table.log.block; + +import static com.uber.hoodie.common.table.log.block.HoodieLogBlock.version; + +abstract class HoodieLogBlockVersion { + + private final int currentVersion; + + public final static int DEFAULT_VERSION = 0; + + HoodieLogBlockVersion(int version) { + this.currentVersion = version; + } + + int getVersion() { + return currentVersion; + } +} + +/** + * A set of feature flags associated with a data log block format. + * Versions are changed when the log block format changes. + * TODO(na) - Implement policies around major/minor versions + */ +final class HoodieAvroDataBlockVersion extends HoodieLogBlockVersion { + + HoodieAvroDataBlockVersion(int version) { + super(version); + } + + public boolean hasRecordCount() { + switch (super.getVersion()) { + case DEFAULT_VERSION: + return true; + default: + return true; + } + } +} + +/** + * A set of feature flags associated with a command log block format. + * Versions are changed when the log block format changes. + * TODO(na) - Implement policies around major/minor versions + */ +final class HoodieCommandBlockVersion extends HoodieLogBlockVersion { + + HoodieCommandBlockVersion(int version) { + super(version); + } +} + +/** + * A set of feature flags associated with a delete log block format. + * Versions are changed when the log block format changes. + * TODO(na) - Implement policies around major/minor versions + */ +final class HoodieDeleteBlockVersion extends HoodieLogBlockVersion { + + HoodieDeleteBlockVersion(int version) { + super(version); + } +} \ No newline at end of file diff --git a/hoodie-common/src/test/java/com/uber/hoodie/common/model/HoodieTestUtils.java b/hoodie-common/src/test/java/com/uber/hoodie/common/model/HoodieTestUtils.java index db0e87d6f..4db0a58b7 100644 --- a/hoodie-common/src/test/java/com/uber/hoodie/common/model/HoodieTestUtils.java +++ b/hoodie-common/src/test/java/com/uber/hoodie/common/model/HoodieTestUtils.java @@ -291,8 +291,9 @@ public class HoodieTestUtils { .overBaseCommit(location.getCommitTime()) .withFs(fs).build(); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, location.getCommitTime()); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, location.getCommitTime()); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); logWriter.appendBlock(new HoodieAvroDataBlock(s.getValue().stream().map(r -> { try { GenericRecord val = (GenericRecord) r.getData().getInsertValue(schema).get(); @@ -304,7 +305,7 @@ public class HoodieTestUtils { } catch (IOException e) { return null; } - }).collect(Collectors.toList()), schema, metadata)); + }).collect(Collectors.toList()), header)); logWriter.close(); } catch (Exception e) { fail(e.toString()); diff --git a/hoodie-common/src/test/java/com/uber/hoodie/common/table/log/HoodieLogFormatTest.java b/hoodie-common/src/test/java/com/uber/hoodie/common/table/log/HoodieLogFormatTest.java index e57c7ebe7..0acd77fb7 100644 --- a/hoodie-common/src/test/java/com/uber/hoodie/common/table/log/HoodieLogFormatTest.java +++ b/hoodie-common/src/test/java/com/uber/hoodie/common/table/log/HoodieLogFormatTest.java @@ -27,7 +27,6 @@ import com.uber.hoodie.common.table.log.HoodieLogFormat.Reader; import com.uber.hoodie.common.table.log.HoodieLogFormat.Writer; import com.uber.hoodie.common.table.log.block.HoodieAvroDataBlock; import com.uber.hoodie.common.table.log.block.HoodieCommandBlock; -import com.uber.hoodie.common.table.log.block.HoodieCommandBlock.HoodieCommandBlockTypeEnum; import com.uber.hoodie.common.table.log.block.HoodieCorruptBlock; import com.uber.hoodie.common.table.log.block.HoodieDeleteBlock; import com.uber.hoodie.common.table.log.block.HoodieLogBlock; @@ -35,7 +34,7 @@ import com.uber.hoodie.common.table.log.block.HoodieLogBlock.HoodieLogBlockType; import com.uber.hoodie.common.util.FSUtils; import com.uber.hoodie.common.util.HoodieAvroUtils; import com.uber.hoodie.common.util.SchemaTestUtil; -import com.uber.hoodie.common.util.collection.DiskBasedMap; +import com.uber.hoodie.exception.CorruptedLogFileException; import org.apache.avro.Schema; import org.apache.avro.generic.GenericRecord; import org.apache.avro.generic.IndexedRecord; @@ -49,12 +48,15 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -68,12 +70,26 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @SuppressWarnings("Duplicates") +@RunWith(Parameterized.class) public class HoodieLogFormatTest { private FileSystem fs; private Path partitionPath; private static String basePath; + private Boolean readBlocksLazily = true; + + public HoodieLogFormatTest(Boolean readBlocksLazily) { + this.readBlocksLazily = readBlocksLazily; + } + + @Parameterized.Parameters(name = "LogBlockReadMode") + public static Collection data() { + return Arrays.asList(new Boolean[][]{ + {true},{false} + }); + } + @BeforeClass public static void setUpClass() throws IOException, InterruptedException { // Append is not supported in LocalFileSystem. HDFS needs to be setup. @@ -119,10 +135,10 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); List records = SchemaTestUtil.generateTestRecords(0, 100); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); long size = writer.getCurrentSize(); assertTrue("We just wrote a block - size should be > 0", size > 0); @@ -138,10 +154,10 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); List records = SchemaTestUtil.generateTestRecords(0, 100); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); // Write out a block writer = writer.appendBlock(dataBlock); // Get the size of the block @@ -153,8 +169,8 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).withSizeThreshold(size - 1).build(); records = SchemaTestUtil.generateTestRecords(0, 100); - dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); assertEquals("This should be a new log file and hence size should be 0", 0, writer.getCurrentSize()); @@ -168,10 +184,10 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); List records = SchemaTestUtil.generateTestRecords(0, 100); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); long size1 = writer.getCurrentSize(); writer.close(); @@ -180,8 +196,8 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); records = SchemaTestUtil.generateTestRecords(0, 100); - dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); long size2 = writer.getCurrentSize(); assertTrue("We just wrote a new block - size2 should be > size1", size2 > size1); @@ -195,8 +211,8 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); records = SchemaTestUtil.generateTestRecords(0, 100); - dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); long size3 = writer.getCurrentSize(); assertTrue("We just wrote a new block - size3 should be > size2", size3 > size2); @@ -220,10 +236,10 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); List records = SchemaTestUtil.generateTestRecords(0, 100); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); long size1 = writer.getCurrentSize(); // do not close this writer - this simulates a data note appending to a log dying without closing the file @@ -233,8 +249,8 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); records = SchemaTestUtil.generateTestRecords(0, 100); - dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); long size2 = writer.getCurrentSize(); assertTrue("We just wrote a new block - size2 should be > size1", size2 > size1); @@ -256,10 +272,10 @@ public class HoodieLogFormatTest { // Some data & append two times. List records = SchemaTestUtil.generateTestRecords(0, 100); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); for (int i = 0; i < 2; i++) { HoodieLogFormat.newWriterBuilder().onParentPath(testPath) @@ -284,15 +300,15 @@ public class HoodieLogFormatTest { List copyOfRecords = records.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); writer.close(); Reader reader = HoodieLogFormat - .newReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema(), true); + .newReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema()); assertTrue("We wrote a block, we should be able to read it", reader.hasNext()); HoodieLogBlock nextBlock = reader.next(); assertEquals("The next block should be a data block", HoodieLogBlockType.AVRO_DATA_BLOCK, @@ -316,10 +332,10 @@ public class HoodieLogFormatTest { List copyOfRecords1 = records1.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - getSimpleSchema(), metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); writer.close(); @@ -330,8 +346,8 @@ public class HoodieLogFormatTest { List copyOfRecords2 = records2.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - dataBlock = new HoodieAvroDataBlock(records2, - getSimpleSchema(), metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + dataBlock = new HoodieAvroDataBlock(records2, header); writer = writer.appendBlock(dataBlock); writer.close(); @@ -343,13 +359,13 @@ public class HoodieLogFormatTest { List copyOfRecords3 = records3.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - dataBlock = new HoodieAvroDataBlock(records3, - getSimpleSchema(), metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + dataBlock = new HoodieAvroDataBlock(records3, header); writer = writer.appendBlock(dataBlock); writer.close(); Reader reader = HoodieLogFormat - .newReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema(), true); + .newReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema()); assertTrue("First block should be available", reader.hasNext()); HoodieLogBlock nextBlock = reader.next(); HoodieAvroDataBlock dataBlockRead = (HoodieAvroDataBlock) nextBlock; @@ -373,6 +389,51 @@ public class HoodieLogFormatTest { dataBlockRead.getRecords()); } + @SuppressWarnings("unchecked") + @Test + public void testBasicAppendAndScanMultipleFiles() + throws IOException, URISyntaxException, InterruptedException { + Writer writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withSizeThreshold(1024).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + Schema schema = HoodieAvroUtils.addMetadataFields(getSimpleSchema()); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + + Set logFiles = new HashSet<>(); + List> allRecords = new ArrayList<>(); + // create 4 log files + while(writer.getLogFile().getLogVersion() != 4) { + logFiles.add(writer.getLogFile()); + List records1 = SchemaTestUtil.generateHoodieTestRecords(0, 100); + List copyOfRecords1 = records1.stream().map(record -> + HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) + .collect(Collectors.toList()); + allRecords.add(copyOfRecords1); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); + writer = writer.appendBlock(dataBlock); + } + writer.close(); + + // scan all log blocks (across multiple log files) + HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, + logFiles.stream().map(logFile -> logFile.getPath().toString()).collect(Collectors.toList()), + schema, "100", 10240L, readBlocksLazily, false); + + List scannedRecords = new ArrayList<>(); + for(HoodieRecord record: scanner) { + scannedRecords.add((IndexedRecord) record.getData().getInsertValue(schema).get()); + } + + assertEquals("Scanner records count should be the same as appended records", + scannedRecords.size(), allRecords.stream().flatMap(records -> records.stream()) + .collect(Collectors.toList()).size()); + + } + + @Test public void testAppendAndReadOnCorruptedLog() throws IOException, URISyntaxException, InterruptedException { @@ -380,10 +441,10 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); List records = SchemaTestUtil.generateTestRecords(0, 100); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, - getSimpleSchema(), metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); writer.close(); @@ -392,19 +453,20 @@ public class HoodieLogFormatTest { FSDataOutputStream outputStream = fs.append(writer.getLogFile().getPath()); // create a block with outputStream.write(HoodieLogFormat.MAGIC); - outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); // Write out a length that does not confirm with the content - outputStream.writeInt(1000); - // Write out some metadata - // TODO : test for failure to write metadata - NA ? - outputStream.write(HoodieLogBlock.getLogMetadataBytes(metadata)); + outputStream.writeLong(1000); + outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); + outputStream.writeInt(HoodieLogFormat.currentVersion); + // Write out a length that does not confirm with the content + outputStream.writeLong(500); + // Write out some bytes outputStream.write("something-random".getBytes()); outputStream.flush(); outputStream.close(); // First round of reads - we should be able to read the first block and then EOF Reader reader = HoodieLogFormat - .newReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema(), true); + .newReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema()); assertTrue("First block should be available", reader.hasNext()); reader.next(); assertTrue("We should have corrupted block next", reader.hasNext()); @@ -412,19 +474,20 @@ public class HoodieLogFormatTest { assertEquals("The read block should be a corrupt block", HoodieLogBlockType.CORRUPT_BLOCK, block.getBlockType()); HoodieCorruptBlock corruptBlock = (HoodieCorruptBlock) block; - assertEquals("", "something-random", new String(corruptBlock.getCorruptedBytes())); + //assertEquals("", "something-random", new String(corruptBlock.getCorruptedBytes())); assertFalse("There should be no more block left", reader.hasNext()); // Simulate another failure back to back outputStream = fs.append(writer.getLogFile().getPath()); // create a block with outputStream.write(HoodieLogFormat.MAGIC); - outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); // Write out a length that does not confirm with the content - outputStream.writeInt(1000); - // Write out some metadata - // TODO : test for failure to write metadata - NA ? - outputStream.write(HoodieLogBlock.getLogMetadataBytes(metadata)); + outputStream.writeLong(1000); + outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); + outputStream.writeInt(HoodieLogFormat.currentVersion); + // Write out a length that does not confirm with the content + outputStream.writeLong(500); + // Write out some bytes outputStream.write("something-else-random".getBytes()); outputStream.flush(); outputStream.close(); @@ -434,13 +497,14 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); records = SchemaTestUtil.generateTestRecords(0, 100); - dataBlock = new HoodieAvroDataBlock(records, getSimpleSchema(), metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, getSimpleSchema().toString()); + dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); writer.close(); // Second round of reads - we should be able to read the first and last block reader = HoodieLogFormat - .newReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema(), true); + .newReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema()); assertTrue("First block should be available", reader.hasNext()); reader.next(); assertTrue("We should get the 1st corrupted block next", reader.hasNext()); @@ -450,7 +514,7 @@ public class HoodieLogFormatTest { assertEquals("The read block should be a corrupt block", HoodieLogBlockType.CORRUPT_BLOCK, block.getBlockType()); corruptBlock = (HoodieCorruptBlock) block; - assertEquals("", "something-else-random", new String(corruptBlock.getCorruptedBytes())); + //assertEquals("", "something-else-random", new String(corruptBlock.getCorruptedBytes())); assertTrue("We should get the last block next", reader.hasNext()); reader.next(); assertFalse("We should have no more blocks left", reader.hasNext()); @@ -471,10 +535,10 @@ public class HoodieLogFormatTest { HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - schema, metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); // Write 2 @@ -482,7 +546,8 @@ public class HoodieLogFormatTest { List copyOfRecords2 = records2.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - dataBlock = new HoodieAvroDataBlock(records2, schema, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + dataBlock = new HoodieAvroDataBlock(records2, header); writer = writer.appendBlock(dataBlock); writer.close(); @@ -493,7 +558,7 @@ public class HoodieLogFormatTest { HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, allLogFiles, - schema, "100", 10240L); + schema, "100", 10240L, readBlocksLazily, false); assertEquals("", 200, scanner.getTotalLogRecords()); Set readKeys = new HashSet<>(200); scanner.forEach(s -> readKeys.add(s.getKey().getRecordKey())); @@ -521,32 +586,35 @@ public class HoodieLogFormatTest { List copyOfRecords1 = records1.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - schema, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); // Write 2 - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "101"); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "101"); List records2 = SchemaTestUtil.generateHoodieTestRecords(0, 100); - dataBlock = new HoodieAvroDataBlock(records2, schema, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + dataBlock = new HoodieAvroDataBlock(records2, header); writer = writer.appendBlock(dataBlock); // Rollback the last write - metadata.put(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME, "101"); - HoodieCommandBlock commandBlock = new HoodieCommandBlock( - HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME, "101"); + header.put(HoodieLogBlock.HeaderMetadataType.COMMAND_BLOCK_TYPE, + String.valueOf(HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK.ordinal())); + HoodieCommandBlock commandBlock = new HoodieCommandBlock(header); writer = writer.appendBlock(commandBlock); // Write 3 - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "102"); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "102"); List records3 = SchemaTestUtil.generateHoodieTestRecords(0, 100); List copyOfRecords3 = records3.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - dataBlock = new HoodieAvroDataBlock(records3, schema, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + dataBlock = new HoodieAvroDataBlock(records3, header); writer = writer.appendBlock(dataBlock); writer.close(); @@ -557,8 +625,9 @@ public class HoodieLogFormatTest { HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, allLogFiles, - schema, "102", 10240L); - assertEquals("We only read 200 records, but only 200 of them are valid", 200, scanner.getTotalLogRecords()); + schema, "102", 10240L, readBlocksLazily, false); + assertEquals("We read 200 records from 2 write batches", 200, + scanner.getTotalLogRecords()); Set readKeys = new HashSet<>(200); scanner.forEach(s -> readKeys.add(s.getKey().getRecordKey())); assertEquals("Stream collect should return all 200 records", 200, readKeys.size()); @@ -585,48 +654,53 @@ public class HoodieLogFormatTest { List copyOfRecords1 = records1.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - schema, metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); writer.close(); // Write 2 - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "101"); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "101"); // Append some arbit byte[] to thee end of the log (mimics a partially written commit) fs = FSUtils.getFs(fs.getUri().toString(), fs.getConf()); FSDataOutputStream outputStream = fs.append(writer.getLogFile().getPath()); // create a block with outputStream.write(HoodieLogFormat.MAGIC); - outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); // Write out a length that does not confirm with the content - outputStream.writeInt(100); - // Write out some metadata - // TODO : test for failure to write metadata - NA ? - outputStream.write(HoodieLogBlock.getLogMetadataBytes(metadata)); + outputStream.writeLong(1000); + + outputStream.writeInt(HoodieLogFormat.currentVersion); + outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); + + // Write out some header + outputStream.write(HoodieLogBlock.getLogMetadataBytes(header)); + outputStream.writeLong("something-random".getBytes().length); outputStream.write("something-random".getBytes()); outputStream.flush(); outputStream.close(); // Rollback the last write - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "102"); - metadata.put(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME, "101"); - HoodieCommandBlock commandBlock = new HoodieCommandBlock( - HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "102"); + header.put(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME, "101"); + header.put(HoodieLogBlock.HeaderMetadataType.COMMAND_BLOCK_TYPE, + String.valueOf(HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK.ordinal())); + HoodieCommandBlock commandBlock = new HoodieCommandBlock(header); writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); writer = writer.appendBlock(commandBlock); // Write 3 - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "103"); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "103"); List records3 = SchemaTestUtil.generateHoodieTestRecords(0, 100); List copyOfRecords3 = records3.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - dataBlock = new HoodieAvroDataBlock(records3, schema, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + dataBlock = new HoodieAvroDataBlock(records3, header); writer = writer.appendBlock(dataBlock); writer.close(); @@ -637,8 +711,9 @@ public class HoodieLogFormatTest { HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, allLogFiles, - schema, "103", 10240L); - assertEquals("We would read 200 records", 200, scanner.getTotalLogRecords()); + schema, "103", 10240L, true, false); + assertEquals("We would read 200 records", 200, + scanner.getTotalLogRecords()); Set readKeys = new HashSet<>(200); scanner.forEach(s -> readKeys.add(s.getKey().getRecordKey())); assertEquals("Stream collect should return all 200 records", 200, readKeys.size()); @@ -665,19 +740,19 @@ public class HoodieLogFormatTest { List copyOfRecords1 = records1.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - schema, metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); // Write 2 - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "101"); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "101"); List records2 = SchemaTestUtil.generateHoodieTestRecords(0, 100); List copyOfRecords2 = records2.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - dataBlock = new HoodieAvroDataBlock(records2, schema, metadata); + dataBlock = new HoodieAvroDataBlock(records2, header); writer = writer.appendBlock(dataBlock); copyOfRecords1.addAll(copyOfRecords2); @@ -689,9 +764,9 @@ public class HoodieLogFormatTest { // Delete 50 keys List deletedKeys = originalKeys.subList(0, 50); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "102"); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "102"); HoodieDeleteBlock deleteBlock = new HoodieDeleteBlock(deletedKeys.toArray(new String[50]), - metadata); + header); writer = writer.appendBlock(deleteBlock); List allLogFiles = FSUtils @@ -701,8 +776,9 @@ public class HoodieLogFormatTest { HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, allLogFiles, - schema, "102", 10240L); - assertEquals("We still would read 200 records", 200, scanner.getTotalLogRecords()); + schema, "102", 10240L, readBlocksLazily, false); + assertEquals("We still would read 200 records", 200, + scanner.getTotalLogRecords()); final List readKeys = new ArrayList<>(200); scanner.forEach(s -> readKeys.add(s.getKey().getRecordKey())); assertEquals("Stream collect should return all 150 records", 150, readKeys.size()); @@ -713,14 +789,16 @@ public class HoodieLogFormatTest { readKeys); // Rollback the last block - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "103"); - metadata.put(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME, "102"); - HoodieCommandBlock commandBlock = new HoodieCommandBlock( - HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "103"); + header.put(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME, "102"); + header.put(HoodieLogBlock.HeaderMetadataType.COMMAND_BLOCK_TYPE, + String.valueOf(HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK.ordinal())); + HoodieCommandBlock commandBlock = new HoodieCommandBlock(header); writer = writer.appendBlock(commandBlock); readKeys.clear(); - scanner = new HoodieCompactedLogRecordScanner(fs, basePath, allLogFiles, schema, "101", 10240L); + scanner = new HoodieCompactedLogRecordScanner(fs, basePath, allLogFiles, schema, "101", 10240L, readBlocksLazily, + false); scanner.forEach(s -> readKeys.add(s.getKey().getRecordKey())); assertEquals("Stream collect should return all 200 records after rollback of delete", 200, readKeys.size()); @@ -741,17 +819,18 @@ public class HoodieLogFormatTest { List records1 = SchemaTestUtil.generateHoodieTestRecords(0, 100); List copyOfRecords1 = records1.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)).collect(Collectors.toList()); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - metadata.put(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME, "100"); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - schema, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); // Write 2 List records2 = SchemaTestUtil.generateHoodieTestRecords(0, 100); - dataBlock = new HoodieAvroDataBlock(records2, schema, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + dataBlock = new HoodieAvroDataBlock(records2, header); writer = writer.appendBlock(dataBlock); List originalKeys = copyOfRecords1.stream() @@ -762,12 +841,13 @@ public class HoodieLogFormatTest { // Delete 50 keys List deletedKeys = originalKeys.subList(0, 50); HoodieDeleteBlock deleteBlock = new HoodieDeleteBlock(deletedKeys.toArray(new String[50]), - metadata); + header); writer = writer.appendBlock(deleteBlock); // Attempt 1 : Write rollback block for a failed write - HoodieCommandBlock commandBlock = new HoodieCommandBlock( - HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.COMMAND_BLOCK_TYPE, + String.valueOf(HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK.ordinal())); + HoodieCommandBlock commandBlock = new HoodieCommandBlock(header); try { writer = writer.appendBlock(commandBlock); // Say job failed, retry writing 2 rollback in the next rollback(..) attempt @@ -785,7 +865,7 @@ public class HoodieLogFormatTest { // all data must be rolled back before merge HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, - allLogFiles, schema, "100", 10240L); + allLogFiles, schema, "100", 10240L, readBlocksLazily, false); assertEquals("We would have scanned 0 records because of rollback", 0, scanner.getTotalLogRecords()); final List readKeys = new ArrayList<>(); @@ -809,11 +889,11 @@ public class HoodieLogFormatTest { List copyOfRecords1 = records1.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - metadata.put(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - schema, metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); List originalKeys = copyOfRecords1.stream() @@ -824,12 +904,13 @@ public class HoodieLogFormatTest { // Delete 50 keys List deletedKeys = originalKeys.subList(0, 50); HoodieDeleteBlock deleteBlock = new HoodieDeleteBlock(deletedKeys.toArray(new String[50]), - metadata); + header); writer = writer.appendBlock(deleteBlock); // Write 2 rollback blocks (1 data block + 1 delete bloc) for a failed write - HoodieCommandBlock commandBlock = new HoodieCommandBlock( - HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.COMMAND_BLOCK_TYPE, + String.valueOf(HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK.ordinal())); + HoodieCommandBlock commandBlock = new HoodieCommandBlock(header); writer = writer.appendBlock(commandBlock); writer = writer.appendBlock(commandBlock); @@ -839,7 +920,7 @@ public class HoodieLogFormatTest { .collect(Collectors.toList()); HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, - allLogFiles, schema, "100", 10240L); + allLogFiles, schema, "100", 10240L, readBlocksLazily, false); assertEquals("We would read 0 records", 0, scanner.getTotalLogRecords()); } @@ -855,16 +936,17 @@ public class HoodieLogFormatTest { // Write 1 List records1 = SchemaTestUtil.generateHoodieTestRecords(0, 100); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - schema, metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); // Write invalid rollback for a failed write (possible for in-flight commits) - metadata.put(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME, "101"); - HoodieCommandBlock commandBlock = new HoodieCommandBlock( - HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME, "101"); + header.put(HoodieLogBlock.HeaderMetadataType.COMMAND_BLOCK_TYPE, + String.valueOf(HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK.ordinal())); + HoodieCommandBlock commandBlock = new HoodieCommandBlock(header); writer = writer.appendBlock(commandBlock); List allLogFiles = FSUtils @@ -873,7 +955,7 @@ public class HoodieLogFormatTest { .collect(Collectors.toList()); HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, - allLogFiles, schema, "100", 10240L); + allLogFiles, schema, "100", 10240L, readBlocksLazily, false); assertEquals("We still would read 100 records", 100, scanner.getTotalLogRecords()); final List readKeys = new ArrayList<>(100); @@ -897,11 +979,11 @@ public class HoodieLogFormatTest { List copyOfRecords1 = records1.stream().map(record -> HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) .collect(Collectors.toList()); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - metadata.put(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - schema, metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); writer = writer.appendBlock(dataBlock); writer = writer.appendBlock(dataBlock); @@ -914,13 +996,14 @@ public class HoodieLogFormatTest { // Delete 50 keys List deletedKeys = originalKeys.subList(0, 50); HoodieDeleteBlock deleteBlock = new HoodieDeleteBlock(deletedKeys.toArray(new String[50]), - metadata); + header); writer = writer.appendBlock(deleteBlock); // Write 1 rollback block for a failed write - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "101"); - HoodieCommandBlock commandBlock = new HoodieCommandBlock( - HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "101"); + header.put(HoodieLogBlock.HeaderMetadataType.COMMAND_BLOCK_TYPE, + String.valueOf(HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK.ordinal())); + HoodieCommandBlock commandBlock = new HoodieCommandBlock(header); writer = writer.appendBlock(commandBlock); List allLogFiles = FSUtils @@ -929,7 +1012,7 @@ public class HoodieLogFormatTest { .collect(Collectors.toList()); HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, - allLogFiles, schema, "101", 10240L); + allLogFiles, schema, "101", 10240L, readBlocksLazily, false); assertEquals("We would read 0 records", 0, scanner.getTotalLogRecords()); } @@ -947,11 +1030,11 @@ public class HoodieLogFormatTest { // Write 1 List records1 = SchemaTestUtil.generateHoodieTestRecords(0, 100); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "100"); - metadata.put(HoodieLogBlock.LogMetadataType.TARGET_INSTANT_TIME, "100"); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, - schema, metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.TARGET_INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); writer = writer.appendBlock(dataBlock); writer = writer.appendBlock(dataBlock); writer = writer.appendBlock(dataBlock); @@ -962,9 +1045,11 @@ public class HoodieLogFormatTest { FSDataOutputStream outputStream = fs.append(writer.getLogFile().getPath()); // create a block with outputStream.write(HoodieLogFormat.MAGIC); + outputStream.writeLong(1000); outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); + outputStream.writeInt(HoodieLogFormat.currentVersion); // Write out a length that does not confirm with the content - outputStream.writeInt(100); + outputStream.writeLong(100); outputStream.flush(); outputStream.close(); @@ -973,9 +1058,11 @@ public class HoodieLogFormatTest { outputStream = fs.append(writer.getLogFile().getPath()); // create a block with outputStream.write(HoodieLogFormat.MAGIC); + outputStream.writeLong(1000); outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); + outputStream.writeInt(HoodieLogFormat.currentVersion); // Write out a length that does not confirm with the content - outputStream.writeInt(100); + outputStream.writeLong(100); outputStream.flush(); outputStream.close(); @@ -991,9 +1078,11 @@ public class HoodieLogFormatTest { outputStream = fs.append(writer.getLogFile().getPath()); // create a block with outputStream.write(HoodieLogFormat.MAGIC); + outputStream.writeLong(1000); outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); + outputStream.writeInt(HoodieLogFormat.currentVersion); // Write out a length that does not confirm with the content - outputStream.writeInt(100); + outputStream.writeLong(100); outputStream.flush(); outputStream.close(); @@ -1001,9 +1090,10 @@ public class HoodieLogFormatTest { .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") .overBaseCommit("100").withFs(fs).build(); // Write 1 rollback block for a failed write - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, "101"); - HoodieCommandBlock commandBlock = new HoodieCommandBlock( - HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK, metadata); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "101"); + header.put(HoodieLogBlock.HeaderMetadataType.COMMAND_BLOCK_TYPE, + String.valueOf(HoodieCommandBlock.HoodieCommandBlockTypeEnum.ROLLBACK_PREVIOUS_BLOCK.ordinal())); + HoodieCommandBlock commandBlock = new HoodieCommandBlock(header); writer = writer.appendBlock(commandBlock); List allLogFiles = FSUtils @@ -1012,9 +1102,282 @@ public class HoodieLogFormatTest { .collect(Collectors.toList()); HoodieCompactedLogRecordScanner scanner = new HoodieCompactedLogRecordScanner(fs, basePath, - allLogFiles, schema, "101", 10240L); + allLogFiles, schema, "101", 10240L, readBlocksLazily, false); assertEquals("We would read 0 records", 0, scanner.getTotalLogRecords()); } + @Test + public void testMagicAndLogVersionsBackwardsCompatibility() + throws IOException, InterruptedException, URISyntaxException { + // Create the log file + Writer writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + Schema schema = HoodieAvroUtils.addMetadataFields(getSimpleSchema()); + List records = SchemaTestUtil.generateHoodieTestRecords(0, 100); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + writer.close(); + + // Write 1 with OLD_MAGIC and no log format version + // Append a log block to end of the log (mimics a log block with old format + // fs = FSUtils.getFs(fs.getUri().toString(), fs.getConf()); + FSDataOutputStream outputStream = fs.append(writer.getLogFile().getPath()); + // create a block with + outputStream.write(HoodieLogFormat.OLD_MAGIC); + outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); + // Write out a length that does not confirm with the content + records = SchemaTestUtil.generateHoodieTestRecords(0, 100); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, new String(HoodieAvroUtils.compress(schema.toString()))); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, schema); + byte [] content = dataBlock.getBytes(schema); + outputStream.writeInt(content.length); + // Write out some content + outputStream.write(content); + outputStream.flush(); + outputStream.hflush(); + outputStream.close(); + + writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + + // Write 2 with MAGIC and latest log format version + records = SchemaTestUtil.generateHoodieTestRecords(0, 100); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + dataBlock = new HoodieAvroDataBlock(records, header); + writer = writer.appendBlock(dataBlock); + + // Write 3 with MAGIC and latest log format version + writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + records = SchemaTestUtil.generateHoodieTestRecords(0, 100); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + dataBlock = new HoodieAvroDataBlock(records, header); + writer = writer.appendBlock(dataBlock); + writer.close(); + + Reader reader = HoodieLogFormat + .newReader(fs, writer.getLogFile(), schema); + + // Read the first block written with latest version and magic + reader.hasNext(); + HoodieLogBlock block = reader.next(); + assertEquals(block.getBlockType(), HoodieLogBlockType.AVRO_DATA_BLOCK); + HoodieAvroDataBlock dBlock = (HoodieAvroDataBlock) block; + assertEquals(dBlock.getRecords().size(), 100); + + // Read second block written with old magic and no version + reader.hasNext(); + block = reader.next(); + assertEquals(block.getBlockType(), HoodieLogBlockType.AVRO_DATA_BLOCK); + dBlock = (HoodieAvroDataBlock) block; + assertEquals(dBlock.getRecords().size(), 100); + + //Read third block written with latest version and magic + reader.hasNext(); + block = reader.next(); + assertEquals(block.getBlockType(), HoodieLogBlockType.AVRO_DATA_BLOCK); + dBlock = (HoodieAvroDataBlock) block; + assertEquals(dBlock.getRecords().size(), 100); + + } + + @SuppressWarnings("unchecked") + @Test + public void testBasicAppendAndReadInReverse() + throws IOException, URISyntaxException, InterruptedException { + Writer writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + Schema schema = getSimpleSchema(); + List records1 = SchemaTestUtil.generateTestRecords(0, 100); + List copyOfRecords1 = records1.stream().map(record -> + HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) + .collect(Collectors.toList()); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); + writer = writer.appendBlock(dataBlock); + writer.close(); + + writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + List records2 = SchemaTestUtil.generateTestRecords(0, 100); + List copyOfRecords2 = records2.stream().map(record -> + HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) + .collect(Collectors.toList()); + dataBlock = new HoodieAvroDataBlock(records2, header); + writer = writer.appendBlock(dataBlock); + writer.close(); + + // Close and Open again and append 100 more records + writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + List records3 = SchemaTestUtil.generateTestRecords(0, 100); + List copyOfRecords3 = records3.stream().map(record -> + HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) + .collect(Collectors.toList()); + dataBlock = new HoodieAvroDataBlock(records3, header); + writer = writer.appendBlock(dataBlock); + writer.close(); + + HoodieLogFileReader reader = + new HoodieLogFileReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema(), readBlocksLazily, + true); + + assertTrue("Last block should be available", reader.hasPrev()); + HoodieLogBlock prevBlock = reader.prev(); + HoodieAvroDataBlock dataBlockRead = (HoodieAvroDataBlock) prevBlock; + + assertEquals("Third records size should be equal to the written records size", + copyOfRecords3.size(), dataBlockRead.getRecords().size()); + assertEquals("Both records lists should be the same. (ordering guaranteed)", copyOfRecords3, + dataBlockRead.getRecords()); + + assertTrue("Second block should be available", reader.hasPrev()); + prevBlock = reader.prev(); + dataBlockRead = (HoodieAvroDataBlock) prevBlock; + assertEquals("Read records size should be equal to the written records size", + copyOfRecords2.size(), dataBlockRead.getRecords().size()); + assertEquals("Both records lists should be the same. (ordering guaranteed)", copyOfRecords2, + dataBlockRead.getRecords()); + + assertTrue("First block should be available", reader.hasPrev()); + prevBlock = reader.prev(); + dataBlockRead = (HoodieAvroDataBlock) prevBlock; + assertEquals("Read records size should be equal to the written records size", + copyOfRecords1.size(), dataBlockRead.getRecords().size()); + assertEquals("Both records lists should be the same. (ordering guaranteed)", copyOfRecords1, + dataBlockRead.getRecords()); + + assertFalse(reader.hasPrev()); + } + + @Test + public void testAppendAndReadOnCorruptedLogInReverse() + throws IOException, URISyntaxException, InterruptedException { + Writer writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + Schema schema = getSimpleSchema(); + List records = SchemaTestUtil.generateTestRecords(0, 100); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); + writer = writer.appendBlock(dataBlock); + writer.close(); + + // Append some arbit byte[] to thee end of the log (mimics a partially written commit) + fs = FSUtils.getFs(fs.getUri().toString(), fs.getConf()); + FSDataOutputStream outputStream = fs.append(writer.getLogFile().getPath()); + // create a block with + outputStream.write(HoodieLogFormat.OLD_MAGIC); + outputStream.writeInt(HoodieLogBlockType.AVRO_DATA_BLOCK.ordinal()); + // Write out a length that does not confirm with the content + outputStream.writeInt(1000); + // Write out footer length + outputStream.writeInt(1); + // Write out some metadata + // TODO : test for failure to write metadata - NA ? + outputStream.write(HoodieLogBlock.getLogMetadataBytes(header)); + outputStream.write("something-random".getBytes()); + outputStream.flush(); + outputStream.close(); + + // Should be able to append a new block + writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + records = SchemaTestUtil.generateTestRecords(0, 100); + dataBlock = new HoodieAvroDataBlock(records, header); + writer = writer.appendBlock(dataBlock); + writer.close(); + + // First round of reads - we should be able to read the first block and then EOF + HoodieLogFileReader reader = + new HoodieLogFileReader(fs, writer.getLogFile(), schema, readBlocksLazily, true); + + assertTrue("Last block should be available", reader.hasPrev()); + HoodieLogBlock block = reader.prev(); + assertTrue("Last block should be datablock", block instanceof HoodieAvroDataBlock); + + assertTrue("Last block should be available", reader.hasPrev()); + try { + reader.prev(); + } catch(CorruptedLogFileException e) { + e.printStackTrace(); + // We should have corrupted block + } + } + + @SuppressWarnings("unchecked") + @Test + public void testBasicAppendAndTraverseInReverse() + throws IOException, URISyntaxException, InterruptedException { + Writer writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + Schema schema = getSimpleSchema(); + List records1 = SchemaTestUtil.generateTestRecords(0, 100); + List copyOfRecords1 = records1.stream().map(record -> + HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) + .collect(Collectors.toList()); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, "100"); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records1, header); + writer = writer.appendBlock(dataBlock); + writer.close(); + + writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + List records2 = SchemaTestUtil.generateTestRecords(0, 100); + List copyOfRecords2 = records2.stream().map(record -> + HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) + .collect(Collectors.toList()); + dataBlock = new HoodieAvroDataBlock(records2, header); + writer = writer.appendBlock(dataBlock); + writer.close(); + + // Close and Open again and append 100 more records + writer = HoodieLogFormat.newWriterBuilder().onParentPath(partitionPath) + .withFileExtension(HoodieLogFile.DELTA_EXTENSION).withFileId("test-fileid1") + .overBaseCommit("100").withFs(fs).build(); + List records3 = SchemaTestUtil.generateTestRecords(0, 100); + List copyOfRecords3 = records3.stream().map(record -> + HoodieAvroUtils.rewriteRecord((GenericRecord) record, schema)) + .collect(Collectors.toList()); + dataBlock = new HoodieAvroDataBlock(records3, header); + writer = writer.appendBlock(dataBlock); + writer.close(); + + HoodieLogFileReader reader = + new HoodieLogFileReader(fs, writer.getLogFile(), SchemaTestUtil.getSimpleSchema(), readBlocksLazily, + true); + + assertTrue("Third block should be available", reader.hasPrev()); + reader.moveToPrev(); + + assertTrue("Second block should be available", reader.hasPrev()); + reader.moveToPrev(); + + // After moving twice, this last reader.prev() should read the First block written + assertTrue("First block should be available", reader.hasPrev()); + HoodieLogBlock prevBlock = reader.prev(); + HoodieAvroDataBlock dataBlockRead = (HoodieAvroDataBlock) prevBlock; + assertEquals("Read records size should be equal to the written records size", + copyOfRecords1.size(), dataBlockRead.getRecords().size()); + assertEquals("Both records lists should be the same. (ordering guaranteed)", copyOfRecords1, + dataBlockRead.getRecords()); + + assertFalse(reader.hasPrev()); + } } diff --git a/hoodie-hadoop-mr/src/main/java/com/uber/hoodie/hadoop/realtime/HoodieRealtimeRecordReader.java b/hoodie-hadoop-mr/src/main/java/com/uber/hoodie/hadoop/realtime/HoodieRealtimeRecordReader.java index 9f231084a..81cae359b 100644 --- a/hoodie-hadoop-mr/src/main/java/com/uber/hoodie/hadoop/realtime/HoodieRealtimeRecordReader.java +++ b/hoodie-hadoop-mr/src/main/java/com/uber/hoodie/hadoop/realtime/HoodieRealtimeRecordReader.java @@ -72,6 +72,11 @@ public class HoodieRealtimeRecordReader implements RecordReader deltaRecordMap; @@ -132,7 +137,8 @@ public class HoodieRealtimeRecordReader implements RecordReader the commit we are trying to read (if using readCommit() API) for (HoodieRecord hoodieRecord : compactedLogRecordScanner) { @@ -140,6 +146,7 @@ public class HoodieRealtimeRecordReader implements RecordReader metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, newCommit); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, writeSchema, metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, newCommit); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, writeSchema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); writer = writer.appendBlock(dataBlock); long size = writer.getCurrentSize(); return writer; diff --git a/hoodie-hive/src/main/java/com/uber/hoodie/hive/HoodieHiveClient.java b/hoodie-hive/src/main/java/com/uber/hoodie/hive/HoodieHiveClient.java index 95d1d5821..69ae7aff5 100644 --- a/hoodie-hive/src/main/java/com/uber/hoodie/hive/HoodieHiveClient.java +++ b/hoodie-hive/src/main/java/com/uber/hoodie/hive/HoodieHiveClient.java @@ -395,7 +395,7 @@ public class HoodieHiveClient { @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private MessageType readSchemaFromLogFile(Optional lastCompactionCommitOpt, Path path) throws IOException { - Reader reader = HoodieLogFormat.newReader(fs, new HoodieLogFile(path), null, true); + Reader reader = HoodieLogFormat.newReader(fs, new HoodieLogFile(path), null); HoodieAvroDataBlock lastBlock = null; while (reader.hasNext()) { HoodieLogBlock block = reader.next(); @@ -404,6 +404,7 @@ public class HoodieHiveClient { } } if (lastBlock != null) { + lastBlock.getRecords(); return new parquet.avro.AvroSchemaConverter().convert(lastBlock.getSchema()); } // Fall back to read the schema from last compaction diff --git a/hoodie-hive/src/test/java/com/uber/hoodie/hive/TestUtil.java b/hoodie-hive/src/test/java/com/uber/hoodie/hive/TestUtil.java index f2eb5e4f1..10b8c11f8 100644 --- a/hoodie-hive/src/test/java/com/uber/hoodie/hive/TestUtil.java +++ b/hoodie-hive/src/test/java/com/uber/hoodie/hive/TestUtil.java @@ -314,9 +314,10 @@ public class TestUtil { List records = (isLogSchemaSimple ? SchemaTestUtil .generateTestRecords(0, 100) : SchemaTestUtil.generateEvolvedTestRecords(100, 100)); - Map metadata = Maps.newHashMap(); - metadata.put(HoodieLogBlock.LogMetadataType.INSTANT_TIME, dataFile.getCommitTime()); - HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, schema, metadata); + Map header = Maps.newHashMap(); + header.put(HoodieLogBlock.HeaderMetadataType.INSTANT_TIME, dataFile.getCommitTime()); + header.put(HoodieLogBlock.HeaderMetadataType.SCHEMA, schema.toString()); + HoodieAvroDataBlock dataBlock = new HoodieAvroDataBlock(records, header); logWriter.appendBlock(dataBlock); logWriter.close(); return logWriter.getLogFile();