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}