/**
* 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.qjournal.server;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.hdfs.server.common.HdfsConstants.NodeType;
import org.apache.hadoop.hdfs.server.common.HdfsConstants.StartupOption;
import org.apache.hadoop.hdfs.server.common.InconsistentFSStateException;
import org.apache.hadoop.hdfs.server.common.Storage;
import org.apache.hadoop.hdfs.server.common.StorageErrorReporter;
import org.apache.hadoop.hdfs.server.common.StorageInfo;
import org.apache.hadoop.hdfs.server.namenode.FileJournalManager;
import org.apache.hadoop.hdfs.server.namenode.NNStorage;
import org.apache.hadoop.hdfs.server.namenode.NNStorage.NameNodeDirType;
import org.apache.hadoop.hdfs.server.namenode.NNStorage.NameNodeFile;
import org.apache.hadoop.hdfs.server.namenode.NNStorageDirectoryRetentionManager;
import org.apache.hadoop.hdfs.server.protocol.NamespaceInfo;
import org.apache.hadoop.hdfs.util.InjectionEvent;
import org.apache.hadoop.util.InjectionEventI;
import org.apache.hadoop.util.InjectionHandler;
import com.google.common.collect.ImmutableList;
/**
* A {@link Storage} implementation for the {@link JournalNode}.
*
* The JN has a storage directory for each namespace for which it stores
* metadata. There is only a single directory per JN in the current design.
* It is used for managing storage directories for both edits and image.
*/
public class JNStorage extends Storage {
private final StorageDirectory sd;
private StorageState state;
private final boolean isImageDir;
private Configuration conf;
private static final List<Pattern> EDITS_CURRENT_DIR_PURGE_REGEXES =
ImmutableList.of(
Pattern.compile("edits_\\d+-(\\d+)"),
Pattern.compile("edits_inprogress_(\\d+)(?:\\..*)?"));
private static final List<Pattern> IMAGE_CURRENT_DIR_PURGE_REGEXES =
ImmutableList.of(
Pattern.compile(NameNodeFile.IMAGE.getName() + "_(-?\\d+)"),
Pattern.compile(NameNodeFile.IMAGE_NEW.getName() + "_(-?\\d+)"),
Pattern.compile(NameNodeFile.IMAGE.getName() + "_(-?\\d+)\\.md5"));
private static final List<Pattern> PAXOS_DIR_PURGE_REGEXES =
ImmutableList.of(Pattern.compile("(\\d+)"));
static final String PAXOS_DIR = "paxos";
/**
* @param logDir the path to the directory in which data will be stored
* @param errorReporter a callback to report errors
* @throws IOException
*/
protected JNStorage(File logDir, StorageErrorReporter errorReporter,
boolean imageDir, Configuration conf) throws IOException {
super(NodeType.JOURNAL_NODE);
this.conf = conf;
sd = new StorageDirectory(logDir, imageDir ? NameNodeDirType.IMAGE
: NameNodeDirType.EDITS);
this.addStorageDir(sd);
this.isImageDir = imageDir;
// null for metrics, will be extended
analyzeStorage();
}
/**
* Find an edits file spanning the given transaction ID range.
* If no such file exists, an exception is thrown.
*/
File findFinalizedEditsFile(long startTxId, long endTxId) throws IOException {
File ret = new File(sd.getCurrentDir(),
NNStorage.getFinalizedEditsFileName(startTxId, endTxId));
if (!ret.exists()) {
throw new IOException(
"No edits file for range " + startTxId + "-" + endTxId);
}
return ret;
}
/**
* @return the path for an in-progress edits file starting at the given
* transaction ID. This does not verify existence of the file.
*/
File getInProgressEditLog(long startTxId) {
return new File(sd.getCurrentDir(),
NNStorage.getInProgressEditsFileName(startTxId));
}
public File getCheckpointImageFile(long txid) {
return NNStorage.getCheckpointImageFile(sd, txid);
}
public File getImageFile(long txid) {
return NNStorage.getImageFile(sd, txid);
}
/**
* @param segmentTxId the first txid of the segment
* @param epoch the epoch number of the writer which is coordinating
* recovery
* @return the temporary path in which an edits log should be stored
* while it is being downloaded from a remote JournalNode
*/
File getSyncLogTemporaryFile(long segmentTxId, long epoch) {
String name = NNStorage.getInProgressEditsFileName(segmentTxId) +
".epoch=" + epoch;
return new File(sd.getCurrentDir(), name);
}
/**
* Get name for temporary file used for log syncing, after a journal node
* crashed.
*/
File getSyncLogTemporaryFile(long segmentTxId, long endTxId, long stamp) {
String name = NNStorage.getFinalizedEditsFileName(segmentTxId, endTxId) +
".tmp=" + stamp;
return new File(sd.getCurrentDir(), name);
}
/**
* Get name for destination file used for log syncing, after a journal node
* crashed.
*/
File getSyncLogDestFile(long segmentTxId, long endTxId) {
String name = NNStorage.getFinalizedEditsFileName(segmentTxId, endTxId);
return new File(sd.getCurrentDir(), name);
}
/**
* @return the path for the file which contains persisted data for the
* paxos-like recovery process for the given log segment.
*/
File getPaxosFile(long segmentTxId) throws IOException {
if (isImageDir) {
throwIOException("Paxos data is not present in image directory");
}
return new File(getPaxosDir(), String.valueOf(segmentTxId));
}
File getPaxosDir() throws IOException {
if (isImageDir) {
throwIOException("Paxos data is not present in image directory");
}
return new File(sd.getCurrentDir(), PAXOS_DIR);
}
/**
* Remove any log files and associated paxos files which are older than
* the given txid.
*/
void purgeDataOlderThan(long minTxIdToKeep) throws IOException {
if (isImageDir) {
purgeMatching(sd.getCurrentDir(), IMAGE_CURRENT_DIR_PURGE_REGEXES,
minTxIdToKeep);
} else {
purgeMatching(sd.getCurrentDir(), EDITS_CURRENT_DIR_PURGE_REGEXES,
minTxIdToKeep);
purgeMatching(getPaxosDir(), PAXOS_DIR_PURGE_REGEXES, minTxIdToKeep);
}
}
/**
* Purge files in the given directory which match any of the set of patterns.
* The patterns must have a single numeric capture group which determines
* the associated transaction ID of the file. Only those files for which
* the transaction ID is less than the <code>minTxIdToKeep</code> parameter
* are removed.
*/
private static void purgeMatching(File dir, List<Pattern> patterns,
long minTxIdToKeep) throws IOException {
for (File f : FileUtil.listFiles(dir)) {
if (!f.isFile()) continue;
for (Pattern p : patterns) {
Matcher matcher = p.matcher(f.getName());
if (matcher.matches()) {
// This parsing will always succeed since the group(1) is
// /\d+/ in the regex itself.
long txid = Long.valueOf(matcher.group(1));
if (txid < minTxIdToKeep) {
LOG.info("Purging no-longer needed file " + txid);
if (!f.delete()) {
LOG.warn("Unable to delete no-longer-needed data " +
f);
}
break;
}
}
}
}
}
/*
* Allow us to backup the storage directory before formatting.
*/
void backupDirs() throws IOException {
File rootDir = sd.getRootDir();
FileSystem localFs = FileSystem.getLocal(conf).getRaw();
NNStorageDirectoryRetentionManager.backupFiles(localFs, rootDir, conf);
}
void format(NamespaceInfo nsInfo) throws IOException {
setStorageInfo(nsInfo);
LOG.info("Formatting journal " + sd + " with nsid: " + getNamespaceID());
InjectionHandler.processEvent(InjectionEvent.JOURNAL_STORAGE_FORMAT, sd);
// Unlock the directory before formatting, because we will
// re-analyze it after format(). The analyzeStorage() call
// below is reponsible for re-locking it. This is a no-op
// if the storage is not currently locked.
unlockAll();
sd.clearDirectory();
sd.write();
if (!isImageDir) {
// paxos data is present in the edit directory
if (!getPaxosDir().mkdirs()) {
throw new IOException("Could not create paxos dir: " + getPaxosDir());
}
}
analyzeStorage();
}
void finalizeStorage() throws IOException {
NNStorage.finalize(sd, this.getLayoutVersion(), this.getCTime());
}
void rollback(NamespaceInfo nsInfo) throws IOException {
setStorageInfo(nsInfo);
LOG.info("Rolling back journal " + sd + " with nsid: " + getNamespaceID());
if (!NNStorage.canRollBack(sd, this)) {
throw new IOException("Cannot rollback journal : " + sd);
}
NNStorage.doRollBack(sd, this);
}
void doUpgrade(NamespaceInfo nsInfo) throws IOException {
setStorageInfo(nsInfo);
LOG.info("Upgrading journal " + sd + " with nsid: " + getNamespaceID());
Storage.upgradeDirectory(sd);
}
void recover(StartupOption startOpt) throws IOException {
LOG.info("Recovering journal " + sd + " with nsid: " + getNamespaceID());
// Unlock the directory before formatting, because we will
// re-analyze it after format(). The analyzeStorage() call
// below is reponsible for re-locking it. This is a no-op
// if the storage is not currently locked.
unlockAll();
try {
StorageState curState = sd.analyzeStorage(startOpt);
NNStorage.recoverDirectory(sd, startOpt, curState, false);
} catch (IOException ioe) {
sd.unlock();
throw ioe;
}
}
void completeUpgrade(NamespaceInfo nsInfo) throws IOException {
setStorageInfo(nsInfo);
LOG.info("Upgrading journal " + sd + " with nsid: " + getNamespaceID());
Storage.completeUpgrade(sd);
}
void analyzeStorage() throws IOException {
this.state = sd.analyzeStorage(StartupOption.REGULAR);
if (state == StorageState.NORMAL) {
sd.read();
}
}
void checkConsistentNamespace(StorageInfo nsInfo)
throws IOException {
if (nsInfo.getNamespaceID() != getNamespaceID()) {
throw new IOException("Incompatible namespaceID for journal " +
this.sd + ": NameNode has nsId " + nsInfo.getNamespaceID() +
" but storage has nsId " + getNamespaceID());
}
}
public void close() throws IOException {
LOG.info("Closing journal storage for " + sd);
unlockAll();
}
public boolean isFormatted() {
return state == StorageState.NORMAL;
}
@Override
public boolean isConversionNeeded(StorageDirectory sd) throws IOException {
return false;
}
@Override
protected void corruptPreUpgradeStorage(File rootDir) throws IOException {
}
StorageDirectory getStorageDirectory() {
return sd;
}
StorageState getStorageState() {
return state;
}
StorageInfo getStorageInfo() {
return new StorageInfo(getLayoutVersion(), getNamespaceID(), getCTime());
}
static void throwIOException(String msg) throws IOException {
LOG.error(msg);
throw new IOException(msg);
}
}