blob: 2cc2a9931e18499e7b09d20b9a9c975f54408166 [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.hadoop.hdfs.server.namenode;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.fs.ContentSummary;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.fs.permission.PermissionStatus;
import org.apache.hadoop.hdfs.DFSUtil;
import org.apache.hadoop.hdfs.protocol.Block;
import org.apache.hadoop.hdfs.protocol.QuotaExceededException;
import org.apache.hadoop.hdfs.server.namenode.INodeReference.DstReference;
import org.apache.hadoop.hdfs.server.namenode.INodeReference.WithName;
import org.apache.hadoop.hdfs.server.namenode.snapshot.FileWithSnapshot;
import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeDirectoryWithSnapshot;
import org.apache.hadoop.hdfs.server.namenode.snapshot.Snapshot;
import org.apache.hadoop.hdfs.util.Diff;
import org.apache.hadoop.util.StringUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
/**
* We keep an in-memory representation of the file/block hierarchy.
* This is a base INode class containing common fields for file and
* directory inodes.
*/
@InterfaceAudience.Private
public abstract class INode implements Diff.Element<byte[]> {
public static final Log LOG = LogFactory.getLog(INode.class);
/** parent is either an {@link INodeDirectory} or an {@link INodeReference}.*/
private INode parent = null;
INode(INode parent) {
this.parent = parent;
}
/** Get inode id */
public abstract long getId();
/**
* Check whether this is the root inode.
*/
final boolean isRoot() {
return getLocalNameBytes().length == 0;
}
/** Get the {@link PermissionStatus} */
abstract PermissionStatus getPermissionStatus(Snapshot snapshot);
/** The same as getPermissionStatus(null). */
final PermissionStatus getPermissionStatus() {
return getPermissionStatus(null);
}
/**
* @param snapshot
* if it is not null, get the result from the given snapshot;
* otherwise, get the result from the current inode.
* @return user name
*/
abstract String getUserName(Snapshot snapshot);
/** The same as getUserName(null). */
public final String getUserName() {
return getUserName(null);
}
/** Set user */
abstract void setUser(String user);
/** Set user */
final INode setUser(String user, Snapshot latest, INodeMap inodeMap)
throws QuotaExceededException {
final INode nodeToUpdate = recordModification(latest, inodeMap);
nodeToUpdate.setUser(user);
return nodeToUpdate;
}
/**
* @param snapshot
* if it is not null, get the result from the given snapshot;
* otherwise, get the result from the current inode.
* @return group name
*/
abstract String getGroupName(Snapshot snapshot);
/** The same as getGroupName(null). */
public final String getGroupName() {
return getGroupName(null);
}
/** Set group */
abstract void setGroup(String group);
/** Set group */
final INode setGroup(String group, Snapshot latest, INodeMap inodeMap)
throws QuotaExceededException {
final INode nodeToUpdate = recordModification(latest, inodeMap);
nodeToUpdate.setGroup(group);
return nodeToUpdate;
}
/**
* @param snapshot
* if it is not null, get the result from the given snapshot;
* otherwise, get the result from the current inode.
* @return permission.
*/
abstract FsPermission getFsPermission(Snapshot snapshot);
/** The same as getFsPermission(null). */
public final FsPermission getFsPermission() {
return getFsPermission(null);
}
/** Set the {@link FsPermission} of this {@link INode} */
abstract void setPermission(FsPermission permission);
/** Set the {@link FsPermission} of this {@link INode} */
INode setPermission(FsPermission permission, Snapshot latest,
INodeMap inodeMap) throws QuotaExceededException {
final INode nodeToUpdate = recordModification(latest, inodeMap);
nodeToUpdate.setPermission(permission);
return nodeToUpdate;
}
/**
* @return if the given snapshot is null, return this;
* otherwise return the corresponding snapshot inode.
*/
public INode getSnapshotINode(final Snapshot snapshot) {
return this;
}
/** Is this inode in the latest snapshot? */
public final boolean isInLatestSnapshot(final Snapshot latest) {
if (latest == null) {
return false;
}
// if parent is a reference node, parent must be a renamed node. We can
// stop the check at the reference node.
if (parent != null && parent.isReference()) {
return true;
}
final INodeDirectory parentDir = getParent();
if (parentDir == null) { // root
return true;
}
if (!parentDir.isInLatestSnapshot(latest)) {
return false;
}
final INode child = parentDir.getChild(getLocalNameBytes(), latest);
if (this == child) {
return true;
}
if (child == null || !(child.isReference())) {
return false;
}
return this == child.asReference().getReferredINode();
}
/** @return true if the given inode is an ancestor directory of this inode. */
public final boolean isAncestorDirectory(final INodeDirectory dir) {
for(INodeDirectory p = getParent(); p != null; p = p.getParent()) {
if (p == dir) {
return true;
}
}
return false;
}
/**
* When {@link #recordModification} is called on a referred node,
* this method tells which snapshot the modification should be
* associated with: the snapshot that belongs to the SRC tree of the rename
* operation, or the snapshot belonging to the DST tree.
*
* @param latestInDst
* the latest snapshot in the DST tree above the reference node
* @return True: the modification should be recorded in the snapshot that
* belongs to the SRC tree. False: the modification should be
* recorded in the snapshot that belongs to the DST tree.
*/
public final boolean shouldRecordInSrcSnapshot(final Snapshot latestInDst) {
Preconditions.checkState(!isReference());
if (latestInDst == null) {
return true;
}
INodeReference withCount = getParentReference();
if (withCount != null) {
int dstSnapshotId = withCount.getParentReference().getDstSnapshotId();
if (dstSnapshotId >= latestInDst.getId()) {
return true;
}
}
return false;
}
/**
* This inode is being modified. The previous version of the inode needs to
* be recorded in the latest snapshot.
*
* @param latest the latest snapshot that has been taken.
* Note that it is null if no snapshots have been taken.
* @param inodeMap while recording modification, the inode or its parent may
* get replaced, and the inodeMap needs to be updated.
* @return The current inode, which usually is the same object of this inode.
* However, in some cases, this inode may be replaced with a new inode
* for maintaining snapshots. The current inode is then the new inode.
*/
abstract INode recordModification(final Snapshot latest,
final INodeMap inodeMap) throws QuotaExceededException;
/** Check whether it's a reference. */
public boolean isReference() {
return false;
}
/** Cast this inode to an {@link INodeReference}. */
public INodeReference asReference() {
throw new IllegalStateException("Current inode is not a reference: "
+ this.toDetailString());
}
/**
* Check whether it's a file.
*/
public boolean isFile() {
return false;
}
/** Cast this inode to an {@link INodeFile}. */
public INodeFile asFile() {
throw new IllegalStateException("Current inode is not a file: "
+ this.toDetailString());
}
/**
* Check whether it's a directory
*/
public boolean isDirectory() {
return false;
}
/** Cast this inode to an {@link INodeDirectory}. */
public INodeDirectory asDirectory() {
throw new IllegalStateException("Current inode is not a directory: "
+ this.toDetailString());
}
/**
* Check whether it's a symlink
*/
public boolean isSymlink() {
return false;
}
/** Cast this inode to an {@link INodeSymlink}. */
public INodeSymlink asSymlink() {
throw new IllegalStateException("Current inode is not a symlink: "
+ this.toDetailString());
}
/**
* Clean the subtree under this inode and collect the blocks from the descents
* for further block deletion/update. The current inode can either resides in
* the current tree or be stored as a snapshot copy.
*
* <pre>
* In general, we have the following rules.
* 1. When deleting a file/directory in the current tree, we have different
* actions according to the type of the node to delete.
*
* 1.1 The current inode (this) is an {@link INodeFile}.
* 1.1.1 If {@code prior} is null, there is no snapshot taken on ancestors
* before. Thus we simply destroy (i.e., to delete completely, no need to save
* snapshot copy) the current INode and collect its blocks for further
* cleansing.
* 1.1.2 Else do nothing since the current INode will be stored as a snapshot
* copy.
*
* 1.2 The current inode is an {@link INodeDirectory}.
* 1.2.1 If {@code prior} is null, there is no snapshot taken on ancestors
* before. Similarly, we destroy the whole subtree and collect blocks.
* 1.2.2 Else do nothing with the current INode. Recursively clean its
* children.
*
* 1.3 The current inode is a {@link FileWithSnapshot}.
* Call recordModification(..) to capture the current states.
* Mark the INode as deleted.
*
* 1.4 The current inode is a {@link INodeDirectoryWithSnapshot}.
* Call recordModification(..) to capture the current states.
* Destroy files/directories created after the latest snapshot
* (i.e., the inodes stored in the created list of the latest snapshot).
* Recursively clean remaining children.
*
* 2. When deleting a snapshot.
* 2.1 To clean {@link INodeFile}: do nothing.
* 2.2 To clean {@link INodeDirectory}: recursively clean its children.
* 2.3 To clean {@link FileWithSnapshot}: delete the corresponding snapshot in
* its diff list.
* 2.4 To clean {@link INodeDirectoryWithSnapshot}: delete the corresponding
* snapshot in its diff list. Recursively clean its children.
* </pre>
*
* @param snapshot
* The snapshot to delete. Null means to delete the current
* file/directory.
* @param prior
* The latest snapshot before the to-be-deleted snapshot. When
* deleting a current inode, this parameter captures the latest
* snapshot.
* @param collectedBlocks
* blocks collected from the descents for further block
* deletion/update will be added to the given map.
* @param removedINodes
* INodes collected from the descents for further cleaning up of
* inodeMap
* @return quota usage delta when deleting a snapshot
*/
public abstract Quota.Counts cleanSubtree(final Snapshot snapshot,
Snapshot prior, BlocksMapUpdateInfo collectedBlocks,
List<INode> removedINodes) throws QuotaExceededException;
/**
* Destroy self and clear everything! If the INode is a file, this method
* collects its blocks for further block deletion. If the INode is a
* directory, the method goes down the subtree and collects blocks from the
* descents, and clears its parent/children references as well. The method
* also clears the diff list if the INode contains snapshot diff list.
*
* @param collectedBlocks
* blocks collected from the descents for further block
* deletion/update will be added to this map.
* @param removedINodes
* INodes collected from the descents for further cleaning up of
* inodeMap
*/
public abstract void destroyAndCollectBlocks(
BlocksMapUpdateInfo collectedBlocks, List<INode> removedINodes);
/** Compute {@link ContentSummary}. */
public final ContentSummary computeContentSummary() {
final Content.Counts counts = computeContentSummary(
Content.Counts.newInstance());
return new ContentSummary(counts.get(Content.LENGTH),
counts.get(Content.FILE) + counts.get(Content.SYMLINK),
counts.get(Content.DIRECTORY), getNsQuota(),
counts.get(Content.DISKSPACE), getDsQuota());
}
/**
* Count subtree content summary with a {@link Content.Counts}.
*
* @param counts The subtree counts for returning.
* @return The same objects as the counts parameter.
*/
public abstract Content.Counts computeContentSummary(Content.Counts counts);
/**
* Check and add namespace/diskspace consumed to itself and the ancestors.
* @throws QuotaExceededException if quote is violated.
*/
public void addSpaceConsumed(long nsDelta, long dsDelta, boolean verify,
int snapshotId) throws QuotaExceededException {
if (parent != null) {
parent.addSpaceConsumed(nsDelta, dsDelta, verify, snapshotId);
}
}
public void addSpaceConsumedToRenameSrc(long nsDelta, long dsDelta,
boolean verify, int snapshotId) throws QuotaExceededException {
if (parent != null) {
parent.addSpaceConsumedToRenameSrc(nsDelta, dsDelta, verify, snapshotId);
}
}
/**
* Get the quota set for this inode
* @return the quota if it is set; -1 otherwise
*/
public long getNsQuota() {
return -1;
}
public long getDsQuota() {
return -1;
}
public final boolean isQuotaSet() {
return getNsQuota() >= 0 || getDsQuota() >= 0;
}
/**
* Count subtree {@link Quota#NAMESPACE} and {@link Quota#DISKSPACE} usages.
*/
public final Quota.Counts computeQuotaUsage() {
return computeQuotaUsage(new Quota.Counts(), true);
}
/**
* Count subtree {@link Quota#NAMESPACE} and {@link Quota#DISKSPACE} usages.
*
* With the existence of {@link INodeReference}, the same inode and its
* subtree may be referred by multiple {@link WithName} nodes and a
* {@link DstReference} node. To avoid circles while quota usage computation,
* we have the following rules:
*
* <pre>
* 1. For a {@link DstReference} node, since the node must be in the current
* tree (or has been deleted as the end point of a series of rename
* operations), we compute the quota usage of the referred node (and its
* subtree) in the regular manner, i.e., including every inode in the current
* tree and in snapshot copies, as well as the size of diff list.
*
* 2. For a {@link WithName} node, since the node must be in a snapshot, we
* only count the quota usage for those nodes that still existed at the
* creation time of the snapshot associated with the {@link WithName} node.
* We do not count in the size of the diff list.
* <pre>
*
* @param counts The subtree counts for returning.
* @param useCache Whether to use cached quota usage. Note that
* {@link WithName} node never uses cache for its subtree.
* @param lastSnapshotId {@link Snapshot#INVALID_ID} indicates the computation
* is in the current tree. Otherwise the id indicates
* the computation range for a {@link WithName} node.
* @return The same objects as the counts parameter.
*/
public abstract Quota.Counts computeQuotaUsage(Quota.Counts counts,
boolean useCache, int lastSnapshotId);
public final Quota.Counts computeQuotaUsage(Quota.Counts counts,
boolean useCache) {
return computeQuotaUsage(counts, useCache, Snapshot.INVALID_ID);
}
/**
* @return null if the local name is null; otherwise, return the local name.
*/
public final String getLocalName() {
final byte[] name = getLocalNameBytes();
return name == null? null: DFSUtil.bytes2String(name);
}
/**
* @return null if the local name is null;
* otherwise, return the local name byte array.
*/
public abstract byte[] getLocalNameBytes();
@Override
public final byte[] getKey() {
return getLocalNameBytes();
}
/**
* Set local file name
*/
public abstract void setLocalName(byte[] name);
public String getFullPathName() {
// Get the full path name of this inode.
return FSDirectory.getFullPathName(this);
}
@Override
public String toString() {
return getLocalName();
}
@VisibleForTesting
public final String getObjectString() {
return getClass().getSimpleName() + "@"
+ Integer.toHexString(super.hashCode());
}
/** @return a string description of the parent. */
@VisibleForTesting
public final String getParentString() {
final INodeReference parentRef = getParentReference();
if (parentRef != null) {
return "parentRef=" + parentRef.getLocalName() + "->";
} else {
final INodeDirectory parentDir = getParent();
if (parentDir != null) {
return "parentDir=" + parentDir.getLocalName() + "/";
} else {
return "parent=null";
}
}
}
@VisibleForTesting
public String toDetailString() {
return toString() + "(" + getObjectString() + "), " + getParentString();
}
/** @return the parent directory */
public final INodeDirectory getParent() {
return parent == null? null
: parent.isReference()? getParentReference().getParent(): parent.asDirectory();
}
/**
* @return the parent as a reference if this is a referred inode;
* otherwise, return null.
*/
public INodeReference getParentReference() {
return parent == null || !parent.isReference()? null: (INodeReference)parent;
}
/** Set parent directory */
public final void setParent(INodeDirectory parent) {
this.parent = parent;
}
/** Set container. */
public final void setParentReference(INodeReference parent) {
this.parent = parent;
}
/** Clear references to other objects. */
public void clear() {
setParent(null);
}
/**
* @param snapshot
* if it is not null, get the result from the given snapshot;
* otherwise, get the result from the current inode.
* @return modification time.
*/
abstract long getModificationTime(Snapshot snapshot);
/** The same as getModificationTime(null). */
public final long getModificationTime() {
return getModificationTime(null);
}
/** Update modification time if it is larger than the current value. */
public abstract INode updateModificationTime(long mtime, Snapshot latest,
INodeMap inodeMap) throws QuotaExceededException;
/** Set the last modification time of inode. */
public abstract void setModificationTime(long modificationTime);
/** Set the last modification time of inode. */
public final INode setModificationTime(long modificationTime,
Snapshot latest, INodeMap inodeMap) throws QuotaExceededException {
final INode nodeToUpdate = recordModification(latest, inodeMap);
nodeToUpdate.setModificationTime(modificationTime);
return nodeToUpdate;
}
/**
* @param snapshot
* if it is not null, get the result from the given snapshot;
* otherwise, get the result from the current inode.
* @return access time
*/
abstract long getAccessTime(Snapshot snapshot);
/** The same as getAccessTime(null). */
public final long getAccessTime() {
return getAccessTime(null);
}
/**
* Set last access time of inode.
*/
public abstract void setAccessTime(long accessTime);
/**
* Set last access time of inode.
*/
public final INode setAccessTime(long accessTime, Snapshot latest,
INodeMap inodeMap) throws QuotaExceededException {
final INode nodeToUpdate = recordModification(latest, inodeMap);
nodeToUpdate.setAccessTime(accessTime);
return nodeToUpdate;
}
/**
* Breaks file path into components.
* @param path
* @return array of byte arrays each of which represents
* a single path component.
*/
static byte[][] getPathComponents(String path) {
return getPathComponents(getPathNames(path));
}
/** Convert strings to byte arrays for path components. */
static byte[][] getPathComponents(String[] strings) {
if (strings.length == 0) {
return new byte[][]{null};
}
byte[][] bytes = new byte[strings.length][];
for (int i = 0; i < strings.length; i++)
bytes[i] = DFSUtil.string2Bytes(strings[i]);
return bytes;
}
/**
* Splits an absolute path into an array of path components.
* @param path
* @throws AssertionError if the given path is invalid.
* @return array of path components.
*/
static String[] getPathNames(String path) {
if (path == null || !path.startsWith(Path.SEPARATOR)) {
throw new AssertionError("Absolute path required");
}
return StringUtils.split(path, Path.SEPARATOR_CHAR);
}
@Override
public final int compareTo(byte[] bytes) {
return DFSUtil.compareBytes(getLocalNameBytes(), bytes);
}
@Override
public final boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null || !(that instanceof INode)) {
return false;
}
return getId() == ((INode) that).getId();
}
@Override
public final int hashCode() {
long id = getId();
return (int)(id^(id>>>32));
}
/**
* Dump the subtree starting from this inode.
* @return a text representation of the tree.
*/
@VisibleForTesting
public final StringBuffer dumpTreeRecursively() {
final StringWriter out = new StringWriter();
dumpTreeRecursively(new PrintWriter(out, true), new StringBuilder(), null);
return out.getBuffer();
}
@VisibleForTesting
public final void dumpTreeRecursively(PrintStream out) {
dumpTreeRecursively(new PrintWriter(out, true), new StringBuilder(), null);
}
/**
* Dump tree recursively.
* @param prefix The prefix string that each line should print.
*/
@VisibleForTesting
public void dumpTreeRecursively(PrintWriter out, StringBuilder prefix,
Snapshot snapshot) {
out.print(prefix);
out.print(" ");
final String name = getLocalName();
out.print(name.isEmpty()? "/": name);
out.print(" (");
out.print(getObjectString());
out.print("), ");
out.print(getParentString());
out.print(", " + getPermissionStatus(snapshot));
}
/**
* Information used for updating the blocksMap when deleting files.
*/
public static class BlocksMapUpdateInfo {
/**
* The list of blocks that need to be removed from blocksMap
*/
private List<Block> toDeleteList;
public BlocksMapUpdateInfo(List<Block> toDeleteList) {
this.toDeleteList = toDeleteList == null ? new ArrayList<Block>()
: toDeleteList;
}
public BlocksMapUpdateInfo() {
toDeleteList = new ArrayList<Block>();
}
/**
* @return The list of blocks that need to be removed from blocksMap
*/
public List<Block> getToDeleteList() {
return toDeleteList;
}
/**
* Add a to-be-deleted block into the
* {@link BlocksMapUpdateInfo#toDeleteList}
* @param toDelete the to-be-deleted block
*/
public void addDeleteBlock(Block toDelete) {
if (toDelete != null) {
toDeleteList.add(toDelete);
}
}
/**
* Clear {@link BlocksMapUpdateInfo#toDeleteList}
*/
public void clear() {
toDeleteList.clear();
}
}
}