/**
* 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.hadoop.hdfs;
import com.google.common.base.Stopwatch;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.metrics2.util.Quantile;
import org.apache.hadoop.metrics2.util.SampleQuantiles;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import org.junit.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* This class tests hflushing concurrently from many threads.
*/
public class TestMultiThreadedHflush {
static final int blockSize = 1024 * 1024;
private static final int NUM_THREADS = 10;
private static final int WRITE_SIZE = 517;
private static final int NUM_WRITES_PER_THREAD = 1000;
private byte[] toWrite = null;
private final SampleQuantiles quantiles = new SampleQuantiles(
new Quantile[]{new Quantile(0.50, 0.050), new Quantile(0.75, 0.025),
new Quantile(0.90, 0.010), new Quantile(0.95, 0.005),
new Quantile(0.99, 0.001)});
/*
* creates a file but does not close it
*/
private FSDataOutputStream createFile(FileSystem fileSys, Path name, int repl)
throws IOException {
FSDataOutputStream stm = fileSys.create(name, true, fileSys.getConf()
.getInt(CommonConfigurationKeys.IO_FILE_BUFFER_SIZE_KEY, 4096),
(short) repl, blockSize);
return stm;
}
private void initBuffer(int size) {
long seed = AppendTestUtil.nextLong();
toWrite = AppendTestUtil.randomBytes(seed, size);
}
private class WriterThread extends Thread {
private final FSDataOutputStream stm;
private final AtomicReference<Throwable> thrown;
private final int numWrites;
private final CountDownLatch countdown;
public WriterThread(FSDataOutputStream stm,
AtomicReference<Throwable> thrown, CountDownLatch countdown,
int numWrites) {
this.stm = stm;
this.thrown = thrown;
this.numWrites = numWrites;
this.countdown = countdown;
}
@Override
public void run() {
try {
countdown.await();
for (int i = 0; i < numWrites && thrown.get() == null; i++) {
doAWrite();
}
} catch (Throwable t) {
thrown.compareAndSet(null, t);
}
}
private void doAWrite() throws IOException {
Stopwatch sw = new Stopwatch().start();
stm.write(toWrite);
stm.hflush();
long micros = sw.elapsedTime(TimeUnit.MICROSECONDS);
quantiles.insert(micros);
}
}
/**
* Test case where a bunch of threads are both appending and flushing.
* They all finish before the file is closed.
*/
@Test
public void testMultipleHflushersRepl1() throws Exception {
doTestMultipleHflushers(1);
}
@Test
public void testMultipleHflushersRepl3() throws Exception {
doTestMultipleHflushers(3);
}
private void doTestMultipleHflushers(int repl) throws Exception {
Configuration conf = new Configuration();
MiniDFSCluster cluster =
new MiniDFSCluster.Builder(conf).numDataNodes(repl).build();
FileSystem fs = cluster.getFileSystem();
Path p = new Path("/multiple-hflushers.dat");
try {
doMultithreadedWrites(conf, p, NUM_THREADS, WRITE_SIZE,
NUM_WRITES_PER_THREAD, repl);
System.out.println("Latency quantiles (in microseconds):\n" + quantiles);
} finally {
fs.close();
cluster.shutdown();
}
}
/**
* Test case where a bunch of threads are continuously calling hflush() while
* another
* thread appends some data and then closes the file.
* <p/>
* The hflushing threads should eventually catch an IOException stating that
* the stream
* was closed -- and not an NPE or anything like that.
*/
@Test
public void testHflushWhileClosing() throws Throwable {
Configuration conf = new Configuration();
MiniDFSCluster cluster = new MiniDFSCluster.Builder(conf).build();
FileSystem fs = cluster.getFileSystem();
Path p = new Path("/hflush-and-close.dat");
final FSDataOutputStream stm = createFile(fs, p, 1);
ArrayList<Thread> flushers = new ArrayList<Thread>();
final AtomicReference<Throwable> thrown = new AtomicReference<Throwable>();
try {
for (int i = 0; i < 10; i++) {
Thread flusher = new Thread() {
@Override
public void run() {
try {
while (true) {
try {
stm.hflush();
} catch (IOException ioe) {
if (!ioe.toString().contains("DFSOutputStream is closed")) {
throw ioe;
} else {
return;
}
}
}
} catch (Throwable t) {
thrown.set(t);
}
}
};
flusher.start();
flushers.add(flusher);
}
// Write some data
for (int i = 0; i < 10000; i++) {
stm.write(1);
}
// Close it while the flushing threads are still flushing
stm.close();
// Wait for the flushers to all die.
for (Thread t : flushers) {
t.join();
}
// They should have all gotten the expected exception, not anything
// else.
if (thrown.get() != null) {
throw thrown.get();
}
} finally {
fs.close();
cluster.shutdown();
}
}
public void doMultithreadedWrites(Configuration conf, Path p, int numThreads,
int bufferSize, int numWrites, int replication) throws Exception {
initBuffer(bufferSize);
// create a new file.
FileSystem fs = p.getFileSystem(conf);
FSDataOutputStream stm = createFile(fs, p, replication);
System.out.println("Created file simpleFlush.dat");
// There have been a couple issues with flushing empty buffers, so do
// some empty flushes first.
stm.hflush();
stm.hflush();
stm.write(1);
stm.hflush();
stm.hflush();
CountDownLatch countdown = new CountDownLatch(1);
ArrayList<Thread> threads = new ArrayList<Thread>();
AtomicReference<Throwable> thrown = new AtomicReference<Throwable>();
for (int i = 0; i < numThreads; i++) {
Thread t = new WriterThread(stm, thrown, countdown, numWrites);
threads.add(t);
t.start();
}
// Start all the threads at the same time for maximum raciness!
countdown.countDown();
for (Thread t : threads) {
t.join();
}
if (thrown.get() != null) {
throw new RuntimeException("Deferred", thrown.get());
}
stm.close();
System.out.println("Closed file.");
}
public static void main(String args[]) throws Exception {
System.exit(ToolRunner.run(new CLIBenchmark(), args));
}
private static class CLIBenchmark extends Configured implements Tool {
public int run(String args[]) throws Exception {
if (args.length != 1) {
System.err
.println("usage: " + TestMultiThreadedHflush.class.getSimpleName() +
" <path to test file> ");
System.err.println("Configurations settable by -D options:\n" +
" num.threads [default 10] - how many threads to run\n" +
" write.size [default 511] - bytes per write\n" +
" num.writes [default 50000] - how many writes to perform");
System.exit(1);
}
TestMultiThreadedHflush test = new TestMultiThreadedHflush();
Configuration conf = getConf();
Path p = new Path(args[0]);
int numThreads = conf.getInt("num.threads", 10);
int writeSize = conf.getInt("write.size", 511);
int numWrites = conf.getInt("num.writes", 50000);
int replication = conf.getInt(DFSConfigKeys.DFS_REPLICATION_KEY,
DFSConfigKeys.DFS_REPLICATION_DEFAULT);
Stopwatch sw = new Stopwatch().start();
test.doMultithreadedWrites(conf, p, numThreads, writeSize, numWrites,
replication);
sw.stop();
System.out.println("Finished in " + sw.elapsedMillis() + "ms");
System.out
.println("Latency quantiles (in microseconds):\n" + test.quantiles);
return 0;
}
}
}