Skip to content

Why Asgarde

Beam recommends handling errors with dead letter queues: the errors are caught in the flow and sent, via side outputs, to a dedicated sink (a file, a table, a topic…) instead of failing the job.

With plain Beam, every step needs its own error handling code, and all the failures must be gathered at the end:

WithFailures.Result<PCollection<String>, Failure> result1 = input
.apply("Map", MapElements
.into(TypeDescriptors.strings())
.via((String word) -> word + "Test")
.exceptionsInto(TypeDescriptor.of(Failure.class))
.exceptionsVia(exElt -> Failure.from("Map", exElt)));
WithFailures.Result<PCollection<String>, Failure> result2 = result1.output()
.apply("FlatMap", FlatMapElements
.into(TypeDescriptors.strings())
.via((String line) -> Arrays.asList(line.split(" ")))
.exceptionsInto(TypeDescriptor.of(Failure.class))
.exceptionsVia(exElt -> Failure.from("FlatMap", exElt)));
final PCollectionTuple result3 = result2.output()
.apply("Word count", ParDo.of(wordCountFn)
.withOutputTags(wordCountFn.getOutputTag(), TupleTagList.of(wordCountFn.getFailuresTag())));
final PCollection<Integer> output = result3.get(wordCountFn.getOutputTag());
final PCollection<Failure> allFailures = PCollectionList
.of(result1.failures())
.and(result2.failures())
.and(result3.get(wordCountFn.getFailuresTag()))
.apply(Flatten.pCollections());

Plus, for each custom DoFn, the tuple tags and the try/catch block:

public class WordCountFn extends DoFn<String, Integer> {
private final TupleTag<Integer> outputTag = new TupleTag<Integer>() {};
private final TupleTag<Failure> failuresTag = new TupleTag<Failure>() {};
@ProcessElement
public void processElement(ProcessContext ctx) {
try {
ctx.output(1 / ctx.element().length());
} catch (Exception e) {
ctx.output(failuresTag, Failure.from("Word count", ctx.element(), e));
}
}
// Getters for the tags...
}

The problems with this approach:

  • The fluent style is lost: the output and the failures must be handled for each step.
  • The same technical code is repeated everywhere: exceptionsInto/exceptionsVia, tuple tags, try/catch blocks.
  • All the failures must be concatenated manually at the end.
  • The code is verbose and error-prone.
final WithFailures.Result<PCollection<Integer>, Failure> result = CollectionComposer.of(input)
.apply("Map", MapElements.into(TypeDescriptors.strings()).via((String word) -> word + "Test"))
.apply("FlatMap", FlatMapElements
.into(TypeDescriptors.strings())
.via((String line) -> Arrays.asList(line.split(" "))))
.apply("Word count", MapElementFn.into(TypeDescriptors.integers()).via(word -> 1 / word.length()))
.getResult();

What Asgarde does for you:

  • Wraps the error handling logic of each step: try/catch blocks, tuple tags, exceptionsInto/exceptionsVia.
  • Keeps the fluent style of Beam while collecting the failures of all the steps in a single PCollection.
  • Gives access to the DoFn lifecycle (setup, start bundle, finish bundle, teardown) with simple actions.
  • Handles errors in filters, which is not available with the Beam Filter transform in Java.
  • Guarantees that the error handling itself can’t make the job fail, and counts the failures per step with Beam metrics.