mirror of https://github.com/apache/nifi.git
NIFI-919 Adding SplitAvro processor that splits binary datafiles into smaller datafiles, or bare records.
- Adding documentation about bare record use, renaming Split Size to Output Size, and adding a test case with 0 records - Removing validators on properties that have allowable values, using positive integer validator for Output Size, and fixing type on processor description
This commit is contained in:
parent
1007999415
commit
b36841e213
|
@ -0,0 +1,370 @@
|
||||||
|
/*
|
||||||
|
* 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.nifi.processors.avro;
|
||||||
|
|
||||||
|
import org.apache.avro.file.CodecFactory;
|
||||||
|
import org.apache.avro.file.DataFileConstants;
|
||||||
|
import org.apache.avro.file.DataFileStream;
|
||||||
|
import org.apache.avro.file.DataFileWriter;
|
||||||
|
import org.apache.avro.generic.GenericDatumReader;
|
||||||
|
import org.apache.avro.generic.GenericDatumWriter;
|
||||||
|
import org.apache.avro.generic.GenericRecord;
|
||||||
|
import org.apache.avro.io.DatumWriter;
|
||||||
|
import org.apache.avro.io.Encoder;
|
||||||
|
import org.apache.avro.io.EncoderFactory;
|
||||||
|
import org.apache.nifi.annotation.behavior.SideEffectFree;
|
||||||
|
import org.apache.nifi.annotation.behavior.SupportsBatching;
|
||||||
|
import org.apache.nifi.annotation.documentation.CapabilityDescription;
|
||||||
|
import org.apache.nifi.annotation.documentation.Tags;
|
||||||
|
import org.apache.nifi.components.AllowableValue;
|
||||||
|
import org.apache.nifi.components.PropertyDescriptor;
|
||||||
|
import org.apache.nifi.flowfile.FlowFile;
|
||||||
|
import org.apache.nifi.processor.AbstractProcessor;
|
||||||
|
import org.apache.nifi.processor.ProcessContext;
|
||||||
|
import org.apache.nifi.processor.ProcessSession;
|
||||||
|
import org.apache.nifi.processor.ProcessorInitializationContext;
|
||||||
|
import org.apache.nifi.processor.Relationship;
|
||||||
|
import org.apache.nifi.processor.exception.ProcessException;
|
||||||
|
import org.apache.nifi.processor.io.InputStreamCallback;
|
||||||
|
import org.apache.nifi.processor.io.OutputStreamCallback;
|
||||||
|
import org.apache.nifi.processor.util.StandardValidators;
|
||||||
|
import org.apache.nifi.stream.io.BufferedOutputStream;
|
||||||
|
import org.apache.nifi.util.ObjectHolder;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@SideEffectFree
|
||||||
|
@SupportsBatching
|
||||||
|
@Tags({ "avro", "split" })
|
||||||
|
@CapabilityDescription("Splits a binary encoded Avro datafile into smaller files based on the configured Output Size. The Output Strategy determines if " +
|
||||||
|
"the smaller files will be Avro datafiles, or bare Avro records with metadata in the FlowFile attributes. The output will always be binary encoded.")
|
||||||
|
public class SplitAvro extends AbstractProcessor {
|
||||||
|
|
||||||
|
public static final String RECORD_SPLIT_VALUE = "Record";
|
||||||
|
public static final AllowableValue RECORD_SPLIT = new AllowableValue(RECORD_SPLIT_VALUE, RECORD_SPLIT_VALUE, "Split at Record boundaries");
|
||||||
|
|
||||||
|
public static final PropertyDescriptor SPLIT_STRATEGY = new PropertyDescriptor.Builder()
|
||||||
|
.name("Split Strategy")
|
||||||
|
.description("The strategy for splitting the incoming datafile. The Record strategy will read the incoming datafile by de-serializing each record.")
|
||||||
|
.required(true)
|
||||||
|
.allowableValues(RECORD_SPLIT)
|
||||||
|
.defaultValue(RECORD_SPLIT.getValue())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final PropertyDescriptor OUTPUT_SIZE = new PropertyDescriptor.Builder()
|
||||||
|
.name("Output Size")
|
||||||
|
.description("The number of Avro records to include per split file. In cases where the incoming file has less records than the Output Size, or " +
|
||||||
|
"when the total number of records does not divide evenly by the Output Size, it is possible to get a split file with less records.")
|
||||||
|
.addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR)
|
||||||
|
.required(true)
|
||||||
|
.defaultValue("1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final String DATAFILE_OUTPUT_VALUE = "Datafile";
|
||||||
|
public static final String BARE_RECORD_OUTPUT_VALUE = "Bare Record";
|
||||||
|
|
||||||
|
public static final AllowableValue DATAFILE_OUTPUT = new AllowableValue(DATAFILE_OUTPUT_VALUE, DATAFILE_OUTPUT_VALUE, "Avro's object container file format");
|
||||||
|
public static final AllowableValue BARE_RECORD_OUTPUT = new AllowableValue(BARE_RECORD_OUTPUT_VALUE, BARE_RECORD_OUTPUT_VALUE, "Bare Avro records");
|
||||||
|
|
||||||
|
public static final PropertyDescriptor OUTPUT_STRATEGY = new PropertyDescriptor.Builder()
|
||||||
|
.name("Output Strategy")
|
||||||
|
.description("Determines the format of the output. Either Avro Datafile, or bare record. Bare record output is only intended for use with systems " +
|
||||||
|
"that already require it, and shouldn't be needed for normal use.")
|
||||||
|
.required(true)
|
||||||
|
.allowableValues(DATAFILE_OUTPUT, BARE_RECORD_OUTPUT)
|
||||||
|
.defaultValue(DATAFILE_OUTPUT.getValue())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final PropertyDescriptor TRANSFER_METADATA = new PropertyDescriptor.Builder()
|
||||||
|
.name("Transfer Metadata")
|
||||||
|
.description("Whether or not to transfer metadata from the parent datafile to the children. If the Output Strategy is Bare Record, " +
|
||||||
|
"then the metadata will be stored as FlowFile attributes, otherwise it will be in the Datafile header.")
|
||||||
|
.required(true)
|
||||||
|
.allowableValues("true", "false")
|
||||||
|
.defaultValue("true")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final Relationship REL_ORIGINAL = new Relationship.Builder()
|
||||||
|
.name("original")
|
||||||
|
.description("The original FlowFile that was split. If the FlowFile fails processing, nothing will be sent to " +
|
||||||
|
"this relationship")
|
||||||
|
.build();
|
||||||
|
public static final Relationship REL_SPLIT = new Relationship.Builder()
|
||||||
|
.name("split")
|
||||||
|
.description("All new files split from the original FlowFile will be routed to this relationship")
|
||||||
|
.build();
|
||||||
|
public static final Relationship REL_FAILURE = new Relationship.Builder()
|
||||||
|
.name("failure")
|
||||||
|
.description("If a FlowFile fails processing for any reason (for example, the FlowFile is not valid Avro), " +
|
||||||
|
"it will be routed to this relationship")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Metadata keys that are not transferred to split files when output strategy is datafile
|
||||||
|
// Avro will write this key/values pairs on its own
|
||||||
|
static final Set<String> RESERVED_METADATA;
|
||||||
|
static {
|
||||||
|
Set<String> reservedMetadata = new HashSet<>();
|
||||||
|
reservedMetadata.add("avro.schema");
|
||||||
|
reservedMetadata.add("avro.codec");
|
||||||
|
RESERVED_METADATA = Collections.unmodifiableSet(reservedMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PropertyDescriptor> properties;
|
||||||
|
private Set<Relationship> relationships;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init(final ProcessorInitializationContext context) {
|
||||||
|
final List<PropertyDescriptor> properties = new ArrayList<>();
|
||||||
|
properties.add(SPLIT_STRATEGY);
|
||||||
|
properties.add(OUTPUT_SIZE);
|
||||||
|
properties.add(OUTPUT_STRATEGY);
|
||||||
|
properties.add(TRANSFER_METADATA);
|
||||||
|
this.properties = Collections.unmodifiableList(properties);
|
||||||
|
|
||||||
|
final Set<Relationship> relationships = new HashSet<>();
|
||||||
|
relationships.add(REL_ORIGINAL);
|
||||||
|
relationships.add(REL_SPLIT);
|
||||||
|
relationships.add(REL_FAILURE);
|
||||||
|
this.relationships = Collections.unmodifiableSet(relationships);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Relationship> getRelationships() {
|
||||||
|
return relationships;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
|
||||||
|
FlowFile flowFile = session.get();
|
||||||
|
if (flowFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int splitSize = context.getProperty(OUTPUT_SIZE).asInteger();
|
||||||
|
final boolean transferMetadata = context.getProperty(TRANSFER_METADATA).asBoolean();
|
||||||
|
|
||||||
|
SplitWriter splitWriter;
|
||||||
|
final String outputStrategy = context.getProperty(OUTPUT_STRATEGY).getValue();
|
||||||
|
switch (outputStrategy) {
|
||||||
|
case DATAFILE_OUTPUT_VALUE:
|
||||||
|
splitWriter = new DatafileSplitWriter(transferMetadata);
|
||||||
|
break;
|
||||||
|
case BARE_RECORD_OUTPUT_VALUE:
|
||||||
|
splitWriter = new BareRecordSplitWriter();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Splitter splitter;
|
||||||
|
final String splitStrategy = context.getProperty(SPLIT_STRATEGY).getValue();
|
||||||
|
switch (splitStrategy) {
|
||||||
|
case RECORD_SPLIT_VALUE:
|
||||||
|
splitter = new RecordSplitter(splitSize, transferMetadata);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final List<FlowFile> splits = splitter.split(session, flowFile, splitWriter);
|
||||||
|
session.transfer(splits, REL_SPLIT);
|
||||||
|
session.transfer(flowFile, REL_ORIGINAL);
|
||||||
|
} catch (ProcessException e) {
|
||||||
|
getLogger().error("Failed to split {} due to {}", new Object[] {flowFile, e.getMessage()}, e);
|
||||||
|
session.transfer(flowFile, REL_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Able to split an incoming Avro datafile into multiple smaller FlowFiles.
|
||||||
|
*/
|
||||||
|
private interface Splitter {
|
||||||
|
List<FlowFile> split(final ProcessSession session, final FlowFile originalFlowFile, final SplitWriter splitWriter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the incoming Avro datafile into batches of records by reading and de-serializing each record.
|
||||||
|
*/
|
||||||
|
private class RecordSplitter implements Splitter {
|
||||||
|
|
||||||
|
private final int splitSize;
|
||||||
|
private final boolean transferMetadata;
|
||||||
|
|
||||||
|
public RecordSplitter(final int splitSize, final boolean transferMetadata) {
|
||||||
|
this.splitSize = splitSize;
|
||||||
|
this.transferMetadata = transferMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FlowFile> split(final ProcessSession session, final FlowFile originalFlowFile, final SplitWriter splitWriter) {
|
||||||
|
final List<FlowFile> childFlowFiles = new ArrayList<>();
|
||||||
|
final ObjectHolder<GenericRecord> recordHolder = new ObjectHolder<>(null);
|
||||||
|
|
||||||
|
session.read(originalFlowFile, new InputStreamCallback() {
|
||||||
|
@Override
|
||||||
|
public void process(InputStream rawIn) throws IOException {
|
||||||
|
try (final InputStream in = new BufferedInputStream(rawIn);
|
||||||
|
final DataFileStream<GenericRecord> reader = new DataFileStream<>(in, new GenericDatumReader<GenericRecord>())) {
|
||||||
|
|
||||||
|
final ObjectHolder<String> codec = new ObjectHolder<>(reader.getMetaString(DataFileConstants.CODEC));
|
||||||
|
if (codec.get() == null) {
|
||||||
|
codec.set(DataFileConstants.NULL_CODEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
// while records are left, start a new split by spawning a FlowFile
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
FlowFile childFlowFile = session.create(originalFlowFile);
|
||||||
|
childFlowFile = session.write(childFlowFile, new OutputStreamCallback() {
|
||||||
|
@Override
|
||||||
|
public void process(OutputStream rawOut) throws IOException {
|
||||||
|
try (final BufferedOutputStream out = new BufferedOutputStream(rawOut)) {
|
||||||
|
splitWriter.init(reader, codec.get(), out);
|
||||||
|
|
||||||
|
// append to the current FlowFile until no more records, or splitSize is reached
|
||||||
|
int recordCount = 0;
|
||||||
|
while (reader.hasNext() && recordCount < splitSize) {
|
||||||
|
recordHolder.set(reader.next(recordHolder.get()));
|
||||||
|
splitWriter.write(recordHolder.get());
|
||||||
|
recordCount++;
|
||||||
|
}
|
||||||
|
splitWriter.flush();
|
||||||
|
} finally {
|
||||||
|
splitWriter.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// would prefer this to be part of the SplitWriter, but putting the metadata in FlowFile attributes
|
||||||
|
// can't be done inside of an OutputStream callback which is where the splitWriter is used
|
||||||
|
if (splitWriter instanceof BareRecordSplitWriter && transferMetadata) {
|
||||||
|
final Map<String,String> metadata = new HashMap<>();
|
||||||
|
for (String metaKey : reader.getMetaKeys()) {
|
||||||
|
metadata.put(metaKey, reader.getMetaString(metaKey));
|
||||||
|
}
|
||||||
|
childFlowFile = session.putAllAttributes(childFlowFile, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
childFlowFiles.add(childFlowFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return childFlowFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes records from the reader to the given output stream.
|
||||||
|
*/
|
||||||
|
private interface SplitWriter {
|
||||||
|
void init(final DataFileStream<GenericRecord> reader, final String codec, final OutputStream out) throws IOException;
|
||||||
|
void write(final GenericRecord datum) throws IOException;
|
||||||
|
void flush() throws IOException;
|
||||||
|
void close() throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a binary Avro Datafile to the OutputStream.
|
||||||
|
*/
|
||||||
|
private class DatafileSplitWriter implements SplitWriter {
|
||||||
|
|
||||||
|
private final boolean transferMetadata;
|
||||||
|
private DataFileWriter<GenericRecord> writer;
|
||||||
|
|
||||||
|
public DatafileSplitWriter(final boolean transferMetadata) {
|
||||||
|
this.transferMetadata = transferMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(final DataFileStream<GenericRecord> reader, final String codec, final OutputStream out) throws IOException {
|
||||||
|
writer = new DataFileWriter<>(new GenericDatumWriter<GenericRecord>());
|
||||||
|
|
||||||
|
if (transferMetadata) {
|
||||||
|
for (String metaKey : reader.getMetaKeys()) {
|
||||||
|
if (!RESERVED_METADATA.contains(metaKey)) {
|
||||||
|
writer.setMeta(metaKey, reader.getMeta(metaKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.setCodec(CodecFactory.fromString(codec));
|
||||||
|
writer.create(reader.getSchema(), out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(final GenericRecord datum) throws IOException {
|
||||||
|
writer.append(datum);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
writer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes bare Avro records to the OutputStream.
|
||||||
|
*/
|
||||||
|
private class BareRecordSplitWriter implements SplitWriter {
|
||||||
|
private Encoder encoder;
|
||||||
|
private DatumWriter<GenericRecord> writer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(final DataFileStream<GenericRecord> reader, final String codec, final OutputStream out) throws IOException {
|
||||||
|
writer = new GenericDatumWriter<>(reader.getSchema());
|
||||||
|
encoder = EncoderFactory.get().binaryEncoder(out, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(GenericRecord datum) throws IOException {
|
||||||
|
writer.write(datum, encoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
encoder.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,4 +13,5 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
org.apache.nifi.processors.avro.ConvertAvroToJSON
|
org.apache.nifi.processors.avro.ConvertAvroToJSON
|
||||||
org.apache.nifi.processors.avro.ExtractAvroMetadata
|
org.apache.nifi.processors.avro.ExtractAvroMetadata
|
||||||
|
org.apache.nifi.processors.avro.SplitAvro
|
||||||
|
|
|
@ -0,0 +1,317 @@
|
||||||
|
/*
|
||||||
|
* 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.nifi.processors.avro;
|
||||||
|
|
||||||
|
import org.apache.avro.Schema;
|
||||||
|
import org.apache.avro.file.DataFileStream;
|
||||||
|
import org.apache.avro.file.DataFileWriter;
|
||||||
|
import org.apache.avro.generic.GenericData;
|
||||||
|
import org.apache.avro.generic.GenericDatumReader;
|
||||||
|
import org.apache.avro.generic.GenericDatumWriter;
|
||||||
|
import org.apache.avro.generic.GenericRecord;
|
||||||
|
import org.apache.avro.io.DatumReader;
|
||||||
|
import org.apache.avro.io.Decoder;
|
||||||
|
import org.apache.avro.io.DecoderFactory;
|
||||||
|
import org.apache.nifi.stream.io.ByteArrayInputStream;
|
||||||
|
import org.apache.nifi.stream.io.ByteArrayOutputStream;
|
||||||
|
import org.apache.nifi.util.MockFlowFile;
|
||||||
|
import org.apache.nifi.util.TestRunner;
|
||||||
|
import org.apache.nifi.util.TestRunners;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TestSplitAvro {
|
||||||
|
|
||||||
|
static final String META_KEY1 = "metaKey1";
|
||||||
|
static final String META_KEY2 = "metaKey2";
|
||||||
|
static final String META_KEY3 = "metaKey3";
|
||||||
|
|
||||||
|
static final String META_VALUE1 = "metaValue1";
|
||||||
|
static final long META_VALUE2 = Long.valueOf(1234567);
|
||||||
|
static final String META_VALUE3 = "metaValue3";
|
||||||
|
|
||||||
|
private Schema schema;
|
||||||
|
private ByteArrayOutputStream users;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws IOException {
|
||||||
|
this.users = new ByteArrayOutputStream();
|
||||||
|
this.schema = new Schema.Parser().parse(new File("src/test/resources/user.avsc"));
|
||||||
|
createUsers(100, users);
|
||||||
|
}
|
||||||
|
|
||||||
|
void createUsers(final int numUsers, final ByteArrayOutputStream users) throws IOException {
|
||||||
|
final List<GenericRecord> userList = new ArrayList<>();
|
||||||
|
for (int i=0; i < numUsers; i++) {
|
||||||
|
final GenericRecord user = new GenericData.Record(schema);
|
||||||
|
user.put("name", "name" + i);
|
||||||
|
user.put("favorite_number", i);
|
||||||
|
userList.add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (final DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(new GenericDatumWriter<GenericRecord>(schema))) {
|
||||||
|
dataFileWriter.setMeta(META_KEY1, META_VALUE1);
|
||||||
|
dataFileWriter.setMeta(META_KEY2, META_VALUE2);
|
||||||
|
dataFileWriter.setMeta(META_KEY3, META_VALUE3.getBytes("UTF-8"));
|
||||||
|
|
||||||
|
dataFileWriter.create(schema, users);
|
||||||
|
for (GenericRecord user : userList) {
|
||||||
|
dataFileWriter.append(user);
|
||||||
|
}
|
||||||
|
dataFileWriter.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecordSplitWithNoIncomingRecords() throws IOException {
|
||||||
|
final TestRunner runner = TestRunners.newTestRunner(new SplitAvro());
|
||||||
|
|
||||||
|
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
createUsers(0, out);
|
||||||
|
|
||||||
|
runner.enqueue(out.toByteArray());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_SPLIT, 0);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_ORIGINAL, 1);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_FAILURE, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecordSplitDatafileOutputWithSingleRecords() throws IOException {
|
||||||
|
final TestRunner runner = TestRunners.newTestRunner(new SplitAvro());
|
||||||
|
|
||||||
|
runner.enqueue(users.toByteArray());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_SPLIT, 100);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_ORIGINAL, 1);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_FAILURE, 0);
|
||||||
|
|
||||||
|
final List<MockFlowFile> flowFiles = runner.getFlowFilesForRelationship(SplitAvro.REL_SPLIT);
|
||||||
|
checkDataFileSplitSize(flowFiles, 1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecordSplitDatafileOutputWithMultipleRecords() throws IOException {
|
||||||
|
final TestRunner runner = TestRunners.newTestRunner(new SplitAvro());
|
||||||
|
runner.setProperty(SplitAvro.OUTPUT_SIZE, "20");
|
||||||
|
|
||||||
|
runner.enqueue(users.toByteArray());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_SPLIT, 5);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_ORIGINAL, 1);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_FAILURE, 0);
|
||||||
|
|
||||||
|
final List<MockFlowFile> flowFiles = runner.getFlowFilesForRelationship(SplitAvro.REL_SPLIT);
|
||||||
|
checkDataFileSplitSize(flowFiles, 20, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecordSplitDatafileOutputWithSplitSizeLarger() throws IOException {
|
||||||
|
final TestRunner runner = TestRunners.newTestRunner(new SplitAvro());
|
||||||
|
runner.setProperty(SplitAvro.OUTPUT_SIZE, "200");
|
||||||
|
|
||||||
|
runner.enqueue(users.toByteArray());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_SPLIT, 1);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_ORIGINAL, 1);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_FAILURE, 0);
|
||||||
|
|
||||||
|
final List<MockFlowFile> flowFiles = runner.getFlowFilesForRelationship(SplitAvro.REL_SPLIT);
|
||||||
|
checkDataFileSplitSize(flowFiles, 100, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecordSplitDatafileOutputWithoutMetadata() throws IOException {
|
||||||
|
final TestRunner runner = TestRunners.newTestRunner(new SplitAvro());
|
||||||
|
runner.setProperty(SplitAvro.TRANSFER_METADATA, "false");
|
||||||
|
|
||||||
|
runner.enqueue(users.toByteArray());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_SPLIT, 100);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_ORIGINAL, 1);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_FAILURE, 0);
|
||||||
|
|
||||||
|
final List<MockFlowFile> flowFiles = runner.getFlowFilesForRelationship(SplitAvro.REL_SPLIT);
|
||||||
|
checkDataFileSplitSize(flowFiles, 1, false);
|
||||||
|
|
||||||
|
for (final MockFlowFile flowFile : flowFiles) {
|
||||||
|
try (final ByteArrayInputStream in = new ByteArrayInputStream(flowFile.toByteArray());
|
||||||
|
final DataFileStream<GenericRecord> reader = new DataFileStream<>(in, new GenericDatumReader<GenericRecord>())) {
|
||||||
|
Assert.assertFalse(reader.getMetaKeys().contains(META_KEY1));
|
||||||
|
Assert.assertFalse(reader.getMetaKeys().contains(META_KEY2));
|
||||||
|
Assert.assertFalse(reader.getMetaKeys().contains(META_KEY3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecordSplitBareOutputWithSingleRecords() throws IOException {
|
||||||
|
final TestRunner runner = TestRunners.newTestRunner(new SplitAvro());
|
||||||
|
runner.setProperty(SplitAvro.OUTPUT_STRATEGY, SplitAvro.BARE_RECORD_OUTPUT);
|
||||||
|
|
||||||
|
runner.enqueue(users.toByteArray());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_SPLIT, 100);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_ORIGINAL, 1);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_FAILURE, 0);
|
||||||
|
|
||||||
|
final List<MockFlowFile> flowFiles = runner.getFlowFilesForRelationship(SplitAvro.REL_SPLIT);
|
||||||
|
|
||||||
|
checkBareRecordsSplitSize(flowFiles, 1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecordSplitBareOutputWithMultipleRecords() throws IOException {
|
||||||
|
final TestRunner runner = TestRunners.newTestRunner(new SplitAvro());
|
||||||
|
runner.setProperty(SplitAvro.OUTPUT_STRATEGY, SplitAvro.BARE_RECORD_OUTPUT);
|
||||||
|
runner.setProperty(SplitAvro.OUTPUT_SIZE, "20");
|
||||||
|
|
||||||
|
runner.enqueue(users.toByteArray());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_SPLIT, 5);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_ORIGINAL, 1);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_FAILURE, 0);
|
||||||
|
|
||||||
|
final List<MockFlowFile> flowFiles = runner.getFlowFilesForRelationship(SplitAvro.REL_SPLIT);
|
||||||
|
|
||||||
|
checkBareRecordsSplitSize(flowFiles, 20, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecordSplitBareOutputWithoutMetadata() throws IOException {
|
||||||
|
final TestRunner runner = TestRunners.newTestRunner(new SplitAvro());
|
||||||
|
runner.setProperty(SplitAvro.OUTPUT_STRATEGY, SplitAvro.BARE_RECORD_OUTPUT);
|
||||||
|
runner.setProperty(SplitAvro.OUTPUT_SIZE, "20");
|
||||||
|
runner.setProperty(SplitAvro.TRANSFER_METADATA, "false");
|
||||||
|
|
||||||
|
runner.enqueue(users.toByteArray());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_SPLIT, 5);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_ORIGINAL, 1);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_FAILURE, 0);
|
||||||
|
|
||||||
|
final List<MockFlowFile> flowFiles = runner.getFlowFilesForRelationship(SplitAvro.REL_SPLIT);
|
||||||
|
|
||||||
|
checkBareRecordsSplitSize(flowFiles, 20, false);
|
||||||
|
|
||||||
|
for (final MockFlowFile flowFile : flowFiles) {
|
||||||
|
Assert.assertFalse(flowFile.getAttributes().containsKey(META_KEY1));
|
||||||
|
Assert.assertFalse(flowFile.getAttributes().containsKey(META_KEY2));
|
||||||
|
Assert.assertFalse(flowFile.getAttributes().containsKey(META_KEY3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure() throws IOException {
|
||||||
|
final TestRunner runner = TestRunners.newTestRunner(new SplitAvro());
|
||||||
|
runner.setProperty(SplitAvro.OUTPUT_SIZE, "200");
|
||||||
|
|
||||||
|
runner.enqueue("not avro".getBytes("UTF-8"));
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_SPLIT, 0);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_ORIGINAL, 0);
|
||||||
|
runner.assertTransferCount(SplitAvro.REL_FAILURE, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkBareRecordsSplitSize(final List<MockFlowFile> flowFiles, final int expectedRecordsPerSplit, final boolean checkMetadata) throws IOException {
|
||||||
|
for (final MockFlowFile flowFile : flowFiles) {
|
||||||
|
try (final ByteArrayInputStream in = new ByteArrayInputStream(flowFile.toByteArray())) {
|
||||||
|
final DatumReader<GenericRecord> reader = new GenericDatumReader<>(schema);
|
||||||
|
final Decoder decoder = DecoderFactory.get().binaryDecoder(in, null);
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
GenericRecord record = reader.read(null, decoder);
|
||||||
|
try {
|
||||||
|
while (record != null) {
|
||||||
|
Assert.assertNotNull(record.get("name"));
|
||||||
|
Assert.assertNotNull(record.get("favorite_number"));
|
||||||
|
count++;
|
||||||
|
record = reader.read(record, decoder);
|
||||||
|
}
|
||||||
|
} catch (EOFException eof) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
Assert.assertEquals(expectedRecordsPerSplit, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkMetadata) {
|
||||||
|
Assert.assertTrue(flowFile.getAttributes().containsKey(META_KEY1));
|
||||||
|
Assert.assertTrue(flowFile.getAttributes().containsKey(META_KEY2));
|
||||||
|
Assert.assertTrue(flowFile.getAttributes().containsKey(META_KEY3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkDataFileSplitSize(List<MockFlowFile> flowFiles, int expectedRecordsPerSplit, boolean checkMetadata) throws IOException {
|
||||||
|
for (final MockFlowFile flowFile : flowFiles) {
|
||||||
|
try (final ByteArrayInputStream in = new ByteArrayInputStream(flowFile.toByteArray());
|
||||||
|
final DataFileStream<GenericRecord> reader = new DataFileStream<>(in, new GenericDatumReader<GenericRecord>())) {
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
GenericRecord record = null;
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
record = reader.next(record);
|
||||||
|
Assert.assertNotNull(record.get("name"));
|
||||||
|
Assert.assertNotNull(record.get("favorite_number"));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
Assert.assertEquals(expectedRecordsPerSplit, count);
|
||||||
|
|
||||||
|
if (checkMetadata) {
|
||||||
|
Assert.assertEquals(META_VALUE1, reader.getMetaString(META_KEY1));
|
||||||
|
Assert.assertEquals(META_VALUE2, reader.getMetaLong(META_KEY2));
|
||||||
|
Assert.assertEquals(META_VALUE3, new String(reader.getMeta(META_KEY3), "UTF-8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkDataFileTotalSize(List<MockFlowFile> flowFiles, int expectedTotalRecords) throws IOException {
|
||||||
|
int count = 0;
|
||||||
|
for (final MockFlowFile flowFile : flowFiles) {
|
||||||
|
try (final ByteArrayInputStream in = new ByteArrayInputStream(flowFile.toByteArray());
|
||||||
|
final DataFileStream<GenericRecord> reader = new DataFileStream<>(in, new GenericDatumReader<GenericRecord>())) {
|
||||||
|
|
||||||
|
GenericRecord record = null;
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
record = reader.next(record);
|
||||||
|
Assert.assertNotNull(record.get("name"));
|
||||||
|
Assert.assertNotNull(record.get("favorite_number"));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.assertEquals(expectedTotalRecords, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue