/*
* 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.server.hightidenode;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
import java.util.Iterator;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Date;
import java.text.SimpleDateFormat;
import java.lang.Thread;
import java.lang.InterruptedException;
import java.net.InetSocketAddress;
import java.net.URI;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.hadoop.hdfs.DFSClient;
import org.apache.hadoop.hdfs.DistributedFileSystem;
import org.apache.hadoop.hdfs.protocol.Block;
import org.apache.hadoop.ipc.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.util.StringUtils;
import org.apache.hadoop.util.Daemon;
import org.apache.hadoop.util.ReflectionUtils;
import org.apache.hadoop.util.ToolRunner;
import org.apache.hadoop.net.NetUtils;
import org.apache.hadoop.fs.HarFileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.BlockLocation;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.hdfs.protocol.HighTideProtocol;
import org.apache.hadoop.hdfs.protocol.PolicyInfo;
import org.apache.hadoop.hdfs.protocol.PolicyInfo.PathInfo;
import org.apache.hadoop.hdfs.server.hightidenode.metrics.HighTideNodeMetrics;
/**
* A {@link HighTideNode} that implements
*/
public class HighTideNode implements HighTideProtocol {
static{
Configuration.addDefaultResource("hdfs-default.xml");
Configuration.addDefaultResource("hdfs-site.xml");
}
public static final Log LOG = LogFactory.getLog( "org.apache.hadoop.hightidenode.HighTideNode");
public static final long SLEEP_TIME = 10000L; // 10 seconds
public static final int DEFAULT_PORT = 60100;
public static final String HIGHTIDE_FULLSYNC_INTERVAL = "hightide.fullsync.interval.seconds";
public static final long HIGHTIDE_FULLSYNC_INTERVAL_DEFAULT = 60 * 60; // 1 hour
public static final SimpleDateFormat dateForm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/** RPC server */
private Server server;
/** RPC server address */
private InetSocketAddress serverAddress = null;
/** only used for testing purposes */
private boolean stopRequested = false;
/** Configuration Manager */
private ConfigManager configMgr;
/** hadoop configuration */
private Configuration conf;
protected boolean initialized; // Are we initialized?
protected volatile boolean running; // Are we running?
/** Deamon thread to find missing blocks */
Daemon triggerThread = null;
/** Daemon thread to fix corrupt files */
public FileFixer fileFixer = null;
Daemon fileFixerThread = null;
static HighTideNodeMetrics myMetrics;
// statistics about replicas fixed
public static class Statistics {
long numProcessedBlocks; // total blocks encountered in namespace
long processedSize; // disk space occupied by all blocks
public void clear() {
numProcessedBlocks = 0;
processedSize = 0;
}
public String toString() {
String msg = " numProcessedBlocks = " + numProcessedBlocks +
" processedSize = " + processedSize;
return msg;
}
}
// Startup options
static public enum StartupOption{
TEST ("-test"),
REGULAR ("-regular");
private String name = null;
private StartupOption(String arg) {this.name = arg;}
public String getName() {return name;}
}
/**
* Start HighTideNode.
* <p>
* The hightidenode-node can be started with one of the following startup options:
* <ul>
* <li>{@link StartupOption#REGULAR REGULAR} - normal hightidenode node startup</li>
* </ul>
* The option is passed via configuration field:
* <tt>fs.hightidenodenode.startup</tt>
*
* The conf will be modified to reflect the actual ports on which
* the HighTideNode is up and running if the user passes the port as
* <code>zero</code> in the conf.
*
* @param conf confirguration
* @throws IOException
*/
HighTideNode(Configuration conf) throws IOException {
try {
initialize(conf);
} catch (IOException e) {
LOG.error(StringUtils.stringifyException(e));
this.stop();
throw e;
} catch (Exception e) {
LOG.error(StringUtils.stringifyException(e));
this.stop();
throw new IOException(e);
}
}
public long getProtocolVersion(String protocol,
long clientVersion) throws IOException {
if (protocol.equals(HighTideProtocol.class.getName())) {
return HighTideProtocol.versionID;
} else {
throw new IOException("Unknown protocol to hightide node: " + protocol);
}
}
public ProtocolSignature getProtocolSignature(String protocol,
long clientVersion, int clientMethodsHash) throws IOException {
return ProtocolSignature.getProtocolSignature(
this, protocol, clientVersion, clientMethodsHash);
}
/**
* Wait for service to finish.
* (Normally, it runs forever.)
*/
public void join() {
try {
if (server != null) server.join();
if (triggerThread != null) triggerThread.join();
if (fileFixerThread != null) fileFixerThread.join();
} catch (InterruptedException ie) {
// do nothing
}
}
/**
* Stop all HighTideNode threads and wait for all to finish.
*/
public void stop() {
if (stopRequested) {
return;
}
stopRequested = true;
running = false;
if (server != null) server.stop();
if (triggerThread != null) triggerThread.interrupt();
if (fileFixer != null) fileFixer.shutdown();
if (fileFixerThread != null) fileFixerThread.interrupt();
if (myMetrics != null) {
myMetrics.shutdown();
}
}
private static InetSocketAddress getAddress(String address) {
return NetUtils.createSocketAddr(address);
}
public static InetSocketAddress getAddress(Configuration conf) {
String nodeport = conf.get("hightidenode.server.address");
if (nodeport == null) {
nodeport = "localhost:" + DEFAULT_PORT;
}
return getAddress(nodeport);
}
public InetSocketAddress getListenerAddress() {
return server.getListenerAddress();
}
private void initialize(Configuration conf)
throws IOException, SAXException, InterruptedException,
HighTideConfigurationException,
ClassNotFoundException, ParserConfigurationException {
this.conf = conf;
InetSocketAddress socAddr = HighTideNode.getAddress(conf);
int handlerCount = conf.getInt("fs.hightidenodenode.handler.count", 10);
// read in the configuration
configMgr = new ConfigManager(conf);
configMgr.reloadConfigsIfNecessary();
configMgr.startReload();
// create Metrics object
myMetrics = new HighTideNodeMetrics(conf, this);
// create rpc server
this.server = RPC.getServer(this, socAddr.getHostName(), socAddr.getPort(),
handlerCount, false, conf);
// The rpc-server port can be ephemeral... ensure we have the correct info
this.serverAddress = this.server.getListenerAddress();
LOG.info("HighTideNode up at: " + this.serverAddress);
initialized = true;
running = true;
this.server.start(); // start RPC server
this.fileFixer = new FileFixer(conf);
this.fileFixerThread = new Daemon(this.fileFixer);
fileFixer.setPolicyInfo(configMgr.getAllPolicies());
this.fileFixerThread.start();
// start the deamon thread to resync if needed
this.triggerThread = new Daemon(new TriggerMonitor());
this.triggerThread.start();
}
/**
* Sync up on hightidenode restart
*/
class TriggerMonitor implements Runnable {
private Map<String, Long> scanTimes = new HashMap<String, Long>();
private Map<String, DirectoryTraversal> scanState =
new HashMap<String, DirectoryTraversal>();
public void run() {
while (running) {
try {
doFullSync();
} catch (IOException e) {
LOG.info("Exception in doFullSync. " + StringUtils.stringifyException(e));
}
long sleepTime = conf.getLong(HIGHTIDE_FULLSYNC_INTERVAL,
HIGHTIDE_FULLSYNC_INTERVAL_DEFAULT) * 1000;
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
LOG.info("InterrupedException in TriggerMonitor.run.");
}
}
}
}
/**
* Full sync of all policies
*/
void doFullSync() throws IOException {
for (PolicyInfo pinfo:configMgr.getAllPolicies()) {
doFullSync(pinfo);
}
}
/**
* Full sync of specified policy
*/
void doFullSync(PolicyInfo pinfo) throws IOException {
Path srcPath = pinfo.getSrcPath();
LOG.info("Starting fullsync of srcPath " + srcPath);
FileSystem srcFs = srcPath.getFileSystem(pinfo.getConf());
int srcRepl = Integer.parseInt(pinfo.getProperty("replication"));
long modTimePeriod = Long.parseLong(pinfo.getProperty("modTimePeriod"));
long now = HighTideNode.now();
// traverse all files inside the subtree rooted at srcPath
List<FileStatus> slist = new ArrayList<FileStatus>();
slist.add(srcFs.getFileStatus(srcPath));
DirectoryTraversal traverse = new DirectoryTraversal(srcFs, slist);
while (true) {
FileStatus sstat = traverse.getNextFile();
if (sstat == null) {
break; // done checking all files
}
// we always follow the order of first changing the replication
// on the destination files before we update the repl factor of
// the source file. So, it is safe to make the following check
// and avoid checking the destination files if the source file
// already is at the specified replication factor.
if (sstat.getReplication() == srcRepl) {
continue;
}
// if the file has been updated recently, then do not
// do anything to it
if (sstat.getModificationTime() + modTimePeriod > now) {
continue;
}
// find the suffix in the srcPath that is mapped to the destination
srcPath = sstat.getPath();
String[] splits = srcPath.toString().split(pinfo.getSrcPath().toString());
String suffix = splits[1];
// match each pair of src and destination paths
boolean match = true;
for (PathInfo destPathInfo: pinfo.getDestPaths()) {
Path destPath = new Path(destPathInfo.getPath().toString() + suffix);
LOG.debug("Comparing " + srcPath + " with " + destPath);
int destRepl = Integer.parseInt(destPathInfo.getProperty("replication"));
FileSystem destFs = destPath.getFileSystem(pinfo.getConf());
FileStatus dstat = null;
try {
dstat = destFs.getFileStatus(destPath);
} catch (java.io.FileNotFoundException e ) {
match = false;
continue; // ok if the destination does not exist
} catch (IOException e) {
match = false;
LOG.info("Unable to locate matching file in destination " +
destPath + StringUtils.stringifyException(e) +
". Ignoring...");
}
LOG.info("Matching " + srcPath + " with " + destPath);
HighTideNode.getMetrics().filesMatched.inc();
if (dstat.getModificationTime() == sstat.getModificationTime() &&
dstat.getBlockSize() == sstat.getBlockSize() &&
dstat.getLen() == sstat.getLen()) {
// first reduce the intended replication on the destination path
if (dstat.getReplication() > destRepl) {
HighTideNode.getMetrics().filesChanged.inc();
long saved = dstat.getLen() * (dstat.getReplication() - destRepl);
LOG.info("Changing replication of dest " + destPath +
" from " + dstat.getReplication() + " to " + destRepl);
destFs.setReplication(dstat.getPath(), (short)destRepl);
saved += HighTideNode.getMetrics().savedSize.get();
HighTideNode.getMetrics().savedSize.set(saved);
}
} else {
// one destination path does not match the source
match = false;
break;
}
}
// if the all the destination paths matched the source, then
// reduce repl factor of the source
if (match && sstat.getReplication() > srcRepl) {
LOG.info("Changing replication of source " + srcPath +
" from " + sstat.getReplication() + " to " + srcRepl);
HighTideNode.getMetrics().filesChanged.inc();
long saved = sstat.getLen() * (sstat.getReplication() - srcRepl);
srcFs.setReplication(sstat.getPath(), (short)srcRepl);
saved += HighTideNode.getMetrics().savedSize.get();
HighTideNode.getMetrics().savedSize.set(saved);
}
}
LOG.info("Completed fullsync of srcPath " + srcPath);
}
/**
* Shuts down the HighTideNode
*/
void shutdown() throws IOException, InterruptedException {
configMgr.stopReload(); // stop config reloads
fileFixer.shutdown(); // stop block fixer
fileFixerThread.interrupt();
server.stop(); // stop http server
}
/**
* Implement HighTideProtocol methods
*/
/** {@inheritDoc} */
public PolicyInfo[] getAllPolicies() throws IOException {
Collection<PolicyInfo> list = configMgr.getAllPolicies();
return list.toArray(new PolicyInfo[list.size()]);
}
/**
* returns my Metrics object
*/
public static HighTideNodeMetrics getMetrics() {
return myMetrics;
}
/**
* Returns current time.
*/
static long now() {
return System.currentTimeMillis();
}
private static void printUsage() {
System.err.println("Usage: java HighTideNode ");
}
private static StartupOption parseArguments(String args[]) {
int argsLen = (args == null) ? 0 : args.length;
StartupOption startOpt = StartupOption.REGULAR;
for(int i=0; i < argsLen; i++) {
String cmd = args[i]; // We have to parse command line args in future.
}
return startOpt;
}
/**
* Convert command line options to configuration parameters
*/
private static void setStartupOption(Configuration conf, StartupOption opt) {
conf.set("fs.hightidenodenode.startup", opt.toString());
}
/**
* Create an instance of the HighTideNode
*/
public static HighTideNode createHighTideNode(String argv[],
Configuration conf) throws IOException {
if (conf == null) {
conf = new Configuration();
}
StartupOption startOpt = parseArguments(argv);
if (startOpt == null) {
printUsage();
return null;
}
setStartupOption(conf, startOpt);
HighTideNode node = new HighTideNode(conf);
return node;
}
/**
*/
public static void main(String argv[]) throws Exception {
try {
StringUtils.startupShutdownMessage(HighTideNode.class, argv, LOG);
HighTideNode hightidenode = createHighTideNode(argv, null);
if (hightidenode != null) {
hightidenode.join();
}
} catch (Throwable e) {
LOG.error(StringUtils.stringifyException(e));
System.exit(-1);
}
}
}