001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hdfs.server.namenode.snapshot; 019 020import java.io.IOException; 021import java.io.PrintWriter; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.HashMap; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Map; 029import java.util.SortedMap; 030import java.util.TreeMap; 031 032import org.apache.hadoop.HadoopIllegalArgumentException; 033import org.apache.hadoop.classification.InterfaceAudience; 034import org.apache.hadoop.hdfs.DFSUtil; 035import org.apache.hadoop.hdfs.protocol.QuotaExceededException; 036import org.apache.hadoop.hdfs.protocol.SnapshotDiffReport; 037import org.apache.hadoop.hdfs.protocol.SnapshotDiffReport.DiffReportEntry; 038import org.apache.hadoop.hdfs.protocol.SnapshotDiffReport.DiffType; 039import org.apache.hadoop.hdfs.protocol.SnapshotException; 040import org.apache.hadoop.hdfs.server.namenode.Content; 041import org.apache.hadoop.hdfs.server.namenode.ContentSummaryComputationContext; 042import org.apache.hadoop.hdfs.server.namenode.INode; 043import org.apache.hadoop.hdfs.server.namenode.INodeDirectory; 044import org.apache.hadoop.hdfs.server.namenode.INodeFile; 045import org.apache.hadoop.hdfs.server.namenode.INodeMap; 046import org.apache.hadoop.hdfs.server.namenode.Quota; 047import org.apache.hadoop.hdfs.server.namenode.snapshot.DirectoryWithSnapshotFeature.ChildrenDiff; 048import org.apache.hadoop.hdfs.server.namenode.snapshot.DirectoryWithSnapshotFeature.DirectoryDiff; 049import org.apache.hadoop.hdfs.util.Diff.ListType; 050import org.apache.hadoop.hdfs.util.ReadOnlyList; 051import org.apache.hadoop.util.Time; 052 053import com.google.common.base.Preconditions; 054import com.google.common.primitives.SignedBytes; 055 056/** 057 * Directories where taking snapshots is allowed. 058 * 059 * Like other {@link INode} subclasses, this class is synchronized externally 060 * by the namesystem and FSDirectory locks. 061 */ 062@InterfaceAudience.Private 063public class INodeDirectorySnapshottable extends INodeDirectory { 064 /** Limit the number of snapshot per snapshottable directory. */ 065 static final int SNAPSHOT_LIMIT = 1 << 16; 066 067 /** Cast INode to INodeDirectorySnapshottable. */ 068 static public INodeDirectorySnapshottable valueOf( 069 INode inode, String src) throws IOException { 070 final INodeDirectory dir = INodeDirectory.valueOf(inode, src); 071 if (!dir.isSnapshottable()) { 072 throw new SnapshotException( 073 "Directory is not a snapshottable directory: " + src); 074 } 075 return (INodeDirectorySnapshottable)dir; 076 } 077 078 /** 079 * A class describing the difference between snapshots of a snapshottable 080 * directory. 081 */ 082 public static class SnapshotDiffInfo { 083 /** Compare two inodes based on their full names */ 084 public static final Comparator<INode> INODE_COMPARATOR = 085 new Comparator<INode>() { 086 @Override 087 public int compare(INode left, INode right) { 088 if (left == null) { 089 return right == null ? 0 : -1; 090 } else { 091 if (right == null) { 092 return 1; 093 } else { 094 int cmp = compare(left.getParent(), right.getParent()); 095 return cmp == 0 ? SignedBytes.lexicographicalComparator().compare( 096 left.getLocalNameBytes(), right.getLocalNameBytes()) : cmp; 097 } 098 } 099 } 100 }; 101 102 /** The root directory of the snapshots */ 103 private final INodeDirectorySnapshottable snapshotRoot; 104 /** The starting point of the difference */ 105 private final Snapshot from; 106 /** The end point of the difference */ 107 private final Snapshot to; 108 /** 109 * A map recording modified INodeFile and INodeDirectory and their relative 110 * path corresponding to the snapshot root. Sorted based on their names. 111 */ 112 private final SortedMap<INode, byte[][]> diffMap = 113 new TreeMap<INode, byte[][]>(INODE_COMPARATOR); 114 /** 115 * A map capturing the detailed difference about file creation/deletion. 116 * Each key indicates a directory whose children have been changed between 117 * the two snapshots, while its associated value is a {@link ChildrenDiff} 118 * storing the changes (creation/deletion) happened to the children (files). 119 */ 120 private final Map<INodeDirectory, ChildrenDiff> dirDiffMap = 121 new HashMap<INodeDirectory, ChildrenDiff>(); 122 123 SnapshotDiffInfo(INodeDirectorySnapshottable snapshotRoot, Snapshot start, 124 Snapshot end) { 125 this.snapshotRoot = snapshotRoot; 126 this.from = start; 127 this.to = end; 128 } 129 130 /** Add a dir-diff pair */ 131 private void addDirDiff(INodeDirectory dir, byte[][] relativePath, 132 ChildrenDiff diff) { 133 dirDiffMap.put(dir, diff); 134 diffMap.put(dir, relativePath); 135 } 136 137 /** Add a modified file */ 138 private void addFileDiff(INodeFile file, byte[][] relativePath) { 139 diffMap.put(file, relativePath); 140 } 141 142 /** @return True if {@link #from} is earlier than {@link #to} */ 143 private boolean isFromEarlier() { 144 return Snapshot.ID_COMPARATOR.compare(from, to) < 0; 145 } 146 147 /** 148 * Generate a {@link SnapshotDiffReport} based on detailed diff information. 149 * @return A {@link SnapshotDiffReport} describing the difference 150 */ 151 public SnapshotDiffReport generateReport() { 152 List<DiffReportEntry> diffReportList = new ArrayList<DiffReportEntry>(); 153 for (INode node : diffMap.keySet()) { 154 diffReportList.add(new DiffReportEntry(DiffType.MODIFY, diffMap 155 .get(node))); 156 if (node.isDirectory()) { 157 ChildrenDiff dirDiff = dirDiffMap.get(node); 158 List<DiffReportEntry> subList = dirDiff.generateReport( 159 diffMap.get(node), isFromEarlier()); 160 diffReportList.addAll(subList); 161 } 162 } 163 return new SnapshotDiffReport(snapshotRoot.getFullPathName(), 164 Snapshot.getSnapshotName(from), Snapshot.getSnapshotName(to), 165 diffReportList); 166 } 167 } 168 169 /** 170 * Snapshots of this directory in ascending order of snapshot names. 171 * Note that snapshots in ascending order of snapshot id are stored in 172 * {@link INodeDirectoryWithSnapshot}.diffs (a private field). 173 */ 174 private final List<Snapshot> snapshotsByNames = new ArrayList<Snapshot>(); 175 176 /** 177 * @return {@link #snapshotsByNames} 178 */ 179 ReadOnlyList<Snapshot> getSnapshotsByNames() { 180 return ReadOnlyList.Util.asReadOnlyList(this.snapshotsByNames); 181 } 182 183 /** Number of snapshots allowed. */ 184 private int snapshotQuota = SNAPSHOT_LIMIT; 185 186 public INodeDirectorySnapshottable(INodeDirectory dir) { 187 super(dir, true, dir.getFeatures()); 188 // add snapshot feature if the original directory does not have it 189 if (!isWithSnapshot()) { 190 addSnapshotFeature(null); 191 } 192 } 193 194 /** @return the number of existing snapshots. */ 195 public int getNumSnapshots() { 196 return snapshotsByNames.size(); 197 } 198 199 private int searchSnapshot(byte[] snapshotName) { 200 return Collections.binarySearch(snapshotsByNames, snapshotName); 201 } 202 203 /** @return the snapshot with the given name. */ 204 public Snapshot getSnapshot(byte[] snapshotName) { 205 final int i = searchSnapshot(snapshotName); 206 return i < 0? null: snapshotsByNames.get(i); 207 } 208 209 Snapshot getSnapshotById(int sid) { 210 for (Snapshot s : snapshotsByNames) { 211 if (s.getId() == sid) { 212 return s; 213 } 214 } 215 return null; 216 } 217 218 /** @return {@link #snapshotsByNames} as a {@link ReadOnlyList} */ 219 public ReadOnlyList<Snapshot> getSnapshotList() { 220 return ReadOnlyList.Util.asReadOnlyList(snapshotsByNames); 221 } 222 223 /** 224 * Rename a snapshot 225 * @param path 226 * The directory path where the snapshot was taken. Used for 227 * generating exception message. 228 * @param oldName 229 * Old name of the snapshot 230 * @param newName 231 * New name the snapshot will be renamed to 232 * @throws SnapshotException 233 * Throw SnapshotException when either the snapshot with the old 234 * name does not exist or a snapshot with the new name already 235 * exists 236 */ 237 public void renameSnapshot(String path, String oldName, String newName) 238 throws SnapshotException { 239 if (newName.equals(oldName)) { 240 return; 241 } 242 final int indexOfOld = searchSnapshot(DFSUtil.string2Bytes(oldName)); 243 if (indexOfOld < 0) { 244 throw new SnapshotException("The snapshot " + oldName 245 + " does not exist for directory " + path); 246 } else { 247 final byte[] newNameBytes = DFSUtil.string2Bytes(newName); 248 int indexOfNew = searchSnapshot(newNameBytes); 249 if (indexOfNew > 0) { 250 throw new SnapshotException("The snapshot " + newName 251 + " already exists for directory " + path); 252 } 253 // remove the one with old name from snapshotsByNames 254 Snapshot snapshot = snapshotsByNames.remove(indexOfOld); 255 final INodeDirectory ssRoot = snapshot.getRoot(); 256 ssRoot.setLocalName(newNameBytes); 257 indexOfNew = -indexOfNew - 1; 258 if (indexOfNew <= indexOfOld) { 259 snapshotsByNames.add(indexOfNew, snapshot); 260 } else { // indexOfNew > indexOfOld 261 snapshotsByNames.add(indexOfNew - 1, snapshot); 262 } 263 } 264 } 265 266 public int getSnapshotQuota() { 267 return snapshotQuota; 268 } 269 270 public void setSnapshotQuota(int snapshotQuota) { 271 if (snapshotQuota < 0) { 272 throw new HadoopIllegalArgumentException( 273 "Cannot set snapshot quota to " + snapshotQuota + " < 0"); 274 } 275 this.snapshotQuota = snapshotQuota; 276 } 277 278 @Override 279 public boolean isSnapshottable() { 280 return true; 281 } 282 283 /** 284 * Simply add a snapshot into the {@link #snapshotsByNames}. Used by FSImage 285 * loading. 286 */ 287 void addSnapshot(Snapshot snapshot) { 288 this.snapshotsByNames.add(snapshot); 289 } 290 291 /** Add a snapshot. */ 292 Snapshot addSnapshot(int id, String name) throws SnapshotException, 293 QuotaExceededException { 294 //check snapshot quota 295 final int n = getNumSnapshots(); 296 if (n + 1 > snapshotQuota) { 297 throw new SnapshotException("Failed to add snapshot: there are already " 298 + n + " snapshot(s) and the snapshot quota is " 299 + snapshotQuota); 300 } 301 final Snapshot s = new Snapshot(id, name, this); 302 final byte[] nameBytes = s.getRoot().getLocalNameBytes(); 303 final int i = searchSnapshot(nameBytes); 304 if (i >= 0) { 305 throw new SnapshotException("Failed to add snapshot: there is already a " 306 + "snapshot with the same name \"" + Snapshot.getSnapshotName(s) + "\"."); 307 } 308 309 final DirectoryDiff d = getDiffs().addDiff(id, this); 310 d.setSnapshotRoot(s.getRoot()); 311 snapshotsByNames.add(-i - 1, s); 312 313 //set modification time 314 updateModificationTime(Time.now(), Snapshot.CURRENT_STATE_ID); 315 s.getRoot().setModificationTime(getModificationTime(), 316 Snapshot.CURRENT_STATE_ID); 317 return s; 318 } 319 320 /** 321 * Remove the snapshot with the given name from {@link #snapshotsByNames}, 322 * and delete all the corresponding DirectoryDiff. 323 * 324 * @param snapshotName The name of the snapshot to be removed 325 * @param collectedBlocks Used to collect information to update blocksMap 326 * @return The removed snapshot. Null if no snapshot with the given name 327 * exists. 328 */ 329 Snapshot removeSnapshot(String snapshotName, 330 BlocksMapUpdateInfo collectedBlocks, final List<INode> removedINodes) 331 throws SnapshotException { 332 final int i = searchSnapshot(DFSUtil.string2Bytes(snapshotName)); 333 if (i < 0) { 334 throw new SnapshotException("Cannot delete snapshot " + snapshotName 335 + " from path " + this.getFullPathName() 336 + ": the snapshot does not exist."); 337 } else { 338 final Snapshot snapshot = snapshotsByNames.get(i); 339 int prior = Snapshot.findLatestSnapshot(this, snapshot.getId()); 340 try { 341 Quota.Counts counts = cleanSubtree(snapshot.getId(), prior, 342 collectedBlocks, removedINodes, true); 343 INodeDirectory parent = getParent(); 344 if (parent != null) { 345 // there will not be any WithName node corresponding to the deleted 346 // snapshot, thus only update the quota usage in the current tree 347 parent.addSpaceConsumed(-counts.get(Quota.NAMESPACE), 348 -counts.get(Quota.DISKSPACE), true); 349 } 350 } catch(QuotaExceededException e) { 351 LOG.error("BUG: removeSnapshot increases namespace usage.", e); 352 } 353 // remove from snapshotsByNames after successfully cleaning the subtree 354 snapshotsByNames.remove(i); 355 return snapshot; 356 } 357 } 358 359 @Override 360 public ContentSummaryComputationContext computeContentSummary( 361 final ContentSummaryComputationContext summary) { 362 super.computeContentSummary(summary); 363 summary.getCounts().add(Content.SNAPSHOT, snapshotsByNames.size()); 364 summary.getCounts().add(Content.SNAPSHOTTABLE_DIRECTORY, 1); 365 return summary; 366 } 367 368 /** 369 * Compute the difference between two snapshots (or a snapshot and the current 370 * directory) of the directory. 371 * 372 * @param from The name of the start point of the comparison. Null indicating 373 * the current tree. 374 * @param to The name of the end point. Null indicating the current tree. 375 * @return The difference between the start/end points. 376 * @throws SnapshotException If there is no snapshot matching the starting 377 * point, or if endSnapshotName is not null but cannot be identified 378 * as a previous snapshot. 379 */ 380 SnapshotDiffInfo computeDiff(final String from, final String to) 381 throws SnapshotException { 382 Snapshot fromSnapshot = getSnapshotByName(from); 383 Snapshot toSnapshot = getSnapshotByName(to); 384 // if the start point is equal to the end point, return null 385 if (from.equals(to)) { 386 return null; 387 } 388 SnapshotDiffInfo diffs = new SnapshotDiffInfo(this, fromSnapshot, 389 toSnapshot); 390 computeDiffRecursively(this, new ArrayList<byte[]>(), diffs); 391 return diffs; 392 } 393 394 /** 395 * Find the snapshot matching the given name. 396 * 397 * @param snapshotName The name of the snapshot. 398 * @return The corresponding snapshot. Null if snapshotName is null or empty. 399 * @throws SnapshotException If snapshotName is not null or empty, but there 400 * is no snapshot matching the name. 401 */ 402 private Snapshot getSnapshotByName(String snapshotName) 403 throws SnapshotException { 404 Snapshot s = null; 405 if (snapshotName != null && !snapshotName.isEmpty()) { 406 final int index = searchSnapshot(DFSUtil.string2Bytes(snapshotName)); 407 if (index < 0) { 408 throw new SnapshotException("Cannot find the snapshot of directory " 409 + this.getFullPathName() + " with name " + snapshotName); 410 } 411 s = snapshotsByNames.get(index); 412 } 413 return s; 414 } 415 416 /** 417 * Recursively compute the difference between snapshots under a given 418 * directory/file. 419 * @param node The directory/file under which the diff is computed. 420 * @param parentPath Relative path (corresponding to the snapshot root) of 421 * the node's parent. 422 * @param diffReport data structure used to store the diff. 423 */ 424 private void computeDiffRecursively(INode node, List<byte[]> parentPath, 425 SnapshotDiffInfo diffReport) { 426 ChildrenDiff diff = new ChildrenDiff(); 427 byte[][] relativePath = parentPath.toArray(new byte[parentPath.size()][]); 428 if (node.isDirectory()) { 429 INodeDirectory dir = node.asDirectory(); 430 DirectoryWithSnapshotFeature sf = dir.getDirectoryWithSnapshotFeature(); 431 if (sf != null) { 432 boolean change = sf.computeDiffBetweenSnapshots(diffReport.from, 433 diffReport.to, diff, dir); 434 if (change) { 435 diffReport.addDirDiff(dir, relativePath, diff); 436 } 437 } 438 ReadOnlyList<INode> children = dir.getChildrenList( 439 diffReport.isFromEarlier() ? Snapshot.getSnapshotId(diffReport.to) : 440 Snapshot.getSnapshotId(diffReport.from)); 441 for (INode child : children) { 442 final byte[] name = child.getLocalNameBytes(); 443 if (diff.searchIndex(ListType.CREATED, name) < 0 444 && diff.searchIndex(ListType.DELETED, name) < 0) { 445 parentPath.add(name); 446 computeDiffRecursively(child, parentPath, diffReport); 447 parentPath.remove(parentPath.size() - 1); 448 } 449 } 450 } else if (node.isFile() && node.asFile().isWithSnapshot()) { 451 INodeFile file = node.asFile(); 452 Snapshot earlierSnapshot = diffReport.isFromEarlier() ? diffReport.from 453 : diffReport.to; 454 Snapshot laterSnapshot = diffReport.isFromEarlier() ? diffReport.to 455 : diffReport.from; 456 boolean change = file.getDiffs().changedBetweenSnapshots(earlierSnapshot, 457 laterSnapshot); 458 if (change) { 459 diffReport.addFileDiff(file, relativePath); 460 } 461 } 462 } 463 464 /** 465 * Replace itself with {@link INodeDirectoryWithSnapshot} or 466 * {@link INodeDirectory} depending on the latest snapshot. 467 */ 468 INodeDirectory replaceSelf(final int latestSnapshotId, final INodeMap inodeMap) 469 throws QuotaExceededException { 470 if (latestSnapshotId == Snapshot.CURRENT_STATE_ID) { 471 Preconditions.checkState(getDirectoryWithSnapshotFeature() 472 .getLastSnapshotId() == Snapshot.CURRENT_STATE_ID, "this=%s", this); 473 } 474 INodeDirectory dir = replaceSelf4INodeDirectory(inodeMap); 475 if (latestSnapshotId != Snapshot.CURRENT_STATE_ID) { 476 dir.recordModification(latestSnapshotId); 477 } 478 return dir; 479 } 480 481 @Override 482 public String toDetailString() { 483 return super.toDetailString() + ", snapshotsByNames=" + snapshotsByNames; 484 } 485 486 @Override 487 public void dumpTreeRecursively(PrintWriter out, StringBuilder prefix, 488 int snapshot) { 489 super.dumpTreeRecursively(out, prefix, snapshot); 490 491 if (snapshot == Snapshot.CURRENT_STATE_ID) { 492 out.println(); 493 out.print(prefix); 494 495 out.print("Snapshot of "); 496 final String name = getLocalName(); 497 out.print(name.isEmpty()? "/": name); 498 out.print(": quota="); 499 out.print(getSnapshotQuota()); 500 501 int n = 0; 502 for(DirectoryDiff diff : getDiffs()) { 503 if (diff.isSnapshotRoot()) { 504 n++; 505 } 506 } 507 Preconditions.checkState(n == snapshotsByNames.size(), "#n=" + n 508 + ", snapshotsByNames.size()=" + snapshotsByNames.size()); 509 out.print(", #snapshot="); 510 out.println(n); 511 512 dumpTreeRecursively(out, prefix, new Iterable<SnapshotAndINode>() { 513 @Override 514 public Iterator<SnapshotAndINode> iterator() { 515 return new Iterator<SnapshotAndINode>() { 516 final Iterator<DirectoryDiff> i = getDiffs().iterator(); 517 private DirectoryDiff next = findNext(); 518 519 private DirectoryDiff findNext() { 520 for(; i.hasNext(); ) { 521 final DirectoryDiff diff = i.next(); 522 if (diff.isSnapshotRoot()) { 523 return diff; 524 } 525 } 526 return null; 527 } 528 529 @Override 530 public boolean hasNext() { 531 return next != null; 532 } 533 534 @Override 535 public SnapshotAndINode next() { 536 final SnapshotAndINode pair = new SnapshotAndINode(next 537 .getSnapshotId(), getSnapshotById(next.getSnapshotId()) 538 .getRoot()); 539 next = findNext(); 540 return pair; 541 } 542 543 @Override 544 public void remove() { 545 throw new UnsupportedOperationException(); 546 } 547 }; 548 } 549 }); 550 } 551 } 552}