I am learning Apache Beam and trying to implement something similar to distcp. I use FileIO.read().filepattern() to get the input files, but while writing with FileIO.write, the files get coalesced sometimes.
Knowing the partition count before job execution is not possible.
PCollection<MatchResult.Metadata> pCollection = pipeline.apply(this.name(), FileIO.match().filepattern(path()))
.apply(FileIO.readMatches())
.apply(name(), FileIO.<FileIO.ReadableFile>write()
.via(FileSink.create())
.to(path()));
Code for Sink
@AutoValue
public abstract static class FileSink implements FileIO.Sink<FileIO.ReadableFile> {
private OutputStream outputStream;
public static FileSink create() {
return new AutoValue_FileIOOperator_FileSink();
}
@Override
public void open(WritableByteChannel channel) throws IOException {
outputStream = Channels.newOutputStream(channel);
}
@Override
public void write(FileIO.ReadableFile element) throws IOException {
try (final InputStream inputStream = Channels.newInputStream(element.open())) {
IOUtils.copy(inputStream, outputStream);
}
}
@Override
public void flush() throws IOException {
outputStream.flush();
}
}
You can use FileIO.writeDynamic
and specify in .by
how you want to write them. For example, if you have unique keys you can use .by(KV::getKey)
and each key element will be written to a separate file. Otherwise, the criterion can be the hash of the row, etc. also you can tune .withNaming
at will. As a demo:
p.apply("Create Data", Create.of(KV.of("one", "this is row 1"), KV.of("two", "this is row 2"), KV.of("three", "this is row 3"), KV.of("four", "this is row 4")))
.apply(FileIO.<String, KV<String, String>>writeDynamic()
.by(KV::getKey)
.withDestinationCoder(StringUtf8Coder.of())
.via(Contextful.fn(KV::getValue), TextIO.sink())
.to(output)
.withNaming(key -> FileIO.Write.defaultNaming("file-" + key, ".txt")));
This will write the four elements into four files:
$ mvn compile -e exec:java \
-Dexec.mainClass=com.dataflow.samples.OneRowOneFile \
-Dexec.args="--project=$PROJECT \
--output="output/" \
--runner=DirectRunner"
$ ls output/
file-four-00001-of-00003.txt file-one-00002-of-00003.txt file-three-00002-of-00003.txt file-two-00002-of-00003.txt
$ cat output/file-four-00001-of-00003.txt
this is row 4
Full code:
package com.dataflow.samples;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.FileIO;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.Description;
import org.apache.beam.sdk.options.Validation;
import org.apache.beam.sdk.transforms.Contextful;
import org.apache.beam.sdk.transforms.Create;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
public abstract class OneRowOneFile {
public interface Options extends PipelineOptions {
@Validation.Required
@Description("Output Path i.e. gs://BUCKET/path/to/output/folder")
String getOutput();
void setOutput(String s);
}
public static void main(String[] args) {
OneRowOneFile.Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(OneRowOneFile.Options.class);
Pipeline p = Pipeline.create(options);
String output = options.getOutput();
p.apply("Create Data", Create.of(KV.of("one", "this is row 1"), KV.of("two", "this is row 2"), KV.of("three", "this is row 3"), KV.of("four", "this is row 4")))
.apply(FileIO.<String, KV<String, String>>writeDynamic()
.by(KV::getKey)
.withDestinationCoder(StringUtf8Coder.of())
.via(Contextful.fn(KV::getValue), TextIO.sink())
.to(output)
.withNaming(key -> FileIO.Write.defaultNaming("file-" + key, ".txt")));
p.run().waitUntilFinish();
}
}
Let me know if that works with your custom sink, too.