You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by lz...@apache.org on 2021/12/14 10:22:03 UTC
[flink] branch master updated: [FLINK-24565][avro] Port avro file format factory to BulkReaderFormatFactory
This is an automated email from the ASF dual-hosted git repository.
lzljs3620320 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new 388dce6 [FLINK-24565][avro] Port avro file format factory to BulkReaderFormatFactory
388dce6 is described below
commit 388dce6af221f5cbe7807d1439305555c91c7449
Author: tsreaper <ts...@gmail.com>
AuthorDate: Tue Dec 14 18:21:33 2021 +0800
[FLINK-24565][avro] Port avro file format factory to BulkReaderFormatFactory
This closes #17520
---
flink-formats/flink-avro/pom.xml | 8 +
.../flink/formats/avro/AbstractAvroBulkFormat.java | 207 ++++++++++++++++
.../flink/formats/avro/AvroFileFormatFactory.java | 68 ++++-
.../formats/avro/AvroFileSystemFormatFactory.java | 193 ---------------
.../org.apache.flink.table.factories.Factory | 1 -
.../flink/formats/avro/AvroBulkFormatITCase.java | 231 +++++++++++++++++
.../flink/formats/avro/AvroBulkFormatTest.java | 275 +++++++++++++++++++++
.../formats/avro/AvroBulkFormatTestUtils.java | 76 ++++++
.../flink/formats/avro/AvroFilesystemITCase.java | 7 +-
9 files changed, 865 insertions(+), 201 deletions(-)
diff --git a/flink-formats/flink-avro/pom.xml b/flink-formats/flink-avro/pom.xml
index d2e05ed..048f0f1 100644
--- a/flink-formats/flink-avro/pom.xml
+++ b/flink-formats/flink-avro/pom.xml
@@ -47,6 +47,7 @@ under the License.
<!-- Table ecosystem and filesystem connector -->
+ <!-- Data stream projects depending on this project might not depend on flink-table-*. -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-common</artifactId>
@@ -86,6 +87,13 @@ under the License.
<dependency>
<groupId>org.apache.flink</groupId>
+ <artifactId>flink-connector-testing</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>
<version>${project.version}</version>
<scope>test</scope>
diff --git a/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AbstractAvroBulkFormat.java b/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AbstractAvroBulkFormat.java
new file mode 100644
index 0000000..1221a04
--- /dev/null
+++ b/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AbstractAvroBulkFormat.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.formats.avro;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.file.src.FileSourceSplit;
+import org.apache.flink.connector.file.src.reader.BulkFormat;
+import org.apache.flink.connector.file.src.util.CheckpointedPosition;
+import org.apache.flink.connector.file.src.util.IteratorResultIterator;
+import org.apache.flink.connector.file.src.util.Pool;
+import org.apache.flink.core.fs.FileSystem;
+import org.apache.flink.core.fs.Path;
+import org.apache.flink.formats.avro.utils.FSDataInputStreamWrapper;
+
+import org.apache.avro.Schema;
+import org.apache.avro.file.DataFileReader;
+import org.apache.avro.file.SeekableInput;
+import org.apache.avro.generic.GenericDatumReader;
+import org.apache.avro.io.DatumReader;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+/** Provides a {@link BulkFormat} for Avro records. */
+@Internal
+public abstract class AbstractAvroBulkFormat<A, T, SplitT extends FileSourceSplit>
+ implements BulkFormat<T, SplitT> {
+
+ private static final long serialVersionUID = 1L;
+
+ protected final Schema readerSchema;
+
+ protected AbstractAvroBulkFormat(Schema readerSchema) {
+ this.readerSchema = readerSchema;
+ }
+
+ @Override
+ public AvroReader createReader(Configuration config, SplitT split) throws IOException {
+ open(split);
+ return createReader(split);
+ }
+
+ @Override
+ public AvroReader restoreReader(Configuration config, SplitT split) throws IOException {
+ open(split);
+ return createReader(split);
+ }
+
+ @Override
+ public boolean isSplittable() {
+ return true;
+ }
+
+ private AvroReader createReader(SplitT split) throws IOException {
+ long end = split.offset() + split.length();
+ if (split.getReaderPosition().isPresent()) {
+ CheckpointedPosition position = split.getReaderPosition().get();
+ return new AvroReader(
+ split.path(),
+ split.offset(),
+ end,
+ position.getOffset(),
+ position.getRecordsAfterOffset());
+ } else {
+ return new AvroReader(split.path(), split.offset(), end, -1, 0);
+ }
+ }
+
+ protected void open(SplitT split) {}
+
+ protected abstract T convert(A record);
+
+ protected abstract A createReusedAvroRecord();
+
+ private class AvroReader implements BulkFormat.Reader<T> {
+
+ private final DataFileReader<A> reader;
+
+ private final long end;
+ private final Pool<A> pool;
+
+ private long currentBlockStart;
+ private long currentRecordsToSkip;
+
+ private AvroReader(Path path, long offset, long end, long blockStart, long recordsToSkip)
+ throws IOException {
+ A reuse = createReusedAvroRecord();
+
+ this.reader = createReaderFromPath(path);
+ if (blockStart >= 0) {
+ reader.seek(blockStart);
+ } else {
+ reader.sync(offset);
+ }
+ for (int i = 0; i < recordsToSkip; i++) {
+ reader.next(reuse);
+ }
+
+ this.end = end;
+ this.pool = new Pool<>(1);
+ this.pool.add(reuse);
+
+ this.currentBlockStart = reader.previousSync();
+ this.currentRecordsToSkip = recordsToSkip;
+ }
+
+ private DataFileReader<A> createReaderFromPath(Path path) throws IOException {
+ FileSystem fileSystem = path.getFileSystem();
+ DatumReader<A> datumReader = new GenericDatumReader<>(null, readerSchema);
+ SeekableInput in =
+ new FSDataInputStreamWrapper(
+ fileSystem.open(path), fileSystem.getFileStatus(path).getLen());
+ return (DataFileReader<A>) DataFileReader.openReader(in, datumReader);
+ }
+
+ @Nullable
+ @Override
+ public RecordIterator<T> readBatch() throws IOException {
+ A reuse;
+ try {
+ reuse = pool.pollEntry();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(
+ "Interrupted while waiting for the previous batch to be consumed", e);
+ }
+
+ if (!readNextBlock()) {
+ pool.recycler().recycle(reuse);
+ return null;
+ }
+
+ currentBlockStart = reader.previousSync();
+ Iterator<T> iterator =
+ new AvroBlockIterator(
+ reader.getBlockCount() - currentRecordsToSkip, reader, reuse);
+ long recordsToSkip = currentRecordsToSkip;
+ currentRecordsToSkip = 0;
+ return new IteratorResultIterator<>(
+ iterator,
+ currentBlockStart,
+ recordsToSkip,
+ () -> pool.recycler().recycle(reuse));
+ }
+
+ private boolean readNextBlock() throws IOException {
+ // read the next block with reader,
+ // returns true if a block is read and false if we reach the end of this split
+ return reader.hasNext() && !reader.pastSync(end);
+ }
+
+ @Override
+ public void close() throws IOException {
+ reader.close();
+ }
+ }
+
+ private class AvroBlockIterator implements Iterator<T> {
+
+ private long numRecordsRemaining;
+ private final DataFileReader<A> reader;
+ private final A reuse;
+
+ private AvroBlockIterator(long numRecordsRemaining, DataFileReader<A> reader, A reuse) {
+ this.numRecordsRemaining = numRecordsRemaining;
+ this.reader = reader;
+ this.reuse = reuse;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return numRecordsRemaining > 0;
+ }
+
+ @Override
+ public T next() {
+ try {
+ numRecordsRemaining--;
+ // reader.next merely deserialize bytes in memory to java objects
+ // and will not read from file
+ return convert(reader.next(reuse));
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "Encountered exception when reading from avro format file", e);
+ }
+ }
+ }
+}
diff --git a/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroFileFormatFactory.java b/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroFileFormatFactory.java
index 7b9d8c0..630d990 100644
--- a/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroFileFormatFactory.java
+++ b/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroFileFormatFactory.java
@@ -20,14 +20,21 @@ package org.apache.flink.formats.avro;
import org.apache.flink.annotation.Internal;
import org.apache.flink.api.common.serialization.BulkWriter;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.configuration.ConfigOption;
import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.connector.file.src.FileSourceSplit;
+import org.apache.flink.connector.file.src.reader.BulkFormat;
+import org.apache.flink.connector.file.table.factories.BulkReaderFormatFactory;
import org.apache.flink.connector.file.table.factories.BulkWriterFormatFactory;
+import org.apache.flink.connector.file.table.format.BulkDecodingFormat;
import org.apache.flink.core.fs.FSDataOutputStream;
import org.apache.flink.formats.avro.typeutils.AvroSchemaConverter;
import org.apache.flink.table.connector.ChangelogMode;
import org.apache.flink.table.connector.format.EncodingFormat;
import org.apache.flink.table.connector.sink.DynamicTableSink;
+import org.apache.flink.table.connector.source.DynamicTableSource;
+import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.factories.DynamicTableFactory;
import org.apache.flink.table.types.DataType;
@@ -36,6 +43,7 @@ import org.apache.flink.table.types.logical.RowType;
import org.apache.avro.Schema;
import org.apache.avro.file.CodecFactory;
import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.DatumWriter;
@@ -49,11 +57,29 @@ import static org.apache.flink.formats.avro.AvroFormatOptions.AVRO_OUTPUT_CODEC;
/** Avro format factory for file system. */
@Internal
-public class AvroFileFormatFactory implements BulkWriterFormatFactory {
+public class AvroFileFormatFactory implements BulkReaderFormatFactory, BulkWriterFormatFactory {
public static final String IDENTIFIER = "avro";
@Override
+ public BulkDecodingFormat<RowData> createDecodingFormat(
+ DynamicTableFactory.Context context, ReadableConfig formatOptions) {
+ return new BulkDecodingFormat<RowData>() {
+ @Override
+ public BulkFormat<RowData, FileSourceSplit> createRuntimeDecoder(
+ DynamicTableSource.Context sourceContext, DataType producedDataType) {
+ return new AvroGenericRecordBulkFormat(
+ sourceContext, (RowType) producedDataType.getLogicalType().copy(false));
+ }
+
+ @Override
+ public ChangelogMode getChangelogMode() {
+ return ChangelogMode.insertOnly();
+ }
+ };
+ }
+
+ @Override
public EncodingFormat<BulkWriter.Factory<RowData>> createEncodingFormat(
DynamicTableFactory.Context context, ReadableConfig formatOptions) {
return new EncodingFormat<BulkWriter.Factory<RowData>>() {
@@ -90,6 +116,46 @@ public class AvroFileFormatFactory implements BulkWriterFormatFactory {
return options;
}
+ private static class AvroGenericRecordBulkFormat
+ extends AbstractAvroBulkFormat<GenericRecord, RowData, FileSourceSplit> {
+
+ private static final long serialVersionUID = 1L;
+
+ private final RowType producedRowType;
+ private final TypeInformation<RowData> producedTypeInfo;
+
+ private transient AvroToRowDataConverters.AvroToRowDataConverter converter;
+ private transient GenericRecord reusedAvroRecord;
+
+ public AvroGenericRecordBulkFormat(
+ DynamicTableSource.Context context, RowType producedRowType) {
+ super(AvroSchemaConverter.convertToSchema(producedRowType));
+ this.producedRowType = producedRowType;
+ this.producedTypeInfo = context.createTypeInformation(producedRowType);
+ }
+
+ @Override
+ protected void open(FileSourceSplit split) {
+ converter = AvroToRowDataConverters.createRowConverter(producedRowType);
+ reusedAvroRecord = new GenericData.Record(readerSchema);
+ }
+
+ @Override
+ protected RowData convert(GenericRecord record) {
+ return record == null ? null : (GenericRowData) converter.convert(record);
+ }
+
+ @Override
+ protected GenericRecord createReusedAvroRecord() {
+ return reusedAvroRecord;
+ }
+
+ @Override
+ public TypeInformation<RowData> getProducedType() {
+ return producedTypeInfo;
+ }
+ }
+
/**
* A {@link BulkWriter.Factory} to convert {@link RowData} to {@link GenericRecord} and wrap
* {@link AvroWriterFactory}.
diff --git a/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroFileSystemFormatFactory.java b/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroFileSystemFormatFactory.java
deleted file mode 100644
index 205ae2f..0000000
--- a/flink-formats/flink-avro/src/main/java/org/apache/flink/formats/avro/AvroFileSystemFormatFactory.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.flink.formats.avro;
-
-import org.apache.flink.annotation.Internal;
-import org.apache.flink.api.common.io.InputFormat;
-import org.apache.flink.configuration.ConfigOption;
-import org.apache.flink.connector.file.table.factories.FileSystemFormatFactory;
-import org.apache.flink.core.fs.FileInputSplit;
-import org.apache.flink.core.fs.Path;
-import org.apache.flink.formats.avro.typeutils.AvroSchemaConverter;
-import org.apache.flink.table.data.GenericRowData;
-import org.apache.flink.table.data.RowData;
-import org.apache.flink.table.types.DataType;
-import org.apache.flink.table.types.logical.RowType;
-import org.apache.flink.table.utils.PartitionPathUtils;
-
-import org.apache.avro.Schema;
-import org.apache.avro.generic.GenericData;
-import org.apache.avro.generic.GenericRecord;
-import org.apache.avro.generic.IndexedRecord;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.apache.flink.formats.avro.AvroFormatOptions.AVRO_OUTPUT_CODEC;
-
-/** Avro format factory for file system. */
-@Internal
-public class AvroFileSystemFormatFactory implements FileSystemFormatFactory {
-
- public static final String IDENTIFIER = "avro";
-
- @Override
- public String factoryIdentifier() {
- return IDENTIFIER;
- }
-
- @Override
- public Set<ConfigOption<?>> requiredOptions() {
- return new HashSet<>();
- }
-
- @Override
- public Set<ConfigOption<?>> optionalOptions() {
- Set<ConfigOption<?>> options = new HashSet<>();
- options.add(AVRO_OUTPUT_CODEC);
- return options;
- }
-
- @Override
- public InputFormat<RowData, ?> createReader(ReaderContext context) {
- String[] fieldNames = context.getSchema().getFieldNames();
- List<String> projectFields =
- Arrays.stream(context.getProjectFields())
- .mapToObj(idx -> fieldNames[idx])
- .collect(Collectors.toList());
- List<String> csvFields =
- Arrays.stream(fieldNames)
- .filter(field -> !context.getPartitionKeys().contains(field))
- .collect(Collectors.toList());
-
- int[] selectFieldToProjectField =
- context.getFormatProjectFields().stream()
- .mapToInt(projectFields::indexOf)
- .toArray();
- int[] selectFieldToFormatField =
- context.getFormatProjectFields().stream().mapToInt(csvFields::indexOf).toArray();
-
- //noinspection unchecked
- return new RowDataAvroInputFormat(
- context.getPaths(),
- context.getFormatRowType(),
- context.getSchema().getFieldDataTypes(),
- context.getSchema().getFieldNames(),
- context.getProjectFields(),
- context.getPartitionKeys(),
- context.getDefaultPartName(),
- context.getPushedDownLimit(),
- selectFieldToProjectField,
- selectFieldToFormatField);
- }
-
- /**
- * InputFormat that reads avro record into {@link RowData}.
- *
- * <p>This extends {@link AvroInputFormat}, but {@link RowData} is not a avro record, so now we
- * remove generic information. TODO {@link AvroInputFormat} support type conversion.
- */
- private static class RowDataAvroInputFormat extends AvroInputFormat {
-
- private static final long serialVersionUID = 1L;
-
- private final DataType[] fieldTypes;
- private final String[] fieldNames;
- private final int[] selectFields;
- private final List<String> partitionKeys;
- private final String defaultPartValue;
- private final long limit;
- private final int[] selectFieldToProjectField;
- private final int[] selectFieldToFormatField;
- private final RowType formatRowType;
-
- private transient long emitted;
- // reuse object for per record
- private transient GenericRowData rowData;
- private transient IndexedRecord record;
- private transient AvroToRowDataConverters.AvroToRowDataConverter converter;
-
- public RowDataAvroInputFormat(
- Path[] filePaths,
- RowType formatRowType,
- DataType[] fieldTypes,
- String[] fieldNames,
- int[] selectFields,
- List<String> partitionKeys,
- String defaultPartValue,
- long limit,
- int[] selectFieldToProjectField,
- int[] selectFieldToFormatField) {
- super(filePaths[0], GenericRecord.class);
- super.setFilePaths(filePaths);
- this.formatRowType = formatRowType;
- this.fieldTypes = fieldTypes;
- this.fieldNames = fieldNames;
- this.partitionKeys = partitionKeys;
- this.defaultPartValue = defaultPartValue;
- this.selectFields = selectFields;
- this.limit = limit;
- this.emitted = 0;
- this.selectFieldToProjectField = selectFieldToProjectField;
- this.selectFieldToFormatField = selectFieldToFormatField;
- }
-
- @Override
- public void open(FileInputSplit split) throws IOException {
- super.open(split);
- Schema schema = AvroSchemaConverter.convertToSchema(formatRowType);
- record = new GenericData.Record(schema);
- rowData =
- PartitionPathUtils.fillPartitionValueForRecord(
- fieldNames,
- fieldTypes,
- selectFields,
- partitionKeys,
- currentSplit.getPath(),
- defaultPartValue);
- this.converter = AvroToRowDataConverters.createRowConverter(formatRowType);
- }
-
- @Override
- public boolean reachedEnd() throws IOException {
- return emitted >= limit || super.reachedEnd();
- }
-
- @Override
- public Object nextRecord(Object reuse) throws IOException {
- @SuppressWarnings("unchecked")
- IndexedRecord r = (IndexedRecord) super.nextRecord(record);
- if (r == null) {
- return null;
- }
- GenericRowData row = (GenericRowData) converter.convert(r);
-
- for (int i = 0; i < selectFieldToFormatField.length; i++) {
- rowData.setField(
- selectFieldToProjectField[i], row.getField(selectFieldToFormatField[i]));
- }
- emitted++;
- return rowData;
- }
- }
-}
diff --git a/flink-formats/flink-avro/src/main/resources/META-INF/services/org.apache.flink.table.factories.Factory b/flink-formats/flink-avro/src/main/resources/META-INF/services/org.apache.flink.table.factories.Factory
index 252b311..40e19c5 100644
--- a/flink-formats/flink-avro/src/main/resources/META-INF/services/org.apache.flink.table.factories.Factory
+++ b/flink-formats/flink-avro/src/main/resources/META-INF/services/org.apache.flink.table.factories.Factory
@@ -15,4 +15,3 @@
org.apache.flink.formats.avro.AvroFormatFactory
org.apache.flink.formats.avro.AvroFileFormatFactory
-org.apache.flink.formats.avro.AvroFileSystemFormatFactory
diff --git a/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroBulkFormatITCase.java b/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroBulkFormatITCase.java
new file mode 100644
index 0000000..7d13d7f
--- /dev/null
+++ b/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroBulkFormatITCase.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.formats.avro;
+
+import org.apache.flink.api.connector.source.Boundedness;
+import org.apache.flink.api.connector.source.Source;
+import org.apache.flink.connector.file.src.FileSource;
+import org.apache.flink.connectors.test.common.TestResource;
+import org.apache.flink.connectors.test.common.environment.ClusterControllable;
+import org.apache.flink.connectors.test.common.environment.MiniClusterTestEnvironment;
+import org.apache.flink.connectors.test.common.environment.TestEnvironment;
+import org.apache.flink.connectors.test.common.external.ExternalContext;
+import org.apache.flink.connectors.test.common.external.SourceSplitDataWriter;
+import org.apache.flink.connectors.test.common.junit.annotations.ExternalContextFactory;
+import org.apache.flink.connectors.test.common.junit.annotations.ExternalSystem;
+import org.apache.flink.connectors.test.common.junit.annotations.TestEnv;
+import org.apache.flink.connectors.test.common.testsuites.SourceTestSuiteBase;
+import org.apache.flink.formats.avro.typeutils.AvroSchemaConverter;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.runtime.typeutils.RowDataSerializer;
+import org.apache.flink.util.FileUtils;
+
+import org.apache.avro.Schema;
+import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.io.DatumWriter;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Random;
+
+import static org.apache.flink.formats.avro.AvroBulkFormatTestUtils.ROW_TYPE;
+
+/** IT cases for {@link AbstractAvroBulkFormat}. */
+public class AvroBulkFormatITCase extends SourceTestSuiteBase<RowData> {
+
+ private static final RowDataSerializer SERIALIZER = new RowDataSerializer(ROW_TYPE);
+
+ @SuppressWarnings("unused")
+ @TestEnv
+ MiniClusterTestEnvironment flink = new MiniClusterTestEnvironment();
+
+ @SuppressWarnings("unused")
+ @ExternalSystem
+ EmptyExternalSystem externalSystem = new EmptyExternalSystem();
+
+ @SuppressWarnings("unused")
+ @ExternalContextFactory
+ AvroBulkFormatExternalContext.Factory oneBlockPerFile =
+ new AvroBulkFormatExternalContext.Factory(1);
+
+ @SuppressWarnings("unused")
+ @ExternalContextFactory
+ AvroBulkFormatExternalContext.Factory twoBlocksPerFile =
+ new AvroBulkFormatExternalContext.Factory(2);
+
+ @Override
+ public void testTaskManagerFailure(
+ TestEnvironment testEnv,
+ ExternalContext<RowData> externalContext,
+ ClusterControllable controller)
+ throws Exception {
+ // this test needs an unbounded source so we currently ignore this,
+ // add this test back once an unbounded file source is added
+ }
+
+ /** An empty {@link TestResource} which does nothing. */
+ public static class EmptyExternalSystem implements TestResource {
+
+ @Override
+ public void startUp() throws Exception {}
+
+ @Override
+ public void tearDown() throws Exception {}
+ }
+
+ /**
+ * {@link ExternalContext} for the IT case. It prepares avro test files and creates avro
+ * sources.
+ */
+ public static class AvroBulkFormatExternalContext implements ExternalContext<RowData> {
+
+ private final Path tmpDir;
+ private final int blocksPerFile;
+ private int index;
+
+ private AvroBulkFormatExternalContext(int blocksPerFile) {
+ try {
+ this.tmpDir = Files.createTempDirectory("avro-bulk-format-it-case");
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "Encountered exception when creating temp directory for tests", e);
+ }
+ this.blocksPerFile = blocksPerFile;
+ this.index = 0;
+ }
+
+ @Override
+ public Source<RowData, ?, ?> createSource(Boundedness boundedness) {
+ AvroBulkFormatTestUtils.TestingAvroBulkFormat format =
+ new AvroBulkFormatTestUtils.TestingAvroBulkFormat();
+ return FileSource.forBulkFileFormat(
+ format, org.apache.flink.core.fs.Path.fromLocalFile(tmpDir.toFile()))
+ .build();
+ }
+
+ @Override
+ public SourceSplitDataWriter<RowData> createSourceSplitDataWriter() {
+ File file = Paths.get(tmpDir.toString(), String.valueOf(index)).toFile();
+ AvroBulkFormatSourceSplitDataWriter writer;
+ try {
+ file.createNewFile();
+ writer = new AvroBulkFormatSourceSplitDataWriter(new FileOutputStream(file));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ index++;
+ return writer;
+ }
+
+ @Override
+ public List<RowData> generateTestData(int splitIndex, long seed) {
+ Random random = new Random(seed);
+ List<RowData> data = new ArrayList<>();
+ for (int i = 0; i < blocksPerFile; i++) {
+ data.add(getBinaryRow(randomString(4, random), randomString(8, random)));
+ data.add(getBinaryRow(randomString(16, random), null));
+ data.add(getBinaryRow(randomString(32, random), randomString(1024, random)));
+ }
+ return data;
+ }
+
+ @Override
+ public void close() throws Exception {
+ FileUtils.deleteDirectory(tmpDir.toFile());
+ }
+
+ private StringData randomString(int len, Random random) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < len; i++) {
+ builder.append((char) (random.nextInt(26) + 'a'));
+ }
+ return StringData.fromString(builder.toString());
+ }
+
+ private RowData getBinaryRow(Object... values) {
+ GenericRowData genericRowData = new GenericRowData(values.length);
+ for (int i = 0; i < values.length; i++) {
+ genericRowData.setField(i, values[i]);
+ }
+ BinaryRowData binaryRowData = SERIALIZER.toBinaryRow(genericRowData);
+ return SERIALIZER.copy(binaryRowData);
+ }
+
+ /** Factory to create {@link AvroBulkFormatExternalContext}. */
+ public static class Factory implements ExternalContext.Factory<RowData> {
+
+ private final int blocksPerFile;
+
+ public Factory(int blocksPerFile) {
+ this.blocksPerFile = blocksPerFile;
+ }
+
+ @Override
+ public ExternalContext<RowData> createExternalContext() {
+ return new AvroBulkFormatExternalContext(blocksPerFile);
+ }
+ }
+ }
+
+ private static class AvroBulkFormatSourceSplitDataWriter
+ implements SourceSplitDataWriter<RowData> {
+
+ private final Schema schema;
+ private final RowDataToAvroConverters.RowDataToAvroConverter converter;
+ private final DataFileWriter<GenericRecord> dataFileWriter;
+
+ private AvroBulkFormatSourceSplitDataWriter(FileOutputStream out) throws IOException {
+ schema = AvroSchemaConverter.convertToSchema(ROW_TYPE);
+ converter = RowDataToAvroConverters.createConverter(ROW_TYPE);
+
+ DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(schema);
+ dataFileWriter = new DataFileWriter<>(datumWriter);
+ dataFileWriter.create(schema, out);
+ dataFileWriter.setSyncInterval(128);
+ }
+
+ @Override
+ public void writeRecords(Collection<RowData> records) {
+ for (RowData rowData : records) {
+ try {
+ dataFileWriter.append((GenericRecord) converter.convert(schema, rowData));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @Override
+ public void close() throws Exception {
+ dataFileWriter.close();
+ }
+ }
+}
diff --git a/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroBulkFormatTest.java b/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroBulkFormatTest.java
new file mode 100644
index 0000000..9080efa
--- /dev/null
+++ b/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroBulkFormatTest.java
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.formats.avro;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.file.src.FileSourceSplit;
+import org.apache.flink.connector.file.src.reader.BulkFormat;
+import org.apache.flink.connector.file.src.util.CheckpointedPosition;
+import org.apache.flink.connector.file.src.util.RecordAndPosition;
+import org.apache.flink.core.fs.Path;
+import org.apache.flink.formats.avro.typeutils.AvroSchemaConverter;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.util.FileUtils;
+import org.apache.flink.util.StringUtils;
+
+import org.apache.avro.Schema;
+import org.apache.avro.file.DataFileConstants;
+import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.io.DatumWriter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import static org.apache.flink.formats.avro.AvroBulkFormatTestUtils.ROW_TYPE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link AbstractAvroBulkFormat}. */
+public class AvroBulkFormatTest {
+
+ private static final List<RowData> TEST_DATA =
+ Arrays.asList(
+ // -------- batch 0, block start 186 --------
+ GenericRowData.of(
+ StringData.fromString("AvroBulk"), StringData.fromString("FormatTest")),
+ GenericRowData.of(
+ StringData.fromString("Apache"), StringData.fromString("Flink")),
+ GenericRowData.of(
+ StringData.fromString(
+ "永和九年,岁在癸丑,暮春之初,会于会稽山阴之兰亭,修禊事也。群贤毕至,少"
+ + "长咸集。此地有崇山峻岭,茂林修竹,又有清流激湍,映带左右。引"
+ + "以为流觞曲水,列坐其次。虽无丝竹管弦之盛,一觞一咏,亦足以畅"
+ + "叙幽情。"),
+ StringData.fromString("")),
+ // -------- batch 1, block start 547 --------
+ GenericRowData.of(
+ StringData.fromString("File"), StringData.fromString("Format")),
+ GenericRowData.of(
+ null,
+ StringData.fromString(
+ "This is a string with English, 中文 and even 🍎🍌🍑🥝🍍🥭🍐")),
+ // -------- batch 2, block start 659 --------
+ GenericRowData.of(
+ StringData.fromString("block with"),
+ StringData.fromString("only one record"))
+ // -------- file length 706 --------
+ );
+ private static final List<Integer> BLOCK_STARTS = Arrays.asList(186, 547, 659);
+
+ private File tmpFile;
+
+ @BeforeEach
+ public void before() throws IOException {
+ tmpFile = Files.createTempFile("avro-bulk-format-test", ".avro").toFile();
+ tmpFile.createNewFile();
+ FileOutputStream out = new FileOutputStream(tmpFile);
+
+ Schema schema = AvroSchemaConverter.convertToSchema(ROW_TYPE);
+ RowDataToAvroConverters.RowDataToAvroConverter converter =
+ RowDataToAvroConverters.createConverter(ROW_TYPE);
+
+ DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(schema);
+ DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter);
+ dataFileWriter.create(schema, out);
+ dataFileWriter.setSyncInterval(64);
+
+ for (RowData rowData : TEST_DATA) {
+ dataFileWriter.append((GenericRecord) converter.convert(schema, rowData));
+ }
+
+ dataFileWriter.close();
+ }
+
+ @AfterEach
+ public void after() throws IOException {
+ FileUtils.deleteFileOrDirectory(tmpFile);
+ }
+
+ @Test
+ public void testReadWholeFileWithOneSplit() throws IOException {
+ AvroBulkFormatTestUtils.TestingAvroBulkFormat bulkFormat =
+ new AvroBulkFormatTestUtils.TestingAvroBulkFormat();
+ assertSplit(
+ bulkFormat,
+ Collections.singletonList(
+ new SplitInfo(
+ 0,
+ tmpFile.length(),
+ Arrays.asList(
+ new BatchInfo(0, 3),
+ new BatchInfo(3, 5),
+ new BatchInfo(5, 6)))));
+ }
+
+ @Test
+ public void testReadWholeFileWithMultipleSplits() throws IOException {
+ AvroBulkFormatTestUtils.TestingAvroBulkFormat bulkFormat =
+ new AvroBulkFormatTestUtils.TestingAvroBulkFormat();
+ long splitLength = tmpFile.length() / 3;
+ assertSplit(
+ bulkFormat,
+ Arrays.asList(
+ new SplitInfo(
+ 0, splitLength, Collections.singletonList(new BatchInfo(0, 3))),
+ new SplitInfo(splitLength, splitLength * 2, Collections.emptyList()),
+ new SplitInfo(
+ splitLength * 2,
+ tmpFile.length(),
+ Arrays.asList(new BatchInfo(3, 5), new BatchInfo(5, 6)))));
+ }
+
+ @Test
+ public void testSplitsAtCriticalLocations() throws IOException {
+ AvroBulkFormatTestUtils.TestingAvroBulkFormat bulkFormat =
+ new AvroBulkFormatTestUtils.TestingAvroBulkFormat();
+ assertSplit(
+ bulkFormat,
+ Arrays.asList(
+ // ends just before the new block
+ new SplitInfo(
+ BLOCK_STARTS.get(0) - DataFileConstants.SYNC_SIZE,
+ BLOCK_STARTS.get(1) - DataFileConstants.SYNC_SIZE,
+ Collections.singletonList(new BatchInfo(0, 3))),
+ // ends just at the beginning of new block
+ new SplitInfo(
+ BLOCK_STARTS.get(1) - DataFileConstants.SYNC_SIZE,
+ BLOCK_STARTS.get(2) - DataFileConstants.SYNC_SIZE + 1,
+ Arrays.asList(new BatchInfo(3, 5), new BatchInfo(5, 6)))));
+ }
+
+ @Test
+ public void testRestoreReader() throws IOException {
+ AvroBulkFormatTestUtils.TestingAvroBulkFormat bulkFormat =
+ new AvroBulkFormatTestUtils.TestingAvroBulkFormat();
+ long splitLength = tmpFile.length() / 3;
+ String splitId = UUID.randomUUID().toString();
+
+ FileSourceSplit split =
+ new FileSourceSplit(
+ splitId, new Path(tmpFile.toString()), splitLength * 2, tmpFile.length());
+ BulkFormat.Reader<RowData> reader = bulkFormat.createReader(new Configuration(), split);
+ long offset1 = assertBatch(reader, new BatchInfo(3, 5));
+ assertBatch(reader, new BatchInfo(5, 6));
+ assertThat(reader.readBatch()).isNull();
+ reader.close();
+
+ split =
+ new FileSourceSplit(
+ splitId,
+ new Path(tmpFile.toString()),
+ splitLength * 2,
+ tmpFile.length(),
+ StringUtils.EMPTY_STRING_ARRAY,
+ new CheckpointedPosition(offset1, 1));
+ reader = bulkFormat.restoreReader(new Configuration(), split);
+ long offset2 = assertBatch(reader, new BatchInfo(3, 5), 1);
+ assertBatch(reader, new BatchInfo(5, 6));
+ assertThat(reader.readBatch()).isNull();
+ reader.close();
+
+ assertThat(offset2).isEqualTo(offset1);
+ }
+
+ private void assertSplit(
+ AvroBulkFormatTestUtils.TestingAvroBulkFormat bulkFormat, List<SplitInfo> splitInfos)
+ throws IOException {
+ for (SplitInfo splitInfo : splitInfos) {
+ FileSourceSplit split =
+ new FileSourceSplit(
+ UUID.randomUUID().toString(),
+ new Path(tmpFile.toString()),
+ splitInfo.start,
+ splitInfo.end - splitInfo.start);
+ BulkFormat.Reader<RowData> reader = bulkFormat.createReader(new Configuration(), split);
+ List<Long> offsets = new ArrayList<>();
+ for (BatchInfo batch : splitInfo.batches) {
+ offsets.add(assertBatch(reader, batch));
+ }
+ assertThat(reader.readBatch()).isNull();
+ for (int j = 1; j < offsets.size(); j++) {
+ assertThat(offsets.get(j - 1) < offsets.get(j)).isTrue();
+ }
+ reader.close();
+ }
+ }
+
+ private long assertBatch(BulkFormat.Reader<RowData> reader, BatchInfo batchInfo)
+ throws IOException {
+ return assertBatch(reader, batchInfo, 0);
+ }
+
+ private long assertBatch(
+ BulkFormat.Reader<RowData> reader, BatchInfo batchInfo, int initialSkipCount)
+ throws IOException {
+ long ret = -1;
+ int skipCount = initialSkipCount;
+ BulkFormat.RecordIterator<RowData> iterator = reader.readBatch();
+ for (RecordAndPosition<RowData> recordAndPos = iterator.next();
+ recordAndPos != null;
+ recordAndPos = iterator.next()) {
+ if (ret == -1) {
+ ret = recordAndPos.getOffset();
+ }
+ assertThat(recordAndPos.getRecord())
+ .isEqualTo(TEST_DATA.get(batchInfo.start + skipCount));
+ assertThat(recordAndPos.getOffset()).isEqualTo(ret);
+ skipCount++;
+ assertThat(recordAndPos.getRecordSkipCount()).isEqualTo(skipCount);
+ }
+ assertThat(skipCount).isEqualTo(batchInfo.end - batchInfo.start);
+ iterator.releaseBatch();
+ return ret;
+ }
+
+ private static class SplitInfo {
+ private final long start;
+ private final long end;
+ private final List<BatchInfo> batches;
+
+ private SplitInfo(long start, long end, List<BatchInfo> batches) {
+ this.start = start;
+ this.end = end;
+ this.batches = batches;
+ }
+ }
+
+ private static class BatchInfo {
+ private final int start;
+ private final int end;
+
+ private BatchInfo(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+}
diff --git a/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroBulkFormatTestUtils.java b/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroBulkFormatTestUtils.java
new file mode 100644
index 0000000..a688ff7
--- /dev/null
+++ b/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroBulkFormatTestUtils.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.formats.avro;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.connector.file.src.FileSourceSplit;
+import org.apache.flink.formats.avro.typeutils.AvroSchemaConverter;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+
+/** Testing utils for tests related to {@link AbstractAvroBulkFormat}. */
+public class AvroBulkFormatTestUtils {
+
+ public static final RowType ROW_TYPE =
+ RowType.of(
+ false,
+ new LogicalType[] {
+ DataTypes.STRING().getLogicalType(), DataTypes.STRING().getLogicalType()
+ },
+ new String[] {"a", "b"});
+
+ /** {@link AbstractAvroBulkFormat} for tests. */
+ public static class TestingAvroBulkFormat
+ extends AbstractAvroBulkFormat<GenericRecord, RowData, FileSourceSplit> {
+
+ private transient GenericRecord reusedAvroRecord;
+ private transient AvroToRowDataConverters.AvroToRowDataConverter converter;
+
+ protected TestingAvroBulkFormat() {
+ super(AvroSchemaConverter.convertToSchema(ROW_TYPE));
+ }
+
+ @Override
+ protected void open(FileSourceSplit split) {
+ reusedAvroRecord = new GenericData.Record(readerSchema);
+ converter = AvroToRowDataConverters.createRowConverter(ROW_TYPE);
+ }
+
+ @Override
+ protected RowData convert(GenericRecord record) {
+ return record == null ? null : (RowData) converter.convert(record);
+ }
+
+ @Override
+ protected GenericRecord createReusedAvroRecord() {
+ return reusedAvroRecord;
+ }
+
+ @Override
+ public TypeInformation<RowData> getProducedType() {
+ return InternalTypeInfo.of(ROW_TYPE);
+ }
+ }
+}
diff --git a/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroFilesystemITCase.java b/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroFilesystemITCase.java
index b89c2be..f240024 100644
--- a/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroFilesystemITCase.java
+++ b/flink-formats/flink-avro/src/test/java/org/apache/flink/formats/avro/AvroFilesystemITCase.java
@@ -30,7 +30,7 @@ import java.util.List;
import static org.apache.avro.file.DataFileConstants.NULL_CODEC;
-/** ITCase to test avro format for {@link AvroFileSystemFormatFactory} in batch mode. */
+/** ITCase to test avro format for {@link AvroFileFormatFactory} in batch mode. */
@RunWith(Parameterized.class)
public class AvroFilesystemITCase extends BatchFileSystemITCaseBase {
@@ -46,11 +46,6 @@ public class AvroFilesystemITCase extends BatchFileSystemITCaseBase {
}
@Override
- public boolean supportsReadingMetadata() {
- return false;
- }
-
- @Override
public String[] formatProperties() {
List<String> ret = new ArrayList<>();
ret.add("'format'='avro'");