blob: 845dcb892ed87a462ff108166f4323186f99b117 [file] [view]
---
title: "Testing Unbounded Pipelines in Apache Beam"
date: 2016-10-20 10:00:00 -0800
categories:
- blog
aliases:
- /blog/2016/10/20/test-stream.html
authors:
- tgroh
---
<!--
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.
-->
The Beam Programming Model unifies writing pipelines for Batch and Streaming
pipelines. Weve recently introduced a new PTransform to write tests for
pipelines that will be run over unbounded datasets and must handle out-of-order
and delayed data.
<!--more-->
Watermarks, Windows and Triggers form a core part of the Beam programming model
-- they respectively determine how your data are grouped, when your input is
complete, and when to produce results. This is true for all pipelines,
regardless of if they are processing bounded or unbounded inputs. If youre not
familiar with watermarks, windowing, and triggering in the Beam model,
[Streaming 101](https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-101)
and [Streaming 102](https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-102)
are an excellent place to get started. A key takeaway from
these articles: in realistic streaming scenarios with intermittent failures and
disconnected users, data can arrive out of order or be delayed. Beams
primitives provide a way for users to perform useful, powerful, and correct
computations in spite of these challenges.
As Beam pipeline authors, we need comprehensive tests that cover crucial
failure scenarios and corner cases to gain real confidence that a pipeline is
ready for production. The existing testing infrastructure within the Beam SDKs
permits tests to be written which examine the contents of a Pipeline at
execution time. However, writing unit tests for pipelines that may receive
late data or trigger multiple times has historically ranged from complex to
not possible, as pipelines that read from unbounded sources do not shut down
without external intervention, while pipelines that read from bounded sources
exclusively cannot test behavior with late data nor most speculative triggers.
Without additional tools, pipelines that use custom triggers and handle
out-of-order data could not be easily tested.
This blog post introduces our new framework for writing tests for pipelines that
handle delayed and out-of-order data in the context of the LeaderBoard pipeline
from the Mobile Gaming example series.
## LeaderBoard and the Mobile Gaming Example
[LeaderBoard](https://github.com/apache/beam/blob/master/examples/java/src/main/java/org/apache/beam/examples/complete/game/LeaderBoard.java#L177)
is part of the [Beam mobile gaming examples](https://github.com/apache/beam/tree/master/examples/java/src/main/java/org/apache/beam/examples/complete/game)
(and [walkthroughs](/get-started/mobile-gaming-example/))
which produces a continuous accounting of user and team scores. User scores are
calculated over the lifetime of the program, while team scores are calculated
within fixed windows with a default duration of one hour. The LeaderBoard
pipeline produces speculative and late panes as appropriate, based on the
configured triggering and allowed lateness of the pipeline. The expected outputs
of the LeaderBoard pipeline vary depending on when elements arrive in relation
to the watermark and the progress of processing time, which could not previously
be controlled within a test.
## Writing Deterministic Tests to Emulate Nondeterminism
The Beam testing infrastructure provides the
[PAssert](https://beam.apache.org/releases/javadoc/{{< param release_latest >}}/org/apache/beam/sdk/testing/PAssert.html)
methods, which assert properties about the contents of a PCollection from within
a pipeline. We have expanded this infrastructure to include
[TestStream](https://github.com/apache/beam/blob/master/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestStream.java),
which is a PTransform that performs a series of events, consisting of adding
additional elements to a pipeline, advancing the watermark of the TestStream,
and advancing the pipeline processing time clock. TestStream permits tests which
observe the effects of triggers on the output a pipeline produces.
While executing a pipeline that reads from a TestStream, the read waits for all
of the consequences of each event to complete before continuing on to the next
event, ensuring that when processing time advances, triggers that are based on
processing time fire as appropriate. With this transform, the effect of
triggering and allowed lateness can be observed on a pipeline, including
reactions to speculative and late panes and dropped data.
## Element Timings
Elements arrive either before, with, or after the watermark, which categorizes
them into the "early", "on-time", and "late" divisions. "Late" elements can be
further subdivided into "unobservably", "observably", and "droppably" late,
depending on the window to which they are assigned and the maximum allowed
lateness, as specified by the windowing strategy. Elements that arrive with
these timings are emitted into panes, which can be "EARLY", "ON-TIME", or
"LATE", depending on the position of the watermark when the pane was emitted.
Using TestStream, we can write tests that demonstrate that speculative panes are
output after their trigger condition is met, that the advancing of the watermark
causes the on-time pane to be produced, and that late-arriving data produces
refinements when it arrives before the maximum allowed lateness, and is dropped
after.
The following examples demonstrate how you can use TestStream to provide a
sequence of events to the Pipeline, where the arrival of elements is interspersed
with updates to the watermark and the advance of processing time. Each of these
events runs to completion before additional events occur.
In the diagrams, the time at which events occurred in "real" (event) time
progresses as the graph moves to the right. The time at which the pipeline
receives them progresses as the graph goes upwards. The watermark is represented
by the squiggly red line, and each starburst is the firing of a trigger and the
associated pane.
<img class="center-block" src="/images/blog/test-stream/elements-all-on-time.png" alt="Elements on the Event and Processing time axes, with the Watermark and produced panes" width="442">
### Everything arrives on-time
For example, if we create a TestStream where all the data arrives before the
watermark and provide the result PCollection as input to the CalculateTeamScores
PTransform:
{{< highlight java >}}
TestStream<GameActionInfo> createEvents = TestStream.create(AvroCoder.of(GameActionInfo.class))
.addElements(new GameActionInfo("sky", "blue", 12, new Instant(0L)),
                new GameActionInfo("navy", "blue", 3, new Instant(0L)),
                new GameActionInfo("navy", "blue", 3, new Instant(0L).plus(Duration.standardMinutes(3))))
   // Move the watermark past the end the end of the window
.advanceWatermarkTo(new Instant(0L).plus(TEAM_WINDOW_DURATION)
                                .plus(Duration.standardMinutes(1)))
.advanceWatermarkToInfinity();
PCollection<KV<String, Integer>> teamScores = p.apply(createEvents)
.apply(new CalculateTeamScores(TEAM_WINDOW_DURATION, ALLOWED_LATENESS));
{{< /highlight >}}
we can then assert that the result PCollection contains elements that arrived:
<img class="center-block" src="/images/blog/test-stream/elements-all-on-time.png" alt="Elements all arrive before the watermark, and are produced in the on-time pane" width="442">
{{< highlight java >}}
// Only one value is emitted for the blue team
PAssert.that(teamScores)
.inWindow(window)
.containsInAnyOrder(KV.of("blue", 18));
p.run();
{{< /highlight >}}
### Some elements are late, but arrive before the end of the window
We can also add data to the TestStream after the watermark, but before the end
of the window (shown below to the left of the red watermark), which demonstrates
"unobservably late" data - that is, data that arrives late, but is promoted by
the system to be on time, as it arrives before the watermark passes the end of
the window
{{< highlight java >}}
TestStream<GameActionInfo> createEvents = TestStream.create(AvroCoder.of(GameActionInfo.class))
.addElements(new GameActionInfo("sky", "blue", 3, new Instant(0L)),
        new GameActionInfo("navy", "blue", 3, new Instant(0L).plus(Duration.standardMinutes(3))))
   // Move the watermark up to "near" the end of the window
.advanceWatermarkTo(new Instant(0L).plus(TEAM_WINDOW_DURATION)
                                .minus(Duration.standardMinutes(1)))
.addElements(new GameActionInfo("sky", "blue", 12, Duration.ZERO))
.advanceWatermarkToInfinity();
PCollection<KV<String, Integer>> teamScores = p.apply(createEvents)
.apply(new CalculateTeamScores(TEAM_WINDOW_DURATION, ALLOWED_LATENESS));
{{< /highlight >}}
<img class="center-block" src="/images/blog/test-stream/elements-unobservably-late.png" alt="An element arrives late, but before the watermark passes the end of the window, and is produced in the on-time pane" width="442">
{{< highlight java >}}
// Only one value is emitted for the blue team
PAssert.that(teamScores)
.inWindow(window)
.containsInAnyOrder(KV.of("blue", 18));
p.run();
{{< /highlight >}}
### Elements are late, and arrive after the end of the window
By advancing the watermark farther in time before adding the late data, we can
demonstrate the triggering behavior that causes the system to emit an on-time
pane, and then after the late data arrives, a pane that refines the result.
{{< highlight java >}}
TestStream<GameActionInfo> createEvents = TestStream.create(AvroCoder.of(GameActionInfo.class))
.addElements(new GameActionInfo("sky", "blue", 3, new Instant(0L)),
          new GameActionInfo("navy", "blue", 3, new Instant(0L).plus(Duration.standardMinutes(3))))
    // Move the watermark up to "near" the end of the window
.advanceWatermarkTo(new Instant(0L).plus(TEAM_WINDOW_DURATION)
                                 .minus(Duration.standardMinutes(1)))
.addElements(new GameActionInfo("sky", "blue", 12, Duration.ZERO))
.advanceWatermarkToInfinity();
PCollection<KV<String, Integer>> teamScores = p.apply(createEvents)
.apply(new CalculateTeamScores(TEAM_WINDOW_DURATION, ALLOWED_LATENESS));
{{< /highlight >}}
<img class="center-block" src="/images/blog/test-stream/elements-observably-late.png" alt="Elements all arrive before the watermark, and are produced in the on-time pane" width="442">
{{< highlight java >}}
// An on-time pane is emitted with the events that arrived before the window closed
PAssert.that(teamScores)
.inOnTimePane(window)
.containsInAnyOrder(KV.of("blue", 6));
// The final pane contains the late refinement
PAssert.that(teamScores)
.inFinalPane(window)
.containsInAnyOrder(KV.of("blue", 18));
p.run();
{{< /highlight >}}
### Elements are late, and after the end of the window plus the allowed lateness
If we push the watermark even further into the future, beyond the maximum
configured allowed lateness, we can demonstrate that the late element is dropped
by the system.
{{< highlight java >}}
TestStream<GameActionInfo> createEvents = TestStream.create(AvroCoder.of(GameActionInfo.class))
.addElements(new GameActionInfo("sky", "blue", 3, Duration.ZERO),
         new GameActionInfo("navy", "blue", 3, Duration.standardMinutes(3)))
    // Move the watermark up to "near" the end of the window
.advanceWatermarkTo(new Instant(0).plus(TEAM_WINDOW_DURATION)
                                        .plus(ALLOWED_LATENESS)
                                        .plus(Duration.standardMinutes(1)))
.addElements(new GameActionInfo(
                     "sky",
                     "blue",
                     12,
                     new Instant(0).plus(TEAM_WINDOW_DURATION).minus(Duration.standardMinutes(1))))
.advanceWatermarkToInfinity();
PCollection<KV<String, Integer>> teamScores = p.apply(createEvents)
.apply(new CalculateTeamScores(TEAM_WINDOW_DURATION, ALLOWED_LATENESS));
{{< /highlight >}}
<img class="center-block" src="/images/blog/test-stream/elements-droppably-late.png" alt="Elements all arrive before the watermark, and are produced in the on-time pane" width="442">
{{< highlight java >}}
// An on-time pane is emitted with the events that arrived before the window closed
PAssert.that(teamScores)
.inWindow(window)
.containsInAnyOrder(KV.of("blue", 6));
p.run();
{{< /highlight >}}
### Elements arrive before the end of the window, and some processing time passes
Using additional methods, we can demonstrate the behavior of speculative
triggers by advancing the processing time of the TestStream. If we add elements
to an input PCollection, occasionally advancing the processing time clock, and
apply `CalculateUserScores`
{{< highlight java >}}
TestStream<GameActionInfo> createEvents = TestStream.create(AvroCoder.of(GameActionInfo.class))
   .addElements(new GameActionInfo("scarlet", "red", 3, new Instant(0L)),
               new GameActionInfo("scarlet", "red", 2, new Instant(0L).plus(Duration.standardMinutes(1))))
.advanceProcessingTime(Duration.standardMinutes(12))
   .addElements(new GameActionInfo("oxblood", "red", 2, new Instant(0L)).plus(Duration.standardSeconds(22)),
               new GameActionInfo("scarlet", "red", 4, new Instant(0L).plus(Duration.standardMinutes(2))))
.advanceProcessingTime(Duration.standardMinutes(15))
.advanceWatermarkToInfinity();
PCollection<KV<String, Integer>> userScores =
   p.apply(createEvents).apply(new CalculateUserScores(ALLOWED_LATENESS));
{{< /highlight >}}
<img class="center-block" src="/images/blog/test-stream/elements-processing-speculative.png" alt="Elements all arrive before the watermark, and are produced in the on-time pane" width="442">
{{< highlight java >}}
PAssert.that(userScores)
.inEarlyGlobalWindowPanes()
.containsInAnyOrder(KV.of("scarlet", 5),
                        KV.of("scarlet", 9),
KV.of("oxblood", 2));
p.run();
{{< /highlight >}}
## TestStream - Under the Hood
TestStream relies on a pipeline concept weve introduced, called quiescence, to
utilize the existing runner infrastructure while providing guarantees about when
a root transform will called by the runner. This consists of properties about
pending elements and triggers, namely:
* No trigger is permitted to fire but has not fired
* All elements are either buffered in state or cannot progress until a side input becomes available
Simplified, this means that, in the absence of an advancement in input
watermarks or processing time, or additional elements being added to the
pipeline, the pipeline will not make progress. Whenever the TestStream PTransform
performs an action, the runner must not reinvoke the same instance until the
pipeline has quiesced. This ensures that the events specified by TestStream
happen "in-order", which ensures that input watermarks and the system clock do
not advance ahead of the elements they hoped to hold up.
The DirectRunner has been modified to use quiescence as the signal that it
should add more work to the Pipeline, and the implementation of TestStream in
that runner uses this fact to perform a single output per event. The DirectRunner
implementation also directly controls the runners system clock, ensuring that
tests will complete promptly even if there is a multi-minute processing time
trigger located within the pipeline.
The TestStream transform is supported in the DirectRunner. For most users, tests
written using TestPipeline and PAsserts will automatically function while using
TestStream.
## Summary
The addition of TestStream alongside window and pane-specific matchers in PAssert
has enabled the testing of Pipelines which produce speculative and late panes.
This permits tests for all styles of pipeline to be expressed directly within the
Java SDK. If you have questions or comments, wed love to hear them on the
[mailing lists](/get-started/support/).