/**
* 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.namenode;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Comparator;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.hdfs.server.common.HdfsConstants;
import org.apache.hadoop.hdfs.server.common.HdfsConstants.StartupOption;
import org.apache.hadoop.hdfs.server.common.HdfsConstants.Transition;
import org.apache.hadoop.hdfs.server.common.StorageErrorReporter;
import org.apache.hadoop.hdfs.server.common.Storage.StorageDirectory;
import org.apache.hadoop.hdfs.server.common.StorageInfo;
import org.apache.hadoop.hdfs.server.namenode.FSEditLogLoader.EditLogValidation;
import org.apache.hadoop.hdfs.server.namenode.NNStorage.NameNodeFile;
import org.apache.hadoop.hdfs.server.namenode.NNStorageRetentionManager.StoragePurger;
import org.apache.hadoop.hdfs.server.namenode.metrics.NameNodeMetrics;
import org.apache.hadoop.hdfs.server.protocol.RemoteEditLog;
import org.apache.hadoop.hdfs.server.protocol.RemoteEditLogManifest;
import com.google.common.base.Joiner;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
/**
* Journal manager for the common case of edits files being written
* to a storage directory.
*
* Note: this class is not thread-safe and should be externally
* synchronized.
*/
public class FileJournalManager implements JournalManager {
private static final Log LOG = LogFactory.getLog(FileJournalManager.class);
private final StorageDirectory sd;
private final NameNodeMetrics metrics;
private final StorageErrorReporter errorReporter;
public static final Pattern EDITS_REGEX = Pattern.compile(
NameNodeFile.EDITS.getName() + "_(\\d+)-(\\d+)");
public static final Pattern EDITS_INPROGRESS_REGEX = Pattern.compile(
NameNodeFile.EDITS_INPROGRESS.getName() + "_(\\d+)");
private File currentInProgress = null;
private long maxSeenTransaction = -1;
StoragePurger purger
= new NNStorageRetentionManager.DeletionStoragePurger();
public FileJournalManager(StorageDirectory sd) {
this(sd, null, null);
}
public FileJournalManager(StorageDirectory sd, NameNodeMetrics metrics,
StorageErrorReporter errorReporter) {
this.sd = sd;
this.metrics = metrics;
this.errorReporter = errorReporter;
}
@Override
public void close() throws IOException {}
@Override
synchronized public EditLogOutputStream startLogSegment(long txid)
throws IOException {
try {
currentInProgress = NNStorage.getInProgressEditsFile(sd, txid);
EditLogOutputStream stm = new EditLogFileOutputStream(currentInProgress,
metrics);
stm.create();
return stm;
} catch (IOException e) {
LOG.warn("Unable to start log segment " + txid + " at "
+ currentInProgress + ": " + e.getLocalizedMessage());
reportErrorOnFile(currentInProgress);
throw e;
}
}
@Override
synchronized public void finalizeLogSegment(long firstTxId, long lastTxId)
throws IOException {
File inprogressFile = NNStorage.getInProgressEditsFile(sd, firstTxId);
File dstFile = NNStorage.getFinalizedEditsFile(
sd, firstTxId, lastTxId);
LOG.info("Finalizing edits file " + inprogressFile + " -> " + dstFile);
if(dstFile.exists()) {
throw new IllegalStateException("Can't finalize edits file "
+ inprogressFile + " since finalized file " + "already exists");
}
if (!inprogressFile.renameTo(dstFile)) {
reportErrorOnFile(dstFile);
throw new IOException("Unable to finalize edits file " + inprogressFile);
}
if (inprogressFile.equals(currentInProgress)) {
currentInProgress = null;
}
}
void reportErrorOnFile(File f) {
if (errorReporter != null) {
errorReporter.reportErrorOnFile(f);
}
}
public StorageDirectory getStorageDirectory() {
return sd;
}
@Override
public void purgeLogsOlderThan(long minTxIdToKeep)
throws IOException {
File[] files = FileUtil.listFiles(sd.getCurrentDir());
List<EditLogFile> editLogs =
FileJournalManager.matchEditLogs(files);
for (EditLogFile log : editLogs) {
if (log.getFirstTxId() < minTxIdToKeep &&
log.getLastTxId() < minTxIdToKeep) {
LOG.info("Purging log: " + log);
purger.purgeLog(log);
}
}
}
@Override
public void setCommittedTxId(final long txid, final boolean force) {
}
/**
* Find all editlog segments starting at or above the given txid.
* Include inprogress segments. Notice that the segments do not have to be
* contiguous. JournalSet handles the holes between segments.
*
* @param fromTxId the txnid which to start looking
* @return a list of remote edit logs
* @throws IOException if edit logs cannot be listed.
*/
public RemoteEditLogManifest getEditLogManifest(long firstTxId) throws IOException {
File currentDir = sd.getCurrentDir();
List<EditLogFile> allLogFiles = matchEditLogs(
FileUtil.listFiles(currentDir));
if (LOG.isDebugEnabled()) {
LOG.debug(allLogFiles);
}
List<RemoteEditLog> ret = new ArrayList<RemoteEditLog>(
allLogFiles.size());
for (EditLogFile elf : allLogFiles) {
if (elf.isCorrupt()) continue;
if (elf.getFirstTxId() >= firstTxId) {
ret.add(new RemoteEditLog(elf.firstTxId,
elf.lastTxId,
elf.isInProgress));
} else if ((firstTxId > elf.getFirstTxId()) &&
(firstTxId <= elf.getLastTxId())) {
throw new IOException("Asked for firstTxId " + firstTxId
+ " which is in the middle of file " + elf.file);
}
}
Collections.sort(ret);
return new RemoteEditLogManifest(ret);
}
static List<EditLogFile> matchEditLogs(File[] filesInStorage) {
List<EditLogFile> ret = new ArrayList<EditLogFile>();
for (File f : filesInStorage) {
String name = f.getName();
// Check for edits
Matcher editsMatch = EDITS_REGEX.matcher(name);
if (editsMatch.matches()) {
try {
long startTxId = Long.valueOf(editsMatch.group(1));
long endTxId = Long.valueOf(editsMatch.group(2));
ret.add(new EditLogFile(f, startTxId, endTxId));
} catch (NumberFormatException nfe) {
LOG.error("Edits file " + f + " has improperly formatted " +
"transaction ID");
// skip
}
}
// Check for in-progress edits
Matcher inProgressEditsMatch = EDITS_INPROGRESS_REGEX.matcher(name);
if (inProgressEditsMatch.matches()) {
try {
long startTxId = Long.valueOf(inProgressEditsMatch.group(1));
ret.add(
new EditLogFile(f, startTxId, HdfsConstants.INVALID_TXID, true));
} catch (NumberFormatException nfe) {
LOG.error("In-progress edits file " + f + " has improperly " +
"formatted transaction ID");
// skip
}
}
}
return ret;
}
@Override
synchronized public void selectInputStreams(
Collection<EditLogInputStream> streams, long fromTxId,
boolean inProgressOk, boolean validateInProgressSegments)
throws IOException {
List<EditLogFile> elfs = matchEditLogs(FileUtil.listFiles(sd.getCurrentDir()));
if (LOG.isDebugEnabled()) {
LOG.debug(this + ": selecting input streams starting at " + fromTxId
+ (inProgressOk ? " (inProgress ok) " : " (excluding inProgress) ")
+ "from among " + elfs.size() + " candidate file(s)");
}
addStreamsToCollectionFromFiles(elfs, streams, fromTxId, inProgressOk,
validateInProgressSegments);
}
void addStreamsToCollectionFromFiles(Collection<EditLogFile> elfs,
Collection<EditLogInputStream> streams, long fromTxId,
boolean inProgressOk, boolean validateInProgressSegments)
throws IOException {
for (EditLogFile elf : elfs) {
if (elf.isInProgress()) {
if (!inProgressOk) {
LOG.debug("passing over " + elf + " because it is in progress "
+ "and we are ignoring in-progress logs.");
continue;
}
if (validateInProgressSegments) {
try {
elf.validateLog();
} catch (IOException e) {
LOG.error("got IOException while trying to validate header of "
+ elf + ". Skipping.", e);
continue;
}
} else {
LOG.info("Skipping validation of edit segment: " + elf);
}
}
if (elf.lastTxId != HdfsConstants.INVALID_TXID && elf.lastTxId < fromTxId) {
LOG.info("passing over " + elf + " because it ends at " + elf.lastTxId
+ ", but we only care about transactions " + "as new as "
+ fromTxId);
continue;
}
EditLogFileInputStream elfis = new EditLogFileInputStream(elf.getFile(),
elf.getFirstTxId(), elf.getLastTxId(), elf.isInProgress());
elfis.setJournalManager(this);
streams.add(elfis);
}
}
@Override
synchronized public void recoverUnfinalizedSegments() throws IOException {
File currentDir = sd.getCurrentDir();
List<EditLogFile> allLogFiles = matchEditLogs(currentDir.listFiles());
for(EditLogFile elf : allLogFiles) {
LOG.info("Found edit file: " + elf);
}
// make sure journal is aware of max seen transaction before moving corrupt
// files aside
findMaxTransaction();
for (EditLogFile elf : allLogFiles) {
if (elf.getFile().equals(currentInProgress)) {
continue;
}
if (elf.isInProgress()) {
// If the file is zero-length, we likely just crashed after opening the
// file, but before writing anything to it. Safe to delete it.
if (elf.getFile().length() == 0) {
LOG.info("Deleting zero-length edit log file " + elf);
if (!elf.getFile().delete()) {
throw new IOException("Unable to delete file " + elf.getFile());
}
continue;
}
elf.validateLog();
if (elf.isCorrupt()) {
elf.moveAsideCorruptFile();
continue;
}
finalizeLogSegment(elf.getFirstTxId(), elf.getLastTxId());
}
}
}
public List<EditLogFile> getLogFiles(long fromTxId) throws IOException {
return getLogFiles(fromTxId, true);
}
/**
* Get all edit log segments
*
* @param fromTxId starting txid
* @param enforceBoundary should we throw an exception if the requested
* txid is not the starting id of a segment
*/
public List<EditLogFile> getLogFiles(long fromTxId, boolean enforceBoundary)
throws IOException {
File currentDir = sd.getCurrentDir();
List<EditLogFile> allLogFiles = matchEditLogs(currentDir.listFiles());
List<EditLogFile> logFiles = new ArrayList<EditLogFile>();
for (EditLogFile elf : allLogFiles) {
if (enforceBoundary && fromTxId > elf.getFirstTxId()
&& fromTxId <= elf.getLastTxId()) {
throw new IOException("Asked for fromTxId " + fromTxId
+ " which is in middle of file " + elf.file);
}
if (fromTxId <= elf.getFirstTxId()) {
logFiles.add(elf);
}
}
Collections.sort(logFiles, EditLogFile.COMPARE_BY_START_TXID);
return logFiles;
}
/**
* Find the maximum transaction in the journal.
* This gets stored in a member variable, as corrupt edit logs
* will be moved aside, but we still need to remember their first
* tranaction id in the case that it was the maximum transaction in
* the journal.
*/
private long findMaxTransaction()
throws IOException {
for (EditLogFile elf : getLogFiles(0)) {
if (elf.isInProgress()) {
maxSeenTransaction = Math.max(elf.getFirstTxId(), maxSeenTransaction);
}
maxSeenTransaction = Math.max(elf.getLastTxId(), maxSeenTransaction);
}
return maxSeenTransaction;
}
@Override
public String toString() {
return String.format("file:/%s", sd.getRoot());
}
/**
* Record of an edit log that has been located and had its filename parsed.
*/
public static class EditLogFile {
private File file;
private final long firstTxId;
private long lastTxId;
private boolean isCorrupt = false;
private final boolean isInProgress;
final static Comparator<EditLogFile> COMPARE_BY_START_TXID
= new Comparator<EditLogFile>() {
public int compare(EditLogFile a, EditLogFile b) {
return ComparisonChain.start()
.compare(a.getFirstTxId(), b.getFirstTxId())
.compare(a.getLastTxId(), b.getLastTxId())
.result();
}
};
EditLogFile(File file,
long firstTxId, long lastTxId) {
this(file, firstTxId, lastTxId, false);
assert (lastTxId != HdfsConstants.INVALID_TXID)
&& (lastTxId >= firstTxId);
}
EditLogFile(File file, long firstTxId,
long lastTxId, boolean isInProgress) {
boolean checkTxIds = true;
checkTxIds &= ((lastTxId == HdfsConstants.INVALID_TXID && isInProgress)
|| (lastTxId != HdfsConstants.INVALID_TXID && lastTxId >= firstTxId));
checkTxIds &= ((firstTxId > -1) || (firstTxId == HdfsConstants.INVALID_TXID));
if (!checkTxIds)
throw new IllegalArgumentException("Illegal transaction ids: "
+ firstTxId + ", " + lastTxId + " in progress: " + isInProgress);
if(file == null)
throw new IllegalArgumentException("File can not be NULL");
this.firstTxId = firstTxId;
this.lastTxId = lastTxId;
this.file = file;
this.isInProgress = isInProgress;
}
public long getFirstTxId() {
return firstTxId;
}
public long getLastTxId() {
return lastTxId;
}
/**
* Count the number of valid transactions in a log.
* This will update the lastTxId of the EditLogFile or
* mark it as corrupt if it is.
*/
public void validateLog() throws IOException {
EditLogValidation val = EditLogFileInputStream.validateEditLog(file);
if (val.getNumTransactions() == 0) {
markCorrupt();
} else {
this.lastTxId = val.getEndTxId();
}
}
public boolean isInProgress() {
return isInProgress;
}
public File getFile() {
return file;
}
void markCorrupt() {
isCorrupt = true;
}
boolean isCorrupt() {
return isCorrupt;
}
public void moveAsideCorruptFile() throws IOException {
assert isCorrupt;
renameSelf(".corrupt");
}
public void moveAsideEmptyFile() throws IOException {
assert lastTxId == HdfsConstants.INVALID_TXID;
renameSelf(".empty");
}
private void renameSelf(String newSuffix) throws IOException {
File src = file;
File dst = new File(src.getParent(), src.getName() + newSuffix);
boolean success = src.renameTo(dst);
if (!success) {
throw new IOException(
"Couldn't rename log " + src + " to " + dst);
}
file = dst;
}
@Override
public String toString() {
return String.format("EditLogFile(file=%s,first=%019d,last=%019d,"
+"inProgress=%b,corrupt=%b)", file.toString(),
firstTxId, lastTxId, isInProgress(), isCorrupt);
}
public EditLogFile(String colonSeparated) {
String[] list = colonSeparated.split(":");
firstTxId = Long.valueOf(list[0]);
lastTxId = Long.valueOf(list[1]);
isInProgress = Boolean.valueOf(list[2]);
isCorrupt = Boolean.valueOf(list[3]);
}
public String toColonSeparatedString() {
Joiner joiner = Joiner.on(":");
return joiner.join(firstTxId, lastTxId, isInProgress, isCorrupt);
}
}
@Override
public void transitionJournal(StorageInfo si, Transition transition,
StartupOption startOpt) throws IOException {
// Transitioning file journals is done by the StorageDirectory
// format code, since they may share their directory with
// checkpoints, etc.
throw new UnsupportedOperationException();
}
@Override
public boolean hasSomeJournalData() {
// Formatting file journals is done by the StorageDirectory
// format code, since they may share their directory with
// checkpoints, etc.
throw new UnsupportedOperationException();
}
@Override
public boolean hasSomeImageData() {
// Formatting file journals is done by the StorageDirectory
// format code, since they may share their directory with
// checkpoints, etc.
throw new UnsupportedOperationException();
}
public EditLogFile getLogFile(long startTxId) throws IOException {
return getLogFile(sd.getCurrentDir(), startTxId);
}
public static EditLogFile getLogFile(File dir, long startTxId)
throws IOException {
List<EditLogFile> files = matchEditLogs(FileUtil.listFiles(dir));
List<EditLogFile> ret = Lists.newLinkedList();
for (EditLogFile elf : files) {
if (elf.getFirstTxId() == startTxId) {
ret.add(elf);
}
}
if (ret.isEmpty()) {
// no matches
return null;
} else if (ret.size() == 1) {
return ret.get(0);
} else {
throw new IllegalStateException("More than one log segment in " +
dir + " starting at txid " + startTxId + ": " +
Joiner.on(", ").join(ret));
}
}
@Override
public String toHTMLString() {
return this.toString();
}
@Override
public boolean hasImageStorage() {
// FileJournalManager is not used directly to handle images.
// We use NNStorage instead
return false;
}
@Override
public RemoteStorageState analyzeJournalStorage() {
// this is done directly through storage directory
throw new UnsupportedOperationException();
}
}