org.apache.hadoop.dfs.ClusterTestDFS.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.dfs.ClusterTestDFS.java

Source

/**
 * 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.dfs;

import junit.framework.TestCase;
import junit.framework.AssertionFailedError;

import org.apache.commons.logging.*;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FSInputStream;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.io.UTF8;
import org.apache.hadoop.conf.Configuration;

import java.io.File;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Random;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * Test DFS.
 * ClusterTestDFS is a JUnit test for DFS using "pseudo multiprocessing" (or 
 more strictly, pseudo distributed) meaning all daemons run in one process 
 and sockets are used to communicate between daemons.  The test permutes
 * various block sizes, number of files, file sizes, and number of
 * datanodes.  After creating 1 or more files and filling them with random
 * data, one datanode is shutdown, and then the files are verfified.
 * Next, all the random test files are deleted and we test for leakage
 * (non-deletion) by directly checking the real directories corresponding
 * to the datanodes still running.
 * <p>
 * Usage notes: TEST_PERMUTATION_MAX can be adjusted to perform more or
 * less testing of permutations.  The ceiling of useful permutation is
 * TEST_PERMUTATION_MAX_CEILING.
 * <p>
 * DFSClient emits many messages that can be ignored like:
 * "Failed to connect to *:7000:java.net.ConnectException: Connection refused: connect"
 * because a datanode is forced to close during testing.
 * <p>
 * Warnings about "Zero targets found" can be ignored (these are naggingly
 * emitted even though it is not possible to achieve the desired replication
 * level with the number of active datanodes.)
 * <p>
 * Possible Extensions:
 * <p>Bring a datanode down and restart it to verify reconnection to namenode.
 * <p>Simulate running out of disk space on one datanode only.
 * <p>Bring the namenode down and restart it to verify that datanodes reconnect.
 * <p>
 * <p>For a another approach to filesystem testing, see the high level
 * (HadoopFS level) test {@link org.apache.hadoop.fs.TestFileSystem}.
 */
public class ClusterTestDFS extends TestCase implements FSConstants {
    private static final Log LOG = LogFactory.getLog("org.apache.hadoop.dfs.ClusterTestDFS");

    private static Configuration conf = new Configuration();
    private static int BUFFER_SIZE = conf.getInt("io.file.buffer.size", 4096);

    private static int testCycleNumber = 0;

    /**
     * all DFS test files go under this base directory
     */
    private static String baseDirSpecified;

    /**
     * base dir as File
     */
    private static File baseDir;

    /** DFS block sizes to permute over in multiple test cycles
     * (array length should be prime).
     */
    private static final int[] BLOCK_SIZES = { 100000, 4096 };

    /** DFS file sizes to permute over in multiple test cycles
     * (array length should be prime).
     */
    private static final int[] FILE_SIZES = { 100000, 100001, 4095, 4096, 4097, 1000000, 1000001 };

    /** DFS file counts to permute over in multiple test cycles
     * (array length should be prime).
     */
    private static final int[] FILE_COUNTS = { 1, 10, 100 };

    /** Number of useful permutations or test cycles.
     * (The 2 factor represents the alternating 2 or 3 number of datanodes
     * started.)
     */
    private static final int TEST_PERMUTATION_MAX_CEILING = BLOCK_SIZES.length * FILE_SIZES.length
            * FILE_COUNTS.length * 2;

    /** Number of permutations of DFS test parameters to perform.
     * If this is greater than ceiling TEST_PERMUTATION_MAX_CEILING, then the
     * ceiling value is used.
     */
    private static final int TEST_PERMUTATION_MAX = 3;
    private Constructor randomDataGeneratorCtor = null;

    static {
        baseDirSpecified = System.getProperty("test.dfs.data", "/tmp/dfs_test");
        baseDir = new File(baseDirSpecified);
    }

    protected void setUp() throws Exception {
        super.setUp();
        conf.setBoolean("test.dfs.same.host.targets.allowed", true);
    }

    /**
     * Remove old files from temp area used by this test case and be sure
     * base temp directory can be created.
     */
    protected void prepareTempFileSpace() {
        if (baseDir.exists()) {
            try { // start from a blank slate
                FileUtil.fullyDelete(baseDir);
            } catch (Exception ignored) {
            }
        }
        baseDir.mkdirs();
        if (!baseDir.isDirectory()) {
            throw new RuntimeException(
                    "Value of root directory property test.dfs.data for dfs test is not a directory: "
                            + baseDirSpecified);
        }
    }

    /**
     * Pseudo Distributed FS Test.
     * Test DFS by running all the necessary daemons in one process.
     * Test various block sizes, number of files, disk space consumption,
     * and leakage.
     *
     * @throws Exception
     */
    public void testFsPseudoDistributed() throws Exception {
        while (testCycleNumber < TEST_PERMUTATION_MAX && testCycleNumber < TEST_PERMUTATION_MAX_CEILING) {
            int blockSize = BLOCK_SIZES[testCycleNumber % BLOCK_SIZES.length];
            int numFiles = FILE_COUNTS[testCycleNumber % FILE_COUNTS.length];
            int fileSize = FILE_SIZES[testCycleNumber % FILE_SIZES.length];
            prepareTempFileSpace();
            testFsPseudoDistributed(fileSize, numFiles, blockSize, (testCycleNumber % 2) + 2);
        }
    }

    /**
     * Pseudo Distributed FS Testing.
     * Do one test cycle with given parameters.
     *
     * @param nBytes         number of bytes to write to each file.
     * @param numFiles       number of files to create.
     * @param blockSize      block size to use for this test cycle.
     * @param initialDNcount number of datanodes to create
     * @throws Exception
     */
    public void testFsPseudoDistributed(long nBytes, int numFiles, int blockSize, int initialDNcount)
            throws Exception {
        long startTime = System.currentTimeMillis();
        int bufferSize = Math.min(BUFFER_SIZE, blockSize);
        boolean checkDataDirsEmpty = false;
        int iDatanodeClosed = 0;
        Random randomDataGenerator = makeRandomDataGenerator();
        final int currentTestCycleNumber = testCycleNumber;
        msg("using randomDataGenerator=" + randomDataGenerator.getClass().getName());

        //
        //     modify config for test

        //
        // set given config param to override other config settings
        conf.setInt("test.dfs.block_size", blockSize);
        // verify that config changed
        assertTrue(blockSize == conf.getInt("test.dfs.block_size", 2)); // 2 is an intentional obviously-wrong block size
        // downsize for testing (just to save resources)
        conf.setInt("dfs.namenode.handler.count", 3);
        if (false) { //  use MersenneTwister, if present
            conf.set("hadoop.random.class", "org.apache.hadoop.util.MersenneTwister");
        }
        conf.setLong("dfs.blockreport.intervalMsec", 50 * 1000L);
        conf.setLong("dfs.datanode.startupMsec", 15 * 1000L);

        String nameFSDir = baseDirSpecified + "/name";
        msg("----Start Test Cycle=" + currentTestCycleNumber + " test.dfs.block_size=" + blockSize + " nBytes="
                + nBytes + " numFiles=" + numFiles + " initialDNcount=" + initialDNcount);

        //
        //          start a NameNode

        int nameNodePort = 9000 + testCycleNumber++; // ToDo: settable base port
        String nameNodeSocketAddr = "localhost:" + nameNodePort;
        conf.set("dfs.name.dir", nameFSDir);
        NameNode nameNodeDaemon = new NameNode(nameNodeSocketAddr, conf);
        DFSClient dfsClient = null;
        try {
            //
            //        start some DataNodes
            //
            ArrayList<DataNode> listOfDataNodeDaemons = new ArrayList<DataNode>();
            FileSystem.setDefaultUri(conf, "hdfs://" + nameNodeSocketAddr);
            for (int i = 0; i < initialDNcount; i++) {
                // uniquely config real fs path for data storage for this datanode
                String dataDirs[] = new String[1];
                dataDirs[0] = baseDirSpecified + "/datanode" + i;
                conf.set("dfs.data.dir", dataDirs[0]);
                DataNode dn = DataNode.makeInstance(dataDirs, conf);
                if (dn != null) {
                    listOfDataNodeDaemons.add(dn);
                    (new Thread(dn, "DataNode" + i + ": " + dataDirs[0])).start();
                }
            }
            try {
                assertTrue("insufficient datanodes for test to continue", (listOfDataNodeDaemons.size() >= 2));

                //
                //          wait for datanodes to report in
                awaitQuiescence();

                //  act as if namenode is a remote process
                dfsClient = new DFSClient(new InetSocketAddress("localhost", nameNodePort), conf);

                //
                //           write nBytes of data using randomDataGenerator to numFiles
                //
                ArrayList<UTF8> testfilesList = new ArrayList<UTF8>();
                byte[] buffer = new byte[bufferSize];
                UTF8 testFileName = null;
                for (int iFileNumber = 0; iFileNumber < numFiles; iFileNumber++) {
                    testFileName = new UTF8("/f" + iFileNumber);
                    testfilesList.add(testFileName);
                    OutputStream nos = dfsClient.create(testFileName.toString(), false);
                    try {
                        for (long nBytesWritten = 0L; nBytesWritten < nBytes; nBytesWritten += buffer.length) {
                            if ((nBytesWritten + buffer.length) > nBytes) {
                                // calculate byte count needed to exactly hit nBytes in length
                                //  to keep randomDataGenerator in sync during the verify step
                                int pb = (int) (nBytes - nBytesWritten);
                                byte[] bufferPartial = new byte[pb];
                                randomDataGenerator.nextBytes(bufferPartial);
                                nos.write(bufferPartial);
                            } else {
                                randomDataGenerator.nextBytes(buffer);
                                nos.write(buffer);
                            }
                        }
                    } finally {
                        nos.flush();
                        nos.close();
                    }
                }

                //
                // No need to wait for blocks to be replicated because replication
                //  is supposed to be complete when the file is closed.
                //

                //
                //                     take one datanode down
                iDatanodeClosed = currentTestCycleNumber % listOfDataNodeDaemons.size();
                DataNode dn = (DataNode) listOfDataNodeDaemons.get(iDatanodeClosed);
                msg("shutdown datanode daemon " + iDatanodeClosed + " dn=" + dn.data);
                try {
                    dn.shutdown();
                } catch (Exception e) {
                    msg("ignoring datanode shutdown exception=" + e);
                }

                //
                //          verify data against a "rewound" randomDataGenerator
                //               that all of the data is intact
                long lastLong = randomDataGenerator.nextLong();
                randomDataGenerator = makeRandomDataGenerator(); // restart (make new) PRNG
                ListIterator li = testfilesList.listIterator();
                while (li.hasNext()) {
                    testFileName = (UTF8) li.next();
                    FSInputStream nis = dfsClient.open(testFileName.toString());
                    byte[] bufferGolden = new byte[bufferSize];
                    int m = 42;
                    try {
                        while (m != -1) {
                            m = nis.read(buffer);
                            if (m == buffer.length) {
                                randomDataGenerator.nextBytes(bufferGolden);
                                assertBytesEqual(buffer, bufferGolden, buffer.length);
                            } else if (m > 0) {
                                byte[] bufferGoldenPartial = new byte[m];
                                randomDataGenerator.nextBytes(bufferGoldenPartial);
                                assertBytesEqual(buffer, bufferGoldenPartial, bufferGoldenPartial.length);
                            }
                        }
                    } finally {
                        nis.close();
                    }
                }
                // verify last randomDataGenerator rand val to ensure last file length was checked
                long lastLongAgain = randomDataGenerator.nextLong();
                assertEquals(lastLong, lastLongAgain);
                msg("Finished validating all file contents");

                //
                //                    now delete all the created files
                msg("Delete all random test files under DFS via remaining datanodes");
                li = testfilesList.listIterator();
                while (li.hasNext()) {
                    testFileName = (UTF8) li.next();
                    assertTrue(dfsClient.delete(testFileName.toString(), true));
                }

                //
                //                   wait for delete to be propagated
                //                  (unlike writing files, delete is lazy)
                msg("Test thread sleeping while datanodes propagate delete...");
                awaitQuiescence();
                msg("Test thread awakens to verify file contents");

                //
                //             check that the datanode's block directory is empty
                //                (except for datanode that had forced shutdown)
                checkDataDirsEmpty = true; // do it during finally clause

            } catch (AssertionFailedError afe) {
                throw afe;
            } catch (Throwable t) {
                msg("Unexpected exception_b: " + t);
                t.printStackTrace();
            } finally {
                //
                // shut down datanode daemons (this takes advantage of being same-process)
                msg("begin shutdown of all datanode daemons for test cycle " + currentTestCycleNumber);

                for (int i = 0; i < listOfDataNodeDaemons.size(); i++) {
                    DataNode dataNode = (DataNode) listOfDataNodeDaemons.get(i);
                    if (i != iDatanodeClosed) {
                        try {
                            if (checkDataDirsEmpty) {
                                assertNoBlocks(dataNode);

                            }
                            dataNode.shutdown();
                        } catch (Exception e) {
                            msg("ignoring exception during (all) datanode shutdown, e=" + e);
                        }
                    }
                }
            }
            msg("finished shutdown of all datanode daemons for test cycle " + currentTestCycleNumber);
            if (dfsClient != null) {
                try {
                    msg("close down subthreads of DFSClient");
                    dfsClient.close();
                } catch (Exception ignored) {
                }
                msg("finished close down of DFSClient");
            }
        } catch (AssertionFailedError afe) {
            throw afe;
        } catch (Throwable t) {
            msg("Unexpected exception_a: " + t);
            t.printStackTrace();
        } finally {
            // shut down namenode daemon (this takes advantage of being same-process)
            msg("begin shutdown of namenode daemon for test cycle " + currentTestCycleNumber);
            try {
                nameNodeDaemon.stop();
            } catch (Exception e) {
                msg("ignoring namenode shutdown exception=" + e);
            }
            msg("finished shutdown of namenode daemon for test cycle " + currentTestCycleNumber);
        }
        msg("test cycle " + currentTestCycleNumber + " elapsed time="
                + (System.currentTimeMillis() - startTime) / 1000. + "sec");
        msg("threads still running (look for stragglers): ");
        msg(summarizeThreadGroup());
    }

    private void assertNoBlocks(DataNode dn) {
        Block[] blocks = dn.data.getBlockReport();
        // if this fails, the delete did not propagate because either
        //   awaitQuiescence() returned before the disk images were removed
        //   or a real failure was detected.
        assertTrue(" data dir not empty: " + dn.data, blocks.length == 0);
    }

    /**
     * Make a data generator.
     * Allows optional use of high quality PRNG by setting property
     * hadoop.random.class to the full class path of a subclass of
     * java.util.Random such as "...util.MersenneTwister".
     * The property test.dfs.random.seed can supply a seed for reproducible
     * testing (a default is set here if property is not set.)
     */
    private Random makeRandomDataGenerator() {
        long seed = conf.getLong("test.dfs.random.seed", 0xB437EF);
        try {
            if (randomDataGeneratorCtor == null) {
                // lazy init
                String rndDataGenClassname = conf.get("hadoop.random.class", "java.util.Random");
                Class<?> clazz = Class.forName(rndDataGenClassname);
                randomDataGeneratorCtor = clazz.getConstructor(Long.TYPE);
            }

            if (randomDataGeneratorCtor != null) {
                Object arg[] = { new Long(seed) };
                return (Random) randomDataGeneratorCtor.newInstance(arg);
            }
        } catch (ClassNotFoundException absorb) {
        } catch (NoSuchMethodException absorb) {
        } catch (SecurityException absorb) {
        } catch (InstantiationException absorb) {
        } catch (IllegalAccessException absorb) {
        } catch (IllegalArgumentException absorb) {
        } catch (InvocationTargetException absorb) {
        }

        // last resort
        return new java.util.Random(seed);
    }

    /** Wait for the DFS datanodes to become quiescent.
     * The initial implementation is to sleep for some fixed amount of time,
     * but a better implementation would be to really detect when distributed
     * operations are completed.
     * @throws InterruptedException
     */
    private void awaitQuiescence() throws InterruptedException {
        // ToDo: Need observer pattern, not static sleep
        // Doug suggested that the block report interval could be made shorter
        //   and then observing that would be a good way to know when an operation
        //   was complete (quiescence detect).
        sleepAtLeast(60000);
    }

    private void assertBytesEqual(byte[] buffer, byte[] bufferGolden, int len) {
        for (int i = 0; i < len; i++) {
            assertEquals(buffer[i], bufferGolden[i]);
        }
    }

    private void msg(String s) {
        //System.out.println(s);
        LOG.info(s);
    }

    public static void sleepAtLeast(int tmsec) {
        long t0 = System.currentTimeMillis();
        long t1 = t0;
        long tslept = t1 - t0;
        while (tmsec > tslept) {
            try {
                long tsleep = tmsec - tslept;
                Thread.sleep(tsleep);
                t1 = System.currentTimeMillis();
            } catch (InterruptedException ie) {
                t1 = System.currentTimeMillis();
            }
            tslept = t1 - t0;
        }
    }

    public static String summarizeThreadGroup() {
        int n = 10;
        int k = 0;
        Thread[] tarray = null;
        StringBuffer sb = new StringBuffer(500);
        do {
            n = n * 10;
            tarray = new Thread[n];
            k = Thread.enumerate(tarray);
        } while (k == n); // while array is too small...
        for (int i = 0; i < k; i++) {
            Thread thread = tarray[i];
            sb.append(thread.toString());
            sb.append("\n");
        }
        return sb.toString();
    }

    public static void main(String[] args) throws Exception {
        String usage = "Usage: ClusterTestDFS (no args)";
        if (args.length != 0) {
            System.err.println(usage);
            System.exit(-1);
        }
        String[] testargs = { "org.apache.hadoop.dfs.ClusterTestDFS" };
        junit.textui.TestRunner.main(testargs);
    }

}