Lab 5.3: Build It — MiniTezCluster Integration Test
Lab type: Build It — a full mini-cluster integration test from scratch
Estimated time: 150 min
Tez module: tez-tests
Key class (you create): org.apache.tez.test.TestMiniClusterOrderedWordCount
Background
In Lab 5.1 you read TestTezJobs. Now you write your own integration test in the same tree, using the
same MiniDFSCluster + MiniTezCluster boilerplate, but you drive the DAG through the client API
(TezClient.submitDAG → DAGClient → DAGStatus) so you can assert on the final DAG state and on
TezCounters — not just "the job returned 0."
This is the exact artifact a committer expects to accompany a DAG-level change: a test that submits a
real multi-vertex DAG on the mini-cluster, waits for completion, asserts DAGStatus.State.SUCCEEDED, and
verifies a specific, derivable counter value. You will build it against the real OrderedWordCount
example DAG (Tokenizer → Summation → Sorter) so every class and counter is real and every command runs.
Note on "using the umbilical properly": In a mini-cluster test you do not touch the task↔AM umbilical (
TaskCommunicator/TezTaskUmbilicalProtocol) directly — that is wired automatically when the AM launches real containers. What you do control is the client-side protocol:TezClient→ AM submission, andDAGClientpolling for status and counters. Manipulating the umbilical by hand is a unit-test concern (seeTestUmbilicalin the runtime tests); at the integration tier you let the real umbilical carry heartbeats and events, and you assert on the results it produces.
Why This Lab Matters for Contributors
- "Add a DAG feature" or "fix a DAG-level bug" patches are only merged with an integration test that asserts the observable outcome — final state plus a counter. This lab produces exactly that.
- Driving
TezClient/DAGClientyourself (rather than theTool.runshortcut) teaches the client protocol you will need to debug submission and status bugs. - Deriving the expected counter by hand — instead of asserting whatever came out — is the discipline that separates a test that proves something from one that merely runs.
Prerequisites
-
Labs 5.1 and 5.2 complete;
export TEZ_SRC=/path/to/tez;mvn install -DskipTests -qsucceeded. -
You can run
TestTezJobs#testOrderedWordCountto green. - Read Counters & Diagnostics and DAG Client.
Confirm the APIs you will call are real before you write against them:
cd "$TEZ_SRC"
grep -n "public synchronized DAGClient submitDAG" tez-api/src/main/java/org/apache/tez/client/TezClient.java
grep -n "waitForCompletionWithStatusUpdates\|getVertexStatus" tez-api/src/main/java/org/apache/tez/dag/api/client/DAGClient.java
grep -n "public enum State\|getState\|getDAGCounters" tez-api/src/main/java/org/apache/tez/dag/api/client/DAGStatus.java
grep -n "createDAG" tez-examples/src/main/java/org/apache/tez/examples/OrderedWordCount.java
Step-by-Step Tasks
Step 1: Understand the DAG you will submit
OrderedWordCount.createDAG(...) builds a 3-vertex DAG (tez-examples):
Tokenizer --(OrderedPartitionedKV edge)--> Summation --(OrderedPartitionedKV edge)--> Sorter
(reads MRInput text, (sums counts, (no-op,
emits <word, 1>) emits <count, word>) writes MROutput)
Its signature (verify with the grep above):
public static DAG createDAG(TezConfiguration tezConf, String inputPath, String outputPath,
int numPartitions, boolean disableSplitGrouping, boolean isGenerateSplitInClient, String dagName)
The vertex names are the string constants "Tokenizer", "Summation", "Sorter" (the sink on Sorter is
named "Output"). You will read a per-vertex counter off the "Tokenizer" vertex.
Step 2: Derive the counter you will assert
Reuse the standard input generator (identical to TestTezJobs.generateOrderedWordCountInput): for each
i in 1..10, write the word a_i exactly (11 - i) times to each of two files. Total word
occurrences:
sum over i=1..10 of (11 - i) = 10 + 9 + ... + 1 = 55 per file
55 * 2 files = 110 total tokens
The Tokenizer emits one <word, 1> record per token, so the Tokenizer vertex's
TaskCounter.OUTPUT_RECORDS must equal 110. That is your hand-derived, deterministic assertion —
not a value you copied from a passing run.
Tip: Deriving the number first is the whole point. If your test just asserts "whatever the run produced," it can't catch a regression that changes the count. A committer will ask "how do you know 110 is right?" — and "I did the arithmetic" is the only acceptable answer.
Step 3: Create the test file
Create tez-tests/src/test/java/org/apache/tez/test/TestMiniClusterOrderedWordCount.java. No pom.xml
changes are needed — tez-tests already depends on tez-examples, hadoop-minicluster, and JUnit.
Here is the complete, runnable test:
/*
* 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.tez.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.EnumSet;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.apache.tez.client.TezClient;
import org.apache.tez.common.counters.TaskCounter;
import org.apache.tez.common.counters.TezCounter;
import org.apache.tez.common.counters.TezCounters;
import org.apache.tez.dag.api.DAG;
import org.apache.tez.dag.api.TezConfiguration;
import org.apache.tez.dag.api.client.DAGClient;
import org.apache.tez.dag.api.client.DAGStatus;
import org.apache.tez.dag.api.client.StatusGetOpts;
import org.apache.tez.dag.api.client.VertexStatus;
import org.apache.tez.examples.OrderedWordCount;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
public class TestMiniClusterOrderedWordCount {
private static MiniTezCluster tezCluster;
private static MiniDFSCluster dfsCluster;
private static Configuration conf = new Configuration();
private static FileSystem remoteFs;
private static final String TEST_ROOT_DIR =
"target" + Path.SEPARATOR + TestMiniClusterOrderedWordCount.class.getName() + "-tmpDir";
@BeforeClass
public static void setup() throws IOException {
// 1) Real in-JVM HDFS.
conf.set(MiniDFSCluster.HDFS_MINIDFS_BASEDIR, TEST_ROOT_DIR);
dfsCluster = new MiniDFSCluster.Builder(conf).numDataNodes(2).format(true).racks(null).build();
remoteFs = dfsCluster.getFileSystem();
// 2) Real in-JVM YARN + Tez AM. Must come AFTER remoteFs exists so fs.defaultFS points at HDFS.
tezCluster = new MiniTezCluster(TestMiniClusterOrderedWordCount.class.getName(), 1, 1, 1);
conf.set("fs.defaultFS", remoteFs.getUri().toString());
conf.setLong(TezConfiguration.TEZ_AM_SLEEP_TIME_BEFORE_EXIT_MILLIS, 500);
tezCluster.init(conf);
tezCluster.start();
}
@AfterClass
public static void tearDown() {
if (tezCluster != null) {
tezCluster.stop();
tezCluster = null;
}
if (dfsCluster != null) {
dfsCluster.shutdown();
dfsCluster = null;
}
}
/** Same input the canonical TestTezJobs uses: a_i written (11-i) times to EACH of two files. */
private static void generateInput(Path inputDir, FileSystem fs) throws IOException {
FSDataOutputStream f1 = fs.create(new Path(inputDir, "inPath1"));
FSDataOutputStream f2 = fs.create(new Path(inputDir, "inPath2"));
try {
for (int i = 1; i <= 10; ++i) {
String word = "a_" + i;
for (int j = 10; j >= i; --j) { // (11 - i) copies
f1.write(word.getBytes());
f1.writeChars("\t");
f2.write(word.getBytes());
f2.writeChars("\t");
}
}
f1.hsync();
f2.hsync();
} finally {
f1.close();
f2.close();
}
}
@Test(timeout = 120000)
public void testOrderedWordCountCountersAndState() throws Exception {
Path inputDir = new Path("/tmp/owc-mini-input");
Path outputDir = new Path("/tmp/owc-mini-output");
Path stagingDir = new Path("/tmp/owc-mini-staging");
remoteFs.mkdirs(inputDir);
remoteFs.mkdirs(stagingDir);
remoteFs.delete(outputDir, true); // MROutput requires a non-existent output dir
generateInput(inputDir, remoteFs);
TezConfiguration tezConf = new TezConfiguration(tezCluster.getConfig());
tezConf.set(TezConfiguration.TEZ_AM_STAGING_DIR, stagingDir.toString());
TezClient tezClient = TezClient.create("OwcMiniClusterTest", tezConf, true /* session */);
tezClient.start();
try {
tezClient.waitTillReady();
DAG dag = OrderedWordCount.createDAG(tezConf, inputDir.toString(), outputDir.toString(),
2 /* numPartitions */, false /* disableSplitGrouping */,
false /* generateSplitsInClient */, "OwcMiniClusterTest");
DAGClient dagClient = tezClient.submitDAG(dag);
DAGStatus dagStatus =
dagClient.waitForCompletionWithStatusUpdates(EnumSet.of(StatusGetOpts.GET_COUNTERS));
// Assertion 1: final DAG state.
assertEquals("DAG must succeed", DAGStatus.State.SUCCEEDED, dagStatus.getState());
// Assertion 2: DAG counters are present.
TezCounters dagCounters = dagStatus.getDAGCounters();
assertNotNull("DAG counters must be present", dagCounters);
// Assertion 3: derived per-vertex counter. Tokenizer emits one record per token; the input
// has sum(i=1..10)(11-i) = 55 tokens per file * 2 files = 110 tokens total.
VertexStatus tokenizerStatus =
dagClient.getVertexStatus("Tokenizer", EnumSet.of(StatusGetOpts.GET_COUNTERS));
assertNotNull(tokenizerStatus);
TezCounter tokenizerOutput =
tokenizerStatus.getVertexCounters().findCounter(TaskCounter.OUTPUT_RECORDS);
assertNotNull(tokenizerOutput);
assertEquals("Tokenizer must emit one record per input token", 110L, tokenizerOutput.getValue());
assertTrue("Sanity: DAG output records must be positive",
tokenizerOutput.getValue() > 0);
} finally {
remoteFs.delete(stagingDir, true);
tezClient.stop();
}
}
}
Step 4: Build and run
cd "$TEZ_SRC"
mvn install -DskipTests -q
mvn test -pl tez-tests -Dtest='TestMiniClusterOrderedWordCount#testOrderedWordCountCountersAndState' 2>&1 | tail -25
Expected tail:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
Step 5: Read status the way the test does
While the DAG runs, the client polls the AM for status. Watch it in the captured log:
grep -n "Submitting dag\|DAG State\|SUCCEEDED\|Tokenizer\|OUTPUT_RECORDS" \
tez-tests/target/surefire-reports/org.apache.tez.test.TestMiniClusterOrderedWordCount-output.txt | head
Confirm you can see the DAG reach SUCCEEDED and (with GET_COUNTERS) the counters being fetched. If you
drop StatusGetOpts.GET_COUNTERS, getDAGCounters() may return counters only for a finished DAG — the
flag forces counters into the status you wait on. This is a common integration-test bug: asking for a
counter you never requested.
Step 5b: The client polling protocol and DAG states
waitForCompletionWithStatusUpdates is a blocking poll loop: the DAGClient repeatedly asks the AM for
the current DAGStatus until the DAG reaches a terminal state. The DAGStatus.State values you can
observe (from DAGStatus.State) are:
| State | Meaning |
|---|---|
SUBMITTED | Returned from the RM only, before the AM is running |
RUNNING | The DAG is executing (INITING is folded into RUNNING) |
SUCCEEDED | Terminal — all vertices completed successfully |
KILLED | Terminal — the DAG was killed |
FAILED | Terminal — a vertex/task exhausted retries |
ERROR | Terminal — an internal error (a bug, like the VertexState.ERROR of Lab 5.2) |
You gate on SUCCEEDED. On any other terminal state, dagStatus.getDiagnostics() tells you why — that
is the first thing to print when an integration test fails:
if (dagStatus.getState() != DAGStatus.State.SUCCEEDED) {
System.err.println("DAG failed: " + dagStatus.getDiagnostics());
}
Passing EnumSet.of(StatusGetOpts.GET_COUNTERS) tells the AM to include counters in every status
response it sends back during the poll — without it, getDAGCounters() and getVertexCounters() can be
empty. This is the single most common "why are my counters null?" bug in a first integration test.
Step 6: Prove the counter assertion bites
Change 110L to 111L and re-run. The test must fail with:
java.lang.AssertionError: Tokenizer must emit one record per input token expected:<111> but was:<110>
Change it back. A test whose numeric assertion you have watched fail on a wrong value is a test you can
trust. Revert to 110L.
Step 7: Compare the two submission styles
You submitted via TezClient.submitDAG + DAGClient. TestTezJobs#testOrderedWordCount submits via
OrderedWordCount.run(...) (the Tool path). Write two or three sentences: which style lets you assert
on counters, which is terser, and when you would choose each.
Implementation Requirements / Deliverables
-
TestMiniClusterOrderedWordCount.javacreated intez-tests/src/test/java/org/apache/tez/test/, compiling and passing. -
The
@BeforeClasswiresMiniDFSClusterthenMiniTezCluster, withfs.defaultFSset afterremoteFsexists. -
Three assertions:
DAGStatus.State.SUCCEEDED; non-nullgetDAGCounters(); TokenizerOUTPUT_RECORDS == 110derived by hand (show the arithmetic in a comment). -
Evidence the counter assertion bites (Step 6: the
111Lfailure, then reverted). -
A short note comparing
submitDAG/DAGClientvs theTool.runpath (Step 7).
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
TezUncheckedException: TezAppJar ... not found | AM jar not built | mvn install -DskipTests first |
FileAlreadyExistsException on the output dir | MROutput refuses to overwrite | remoteFs.delete(outputDir, true) before submit (already in the test) |
getDAGCounters() returns null | You didn't request counters | Pass EnumSet.of(StatusGetOpts.GET_COUNTERS) to waitForCompletionWithStatusUpdates |
getVertexStatus("Tokenizer", ...) is null / NPE | Wrong vertex name | Names come from OrderedWordCount: "Tokenizer", "Summation", "Sorter" — grep to confirm |
| Tokenizer count is not 110 | Different input, or split grouping merged/duplicated reads | Use the exact generateInput above; keep disableSplitGrouping=false and re-derive if you change input |
| Test hangs then passes | serviceStop → waitForAppsToFinish() | Expected teardown behavior (Lab 5.1) |
IllegalStateException: session not started | Called submitDAG before start()/waitTillReady() | Start the session and waitTillReady() first (as shown) |
Stretch Goals
- Assert a DAG-level counter. From
dagStatus.getDAGCounters(), findTaskCounter.OUTPUT_RECORDSaggregated across all vertices, derive its expected value (Tokenizer 110 + Summation 10 distinct words- Sorter 10), and assert it. Reason carefully about grouping.
- Force a failure and assert diagnostics. Point the input at a non-existent path, submit, and assert
dagStatus.getState() == FAILEDand thatdagStatus.getDiagnostics()is non-empty. This is how you test the unhappy path. - Parameterize split grouping. Turn the test into a JUnit
@Parameterizedclass that runs withdisableSplitGroupingbothtrueandfalse, mirroring howTestTezJobscovers both. - Session reuse. Submit the DAG twice on the same started
TezClientsession and assert both succeed — proving you understand session vs per-DAG AM lifecycle.
Deeper Understanding
| # | Question |
|---|---|
| 1 | You wait with waitForCompletionWithStatusUpdates(EnumSet.of(GET_COUNTERS)). How does this differ from calling waitForCompletion() and then getDAGStatus(EnumSet.of(GET_COUNTERS))? Are the counters guaranteed present in both? |
| 2 | The timeout is 120000 ms for a ~110-token DAG. Why does a tiny DAG need a two-minute budget? (Hint: what is booted per @BeforeClass, and what happens at serviceStop?) |
| 3 | You created the client with TezClient.create(name, tezConf, true) — a session. What changes if you pass false (non-session)? Where does the AM live in each case, and how does that affect submitDAG? |
| 4 | If you mistype the vertex name in getVertexStatus("Tokeniser", ...), what happens — a clear error, or a silent null? How would you make the test fail loudly instead? |
| 5 | The DAG-level getDAGCounters() aggregates across all vertices, while getVertexStatus(...).getVertexCounters() is per-vertex. Which should you assert on when you want a precise number, and why is the per-vertex counter easier to derive by hand? |
Validation / Self-check
- Why must
conf.set("fs.defaultFS", ...)come afterremoteFs = dfsCluster.getFileSystem()? - Which API returns counters, and what must you pass to get them into the
DAGStatusyou wait on? - Show the arithmetic that yields
110for the Tokenizer'sOUTPUT_RECORDS. - What does "using the umbilical properly" mean at the integration tier, and why don't you touch it directly here?
- What is the difference between submitting via
TezClient.submitDAG/DAGClientand via the example'sTool.run? Which can assert on counters? - How did you prove your
110Lassertion actually guards against regressions? - Why does
MROutputrequire you to delete the output directory before submitting?
When your test passes, its counter assertion bites, and you can explain the client protocol, continue to Lab 5.4: Fix It — Un-Ignore a Flaky Test.