Lab 5.1: MiniTezCluster and TestOrderedWordCount
Lab type: Read & Run
Estimated time: 90 min
Tez modules: tez-tests, tez-examples
Key classes: org.apache.tez.test.MiniTezCluster, org.apache.tez.test.TestTezJobs
Background
An integration test boots real subsystems and runs a real DAG; a unit test mocks everything and
drives one class. This lab is your first hands-on encounter with the integration tier. You will read the
MiniTezCluster harness, trace exactly how the canonical TestTezJobs#testOrderedWordCount wires a
MiniDFSCluster and a MiniTezCluster together in @BeforeClass, run it for real, and read its logs.
MiniTezCluster extends MiniYARNCluster (Hadoop). Inside one JVM it starts an in-process YARN
ResourceManager and NodeManager(s); the test additionally starts a MiniDFSCluster (NameNode +
DataNodes) so the DAG reads and writes real HDFS. The Tez ApplicationMaster (DAGAppMaster) then runs as
an ordinary YARN application inside that mini-YARN. No external Hadoop install is required.
Note: The name is a trap.
TestOrderedWordCount(tez-tests/src/main/java/org/apache/tez/mapreduce/examples/TestOrderedWordCount.java) is a runnableToolexample, not a JUnit test — it lives insrc/mainand has no@Testmethods. The JUnit test that actually boots the mini-cluster and runs ordered word count isTestTezJobs#testOrderedWordCountinsrc/test, and the DAG it submits isorg.apache.tez.examples.OrderedWordCountfromtez-examples.TestSecureShuffledoes invoke theTestOrderedWordCountexample directly (viawordCount.run(args)) — that's the one place the example is exercised from a test.
Why This Lab Matters for Contributors
- Almost every DAG-level bug fix must ship an integration test in exactly this shape. If you cannot read
TestTezJobs, you cannot add a test a committer will accept. - Understanding what the mini-cluster boots — and what it costs — is the difference between a 3-second unit test and a 40-second integration test. Committers push back hard on using the heavy tier for logic that a unit test could prove.
- The
@BeforeClassDFS+Tez wiring here is copy-paste boilerplate you will reuse verbatim in Lab 5.3.
Prerequisites
-
Level 4 complete (you understand
VertexImpl,DAGImpl, and the DAG model). -
export TEZ_SRC=/path/to/tezpointing at your real checkout. -
cd "$TEZ_SRC" && mvn install -DskipTests -qhas succeeded (the AM jar must exist on disk, or the mini-cluster throwsTezUncheckedException). - Read the Testing Framework and YARN Integration deep dives.
cd "$TEZ_SRC"
find . -name "MiniTezCluster.java" # tez-tests/src/test/java/.../test/MiniTezCluster.java
find . -name "TestTezJobs.java" # tez-tests/src/test/java/.../test/TestTezJobs.java
find . -name "OrderedWordCount.java" -path "*examples*" # tez-examples/src/main/java/.../examples/OrderedWordCount.java
Step-by-Step Tasks
Step 1: Read the MiniTezCluster lifecycle
grep -n "extends MiniYARNCluster\|public MiniTezCluster\|serviceInit\|serviceStart\|serviceStop\|APPJAR\|TEZ_LIB_URIS\|waitForAppsToFinish" \
"$TEZ_SRC"/tez-tests/src/test/java/org/apache/tez/test/MiniTezCluster.java
Open the file and answer, citing the method you found it in (not a line number):
| # | Question | Where to look |
|---|---|---|
| 1 | What superclass does MiniTezCluster extend, and which Hadoop class does that pull in? | class declaration; import ... MiniYARNCluster |
| 2 | What does APPJAR resolve to, and what happens in serviceInit if the jar file does not exist? | JarFinder.getJar(DAGAppMaster.class); the if (!appJarLocalFile.exists()) block throwing TezUncheckedException |
| 3 | In serviceInit, which config keys make the AM use cluster Hadoop libs and disable node blacklisting? | TEZ_USE_CLUSTER_HADOOP_LIBS, TEZ_AM_NODE_BLACKLISTING_ENABLED |
| 4 | How does the AM jar get onto HDFS, and which config points the AM at it? | fs.copyFromLocalFile(...) then conf.set(TezConfiguration.TEZ_LIB_URIS, ...) |
| 5 | What does serviceStop do before super.serviceStop(), and how long can it wait? | waitForAppsToFinish(); TEZ_TEST_MINI_CLUSTER_APP_WAIT_ON_SHUTDOWN_SECS (default 30) |
The constructor you will use most:
// From MiniTezCluster.java — (testName, numNodeManagers, numLocalDirs, numLogDirs)
public MiniTezCluster(String testName, int noOfNMs, int numLocalDirs, int numLogDirs) {
super(testName, noOfNMs, numLocalDirs, numLogDirs);
}
Tip:
serviceStopcallingwaitForAppsToFinish()is why a mini-cluster test can appear to "hang" for up to 30 seconds at teardown when a DAG did not finish cleanly. It is polling YARN for still-running apps and then killing them. This is a real source of slow test suites — remember it for Lab 5.4.
Step 2: Trace the @BeforeClass in TestTezJobs
grep -n "MiniDFSCluster\|MiniTezCluster\|@BeforeClass\|@AfterClass\|remoteFs\|localFs\|fs.defaultFS\|TEZ_AM_SLEEP_TIME_BEFORE_EXIT" \
"$TEZ_SRC"/tez-tests/src/test/java/org/apache/tez/test/TestTezJobs.java | head -30
The setup is the reusable boilerplate. It looks like this in the source (TestTezJobs, setup()):
@BeforeClass
public static void setup() throws IOException {
localFs = FileSystem.getLocal(conf);
try {
conf.set(MiniDFSCluster.HDFS_MINIDFS_BASEDIR, TEST_ROOT_DIR);
dfsCluster = new MiniDFSCluster.Builder(conf).numDataNodes(2).format(true).racks(null).build();
remoteFs = dfsCluster.getFileSystem();
} catch (IOException io) {
throw new RuntimeException("problem starting mini dfs cluster", io);
}
if (mrrTezCluster == null) {
mrrTezCluster = new MiniTezCluster(TestTezJobs.class.getName(), 1, 1, 1);
conf.set("fs.defaultFS", remoteFs.getUri().toString()); // use HDFS
conf.setLong(TezConfiguration.TEZ_AM_SLEEP_TIME_BEFORE_EXIT_MILLIS, 500);
mrrTezCluster.init(conf);
mrrTezCluster.start();
}
}
Answer:
| # | Question |
|---|---|
| 1 | How many DataNodes does the MiniDFSCluster start with? How many NodeManagers does the MiniTezCluster start with? |
| 2 | Which line makes Tez use HDFS instead of the local FS, and why must it come after remoteFs is created? |
| 3 | The mrrTezCluster == null guard means the cluster is created once per what — per class, or per method? |
| 4 | In @AfterClass tearDown(), what is the shutdown order — Tez cluster or DFS cluster first? (Grep for tearDown.) |
Step 3: Trace the test body
grep -n "public void testOrderedWordCount\|generateOrderedWordCountInput\|verifyOutput\|OrderedWordCount job\|job.run" \
"$TEZ_SRC"/tez-tests/src/test/java/org/apache/tez/test/TestTezJobs.java
The test (TestTezJobs, testOrderedWordCount()) does exactly this:
@Test(timeout = 60000)
public void testOrderedWordCount() throws Exception {
String inputDirStr = "/tmp/owc-input/";
Path inputDir = new Path(inputDirStr);
Path stagingDirPath = new Path("/tmp/owc-staging-dir");
remoteFs.mkdirs(inputDir);
remoteFs.mkdirs(stagingDirPath);
generateOrderedWordCountInput(inputDir, remoteFs); // writes input to HDFS
String outputDirStr = "/tmp/owc-output/";
Path outputDir = new Path(outputDirStr);
TezConfiguration tezConf = new TezConfiguration(mrrTezCluster.getConfig());
tezConf.set(TezConfiguration.TEZ_AM_STAGING_DIR, stagingDirPath.toString());
OrderedWordCount job = new OrderedWordCount();
Assert.assertTrue("OrderedWordCount failed",
job.run(tezConf, new String[]{"-counter", inputDirStr, outputDirStr, "2"}, null) == 0);
verifyOutput(outputDir, remoteFs); // asserts the ordered output
}
Note what the assertions actually check. The primary gate is job.run(...) == 0 (the DAG succeeded).
Then verifyOutput → verifyOrderedWordCountOutput reads the output file and asserts the counts are
correct and in order. Read generateOrderedWordCountInput and confirm this arithmetic yourself:
- For each
iin1..10, the worda_iis written(11 - i)times to each of two files. - So
a_10appears once per file,a_1ten times per file. Combined count ofa_iacross both files is(11 - i) * 2. verifyOrderedWordCountOutputasserts exactlyAssert.assertEquals((long)(11 - currentCounter) * 2, ...)walkingcurrentCounterfrom 10 down to 1.
| # | Question |
|---|---|
| 1 | What are the three vertices of the DAG being submitted? (Read OrderedWordCount.createDAG in tez-examples: TOKENIZER, SUMMATION, SORTER.) |
| 2 | The DAG is submitted with job.run(...). Does that return counters? If you wanted to assert on a TezCounter, which API would you need instead? (Preview of Lab 5.3: TezClient.submitDAG → DAGClient → DAGStatus.getDAGCounters().) |
| 3 | Where is the input written — HDFS or local FS? Where does testOrderedWordCountDisableSplitGrouping write it instead, and what two extra args does it pass? |
Step 3b: What this integration test exercises that a unit test cannot
This is the conceptual payoff of the lab. Contrast the two tiers on the same VertexImpl:
| Concern | Unit test (TestVertexImpl, Lab 5.2) | Integration test (TestTezJobs, this lab) |
|---|---|---|
VertexImpl state transitions | Yes — driven by hand-fired VertexEvents | Yes — but driven by real task completions |
| Real container launch + task execution | No (all mocked) | Yes (real YARN containers run the processors) |
Shuffle over the network / ShuffleHandler | No | Yes (the NM aux-service moves data between vertices) |
HDFS read/write, splits, MRInput/MROutput | No | Yes (real MiniDFSCluster) |
| AM ↔ task umbilical heartbeats and events | No | Yes (the real umbilical carries them) |
| Speed | seconds | tens of seconds to minutes |
An integration test is the only place the whole path is exercised end to end: split calculation,
container allocation, the umbilical, shuffle, counters, and the DAG state machine all interacting. That
coverage is expensive, which is exactly why you reserve it for behavior a unit test cannot prove. The
MiniDFSCluster here is not decoration — it gives the DAG a real filesystem so MRInput computes real
splits and MROutput writes real files that verifyOutput reads back.
Step 4: Run it for real
Build first (the mini-cluster needs the AM jar on disk), then run the single method:
cd "$TEZ_SRC"
mvn install -DskipTests -q
mvn test -pl tez-tests -Dtest='TestTezJobs#testOrderedWordCount' 2>&1 | tail -25
Expected shape of the result (the exact numbers/timings vary, but the last lines look like):
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
If you instead see Unable to find a test class or No tests were executed, you almost certainly ran
-Dtest=TestOrderedWordCount (the example) instead of -Dtest='TestTezJobs#testOrderedWordCount'.
Step 5: Read the logs and the report
Surefire captures per-test stdout/stderr and the mini-cluster's logs:
ls tez-tests/target/surefire-reports/
# Plain-text dump of everything the test JVM printed, incl. AM + YARN + DFS logs:
sed -n '1,60p' tez-tests/target/surefire-reports/org.apache.tez.test.TestTezJobs.txt
# Grep the captured output for the DAG lifecycle:
grep -n "Submitting dag\|DAG completed\|SUCCEEDED\|Starting MiniTezCluster" \
tez-tests/target/surefire-reports/org.apache.tez.test.TestTezJobs-output.txt | head
In the captured output you should be able to see, in order: Starting MiniTezCluster (from
MiniTezCluster.serviceStart), the DAG being submitted, task attempts running, and the DAG reaching
SUCCEEDED. This is the log trail you will grep in every future integration-test debugging session.
Step 6: Time it and locate the overhead
time mvn test -pl tez-tests -Dtest='TestTezJobs#testOrderedWordCount' -q 2>&1 | tail -3
Answer: where does the wall-clock time go — booting MiniDFSCluster + MiniTezCluster, running the DAG,
or the waitForAppsToFinish() teardown (up to 30s)? Cross-reference the timestamps in the
-output.txt log. This is why integration tests use @BeforeClass (boot once per class) rather than
@Before (boot per method).
Step 7: Survey the neighbourhood
grep -rl "MiniTezCluster" "$TEZ_SRC"/tez-tests/src/test/java/ | sort
Pick one that is not TestTezJobs — good choices are TestFaultTolerance (kills tasks/inputs),
TestRecovery (AM restart), or TestSecureShuffle (SSL shuffle). Read its @BeforeClass and one
@Test. Answer:
- What scenario does it cover that
testOrderedWordCountdoes not? - Does it reuse the same
MiniTezClusterinstance across methods (@BeforeClass), or start a fresh one per method (@Before, asTestSecureShuffledoes because it toggles SSL)?
Deliverables
- Written answers to the Step 1–3 question tables, each citing a real method name (not a line number).
-
A green run of
TestTezJobs#testOrderedWordCount, with the finalTests run: 1, Failures: 0line pasted in. -
The DAG-lifecycle log lines you grepped out of the Surefire
-output.txt. -
A one-paragraph note on where the wall-clock time went (Step 6) and why
@BeforeClassis used. -
A one-paragraph comparison of your Step 7 neighbour test vs
testOrderedWordCount.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
TezUncheckedException: TezAppJar ... not found. Exiting. | The AM jar isn't built; APPJAR = JarFinder.getJar(DAGAppMaster.class) can't find it | mvn install -DskipTests from the repo root first |
No tests were executed | You ran the src/main example, not the JUnit test | Use -Dtest='TestTezJobs#testOrderedWordCount' |
| Test hangs ~30s at the end then passes | serviceStop → waitForAppsToFinish() polling YARN | Expected; lower TEZ_TEST_MINI_CLUSTER_APP_WAIT_ON_SHUTDOWN_SECS only if you understand the trade-off |
BindException / port in use | A previous mini-cluster JVM didn't die | Kill stale surefire JVMs; mini-cluster binds ephemeral ports, so a leaked process is the usual culprit |
| Stale / inconsistent NameNode errors | Old target/*-tmpDir state | mvn clean on tez-tests; the builder uses format(true) but leftover dirs can still confuse |
OutOfMemoryError during the run | Default Surefire heap too small for real YARN+HDFS | Increase the module's Surefire argLine heap, or run just the one method |
Stretch Goals
- Add a counter assertion. Copy
testOrderedWordCount's setup into a throwaway method, but submit the DAG viaTezClient.submitDAG(OrderedWordCount.createDAG(...))and assertdagClient.getDAGStatus(EnumSet.of(StatusGetOpts.GET_COUNTERS)).getDAGCounters()is non-null. This is the bridge to Lab 5.3. - Turn on DEBUG for one package. Add a
log4j/slf4joverride soorg.apache.tez.dag.applogs at DEBUG, re-run, and find where the DAG transitions toRUNNINGthenSUCCEEDEDin the log. - Read
TestSecureShuffle. It is the one test that drives theTestOrderedWordCountexample directly (wordCount.run(args)). Trace how it constructsargsand why it starts a freshMiniTezClusterper@Before(it toggles SSL between runs).
Deeper Understanding
| # | Question |
|---|---|
| 1 | serviceInit sets TEZ_USE_CLUSTER_HADOOP_LIBS=true. Why does the mini-cluster use cluster Hadoop libs instead of shipping its own, and what would break if the AM jar (APPJAR) were built against a different Hadoop version? |
| 2 | The test writes input to the HDFS paths /tmp/owc-input/ (absolute HDFS paths), while testOrderedWordCountDisableSplitGrouping uses TEST_ROOT_DIR + "/tmp/..." on the local FS. Why does one live on HDFS and the other on the local FS? (Hint: which FS is fs.defaultFS in each case?) |
| 3 | serviceInit disables AM node blacklisting (TEZ_AM_NODE_BLACKLISTING_ENABLED=false). Why would blacklisting cause scheduling problems in a one-NodeManager mini-cluster? |
| 4 | If you added a second @Test to TestTezJobs, would it get a fresh MiniTezCluster or reuse the one from @BeforeClass? What must each test do to avoid polluting the next? |
| 5 | The MiniDFSCluster.Builder uses numDataNodes(2). What does replication across two DataNodes let a test exercise that a single DataNode could not? |
Validation / Self-check
- What class does
MiniTezClusterextend, and what does that superclass boot? - Why does
-Dtest=TestOrderedWordCountrun zero tests, and what is the correct command? - In
TestTezJobs.setup(), what is the exact wiring order ofMiniDFSCluster,remoteFs,fs.defaultFS, andMiniTezCluster? Why does that order matter? - Where is the input written for
testOrderedWordCountvstestOrderedWordCountDisableSplitGrouping? - Derive the expected combined count of the word
a_3across both input files, and show it matches theverifyOrderedWordCountOutputassertion. - Why can
serviceStopmake a passing test appear to hang for up to 30 seconds? - Why is
@BeforeClass(not@Before) the right lifecycle hook for the mini-cluster?
When you can boot the mini-cluster, run the canonical DAG test, and read its log trail, continue to Lab 5.2: Add a Missing TestVertexImpl Transition Test.