blob: 873fe17d747fdcc0c161172fe1aec87472ec59d6 [file] [log] [blame]
/*
* 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.solr.cloud.hdfs;
import java.io.File;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.regex.Pattern;
import org.apache.commons.lang3.time.FastDateFormat;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.HardLink;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.RawLocalFileSystem;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.apache.hadoop.hdfs.MiniDFSNNTopology;
import org.apache.hadoop.hdfs.server.datanode.fsdataset.impl.BlockPoolSlice;
import org.apache.hadoop.hdfs.server.namenode.NameNodeAdapter;
import org.apache.hadoop.hdfs.server.namenode.NameNodeResourceChecker;
import org.apache.hadoop.hdfs.server.namenode.ha.HATestUtil;
import org.apache.hadoop.http.HttpServer2;
import org.apache.hadoop.io.nativeio.NativeIO;
import org.apache.hadoop.metrics2.MetricsSystem;
import org.apache.hadoop.metrics2.impl.MetricsSystemImpl;
import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
import org.apache.hadoop.util.DiskChecker;
import org.apache.lucene.util.Constants;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.util.IOUtils;
import org.apache.solr.common.util.SuppressForbidden;
import org.apache.solr.core.DirectoryFactory;
import org.apache.solr.util.HdfsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.lucene.util.LuceneTestCase.random;
public class HdfsTestUtil {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String SOLR_HACK_FOR_CLASS_VERIFICATION_FIELD = "SOLR_HACK_FOR_CLASS_VERIFICATION";
private static final String LOGICAL_HOSTNAME = "ha-nn-uri-%d";
private static final boolean HA_TESTING_ENABLED = false; // SOLR-XXX
private static Map<MiniDFSCluster,Timer> timers = new HashMap<>();
private static final Object TIMERS_LOCK = new Object();
private static FSDataOutputStream badTlogOutStream;
private static FileSystem badTlogOutStreamFs;
public static MiniDFSCluster setupClass(String dir) throws Exception {
return setupClass(dir, true, true);
}
public static MiniDFSCluster setupClass(String dir, boolean haTesting) throws Exception {
return setupClass(dir, haTesting, true);
}
public static void checkAssumptions() {
ensureHadoopHomeNotSet();
checkHadoopWindows();
checkOverriddenHadoopClasses();
checkFastDateFormat();
checkGeneratedIdMatches();
}
/**
* If Hadoop home is set via environment variable HADOOP_HOME or Java system property
* hadoop.home.dir, the behavior of test is undefined. Ensure that these are not set
* before starting. It is not possible to easily unset environment variables so better
* to bail out early instead of trying to test.
*/
private static void ensureHadoopHomeNotSet() {
if (System.getenv("HADOOP_HOME") != null) {
LuceneTestCase.fail("Ensure that HADOOP_HOME environment variable is not set.");
}
if (System.getProperty("hadoop.home.dir") != null) {
LuceneTestCase.fail("Ensure that \"hadoop.home.dir\" Java property is not set.");
}
}
/**
* Hadoop integration tests fail on Windows without Hadoop NativeIO
*/
private static void checkHadoopWindows() {
LuceneTestCase.assumeTrue("Hadoop does not work on Windows without Hadoop NativeIO",
!Constants.WINDOWS || NativeIO.isAvailable());
}
/**
* Ensure that the tests are picking up the modified Hadoop classes
*/
private static void checkOverriddenHadoopClasses() {
List<Class<?>> modifiedHadoopClasses = Arrays.asList(BlockPoolSlice.class, DiskChecker.class,
FileUtil.class, HardLink.class, HttpServer2.class, NameNodeResourceChecker.class, RawLocalFileSystem.class);
for (Class<?> clazz : modifiedHadoopClasses) {
try {
LuceneTestCase.assertNotNull("Field on " + clazz.getCanonicalName() + " should not have been null",
clazz.getField(SOLR_HACK_FOR_CLASS_VERIFICATION_FIELD));
} catch (NoSuchFieldException e) {
LuceneTestCase.fail("Expected to load Solr modified Hadoop class " + clazz.getCanonicalName() +
" , but it was not found.");
}
}
}
/**
* Checks that commons-lang3 FastDateFormat works with configured locale
*/
@SuppressForbidden(reason="Call FastDateFormat.format same way Hadoop calls it")
private static void checkFastDateFormat() {
try {
FastDateFormat.getInstance().format(System.currentTimeMillis());
} catch (ArrayIndexOutOfBoundsException e) {
LuceneTestCase.assumeNoException("commons-lang3 FastDateFormat doesn't work with " +
Locale.getDefault().toLanguageTag(), e);
}
}
/**
* Hadoop fails to generate locale agnostic ids - Checks that generated string matches
*/
private static void checkGeneratedIdMatches() {
// This is basically how Namenode generates fsimage ids and checks that the fsimage filename matches
LuceneTestCase.assumeTrue("Check that generated id matches regex",
Pattern.matches("(\\d+)", String.format(Locale.getDefault(),"%019d", 0)));
}
public static MiniDFSCluster setupClass(String dir, boolean safeModeTesting, boolean haTesting) throws Exception {
checkAssumptions();
if (!HA_TESTING_ENABLED) haTesting = false;
DefaultMetricsSystem.setInstance(new FakeMetricsSystem());
Configuration conf = getBasicConfiguration(new Configuration());
conf.set("hdfs.minidfs.basedir", dir + File.separator + "hdfsBaseDir");
conf.set("dfs.namenode.name.dir", dir + File.separator + "nameNodeNameDir");
// Disable metrics logging for HDFS
conf.setInt("dfs.namenode.metrics.logger.period.seconds", 0);
conf.setInt("dfs.datanode.metrics.logger.period.seconds", 0);
System.setProperty("test.build.data", dir + File.separator + "hdfs" + File.separator + "build");
System.setProperty("test.cache.data", dir + File.separator + "hdfs" + File.separator + "cache");
System.setProperty("solr.lock.type", DirectoryFactory.LOCK_TYPE_HDFS);
// test-files/solr/solr.xml sets this to be 15000. This isn't long enough for HDFS in some cases.
System.setProperty("socketTimeout", "90000");
String blockcacheGlobal = System.getProperty("solr.hdfs.blockcache.global", Boolean.toString(random().nextBoolean()));
System.setProperty("solr.hdfs.blockcache.global", blockcacheGlobal);
// Limit memory usage for HDFS tests
if(Boolean.parseBoolean(blockcacheGlobal)) {
System.setProperty("solr.hdfs.blockcache.blocksperbank", "4096");
} else {
System.setProperty("solr.hdfs.blockcache.blocksperbank", "512");
System.setProperty("tests.hdfs.numdatanodes", "1");
}
int dataNodes = Integer.getInteger("tests.hdfs.numdatanodes", 2);
final MiniDFSCluster.Builder dfsClusterBuilder = new MiniDFSCluster.Builder(conf)
.numDataNodes(dataNodes).format(true);
if (haTesting) {
dfsClusterBuilder.nnTopology(MiniDFSNNTopology.simpleHATopology());
}
MiniDFSCluster dfsCluster = dfsClusterBuilder.build();
HdfsUtil.TEST_CONF = getClientConfiguration(dfsCluster);
System.setProperty("solr.hdfs.home", getDataDir(dfsCluster, "solr_hdfs_home"));
dfsCluster.waitActive();
if (haTesting) dfsCluster.transitionToActive(0);
int rndMode = random().nextInt(3);
if (safeModeTesting && rndMode == 1) {
NameNodeAdapter.enterSafeMode(dfsCluster.getNameNode(), false);
int rnd = random().nextInt(10000);
Timer timer = new Timer();
synchronized (TIMERS_LOCK) {
if (timers == null) {
timers = new HashMap<>();
}
timers.put(dfsCluster, timer);
}
timer.schedule(new TimerTask() {
@Override
public void run() {
NameNodeAdapter.leaveSafeMode(dfsCluster.getNameNode());
}
}, rnd);
} else if (haTesting && rndMode == 2) {
int rnd = random().nextInt(30000);
Timer timer = new Timer();
synchronized (TIMERS_LOCK) {
if (timers == null) {
timers = new HashMap<>();
}
timers.put(dfsCluster, timer);
}
timer.schedule(new TimerTask() {
@Override
public void run() {
// TODO: randomly transition to standby
// try {
// dfsCluster.transitionToStandby(0);
// dfsCluster.transitionToActive(1);
// } catch (IOException e) {
// throw new RuntimeException();
// }
}
}, rnd);
} else {
// TODO: we could do much better at testing this
// force a lease recovery by creating a tlog file and not closing it
URI uri = dfsCluster.getURI();
Path hdfsDirPath = new Path(uri.toString() + "/solr/collection1/core_node1/data/tlog/tlog.0000000000000000000");
// tran log already being created testing
badTlogOutStreamFs = FileSystem.get(hdfsDirPath.toUri(), getClientConfiguration(dfsCluster));
badTlogOutStream = badTlogOutStreamFs.create(hdfsDirPath);
}
SolrTestCaseJ4.useFactory("org.apache.solr.core.HdfsDirectoryFactory");
return dfsCluster;
}
private static Configuration getBasicConfiguration(Configuration conf) {
conf.setBoolean("dfs.block.access.token.enable", false);
conf.setBoolean("dfs.permissions.enabled", false);
conf.set("hadoop.security.authentication", "simple");
conf.setBoolean("fs.hdfs.impl.disable.cache", true);
return conf;
}
public static Configuration getClientConfiguration(MiniDFSCluster dfsCluster) {
Configuration conf = getBasicConfiguration(dfsCluster.getConfiguration(0));
if (dfsCluster.getNumNameNodes() > 1) {
HATestUtil.setFailoverConfigurations(dfsCluster, conf);
}
return conf;
}
public static void teardownClass(MiniDFSCluster dfsCluster) throws Exception {
HdfsUtil.TEST_CONF = null;
if (badTlogOutStream != null) {
IOUtils.closeQuietly(badTlogOutStream);
badTlogOutStream = null;
}
if (badTlogOutStreamFs != null) {
IOUtils.closeQuietly(badTlogOutStreamFs);
badTlogOutStreamFs = null;
}
try {
try {
SolrTestCaseJ4.resetFactory();
} catch (Exception e) {
log.error("Exception trying to reset solr.directoryFactory", e);
}
if (dfsCluster != null) {
synchronized (TIMERS_LOCK) {
if (timers != null) {
Timer timer = timers.remove(dfsCluster);
if (timer != null) {
timer.cancel();
}
if (timers.isEmpty()) {
timers = null;
}
}
}
try {
dfsCluster.shutdown(true);
} catch (Error e) {
// Added in SOLR-7134
// Rarely, this can fail to either a NullPointerException
// or a class not found exception. The later may fixable
// by adding test dependencies.
log.warn("Exception shutting down dfsCluster", e);
}
}
} finally {
System.clearProperty("test.build.data");
System.clearProperty("test.cache.data");
System.clearProperty("socketTimeout");
System.clearProperty("tests.hdfs.numdatanodes");
System.clearProperty("solr.lock.type");
// Clear "solr.hdfs." system properties
Enumeration<?> propertyNames = System.getProperties().propertyNames();
while(propertyNames.hasMoreElements()) {
String propertyName = String.valueOf(propertyNames.nextElement());
if(propertyName.startsWith("solr.hdfs.")) {
System.clearProperty(propertyName);
}
}
}
}
public static String getDataDir(MiniDFSCluster dfsCluster, String dataDir) {
if (dataDir == null) {
return null;
}
String dir = "/"
+ new File(dataDir).toString().replaceAll(":", "_")
.replaceAll("/", "_").replaceAll(" ", "_");
return getURI(dfsCluster) + dir;
}
public static String getURI(MiniDFSCluster dfsCluster) {
if (dfsCluster.getNameNodeInfos().length > 1) {
String logicalName = String.format(Locale.ENGLISH, LOGICAL_HOSTNAME, dfsCluster.getInstanceId()); // NOTE: hdfs uses default locale
return "hdfs://" + logicalName;
} else {
URI uri = dfsCluster.getURI(0);
return uri.toString();
}
}
/**
* By default in JDK9+, the ForkJoinWorkerThreadFactory does not give SecurityManager permissions
* to threads that are created. This works around that with a custom thread factory.
* See SOLR-9515 and HDFS-14251
* Used in org.apache.hadoop.hdfs.server.datanode.fsdataset.impl.BlockPoolSlice
*/
public static class HDFSForkJoinThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory {
@Override
public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
ForkJoinWorkerThread worker = new SecurityManagerWorkerThread(pool);
worker.setName("solr-hdfs-threadpool-" + worker.getPoolIndex());
return worker;
}
}
private static class SecurityManagerWorkerThread extends ForkJoinWorkerThread {
SecurityManagerWorkerThread(ForkJoinPool pool) {
super(pool);
}
}
/**
* Ensures that we don't try to initialize metrics and read files outside
* the source tree.
*/
public static class FakeMetricsSystem extends MetricsSystemImpl {
@Override
public synchronized MetricsSystem init(String prefix) {
return this;
}
}
}