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;
019
020import java.io.PrintStream;
021import java.io.PrintWriter;
022import java.io.StringWriter;
023import java.util.ArrayList;
024import java.util.List;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.apache.hadoop.classification.InterfaceAudience;
029import org.apache.hadoop.fs.ContentSummary;
030import org.apache.hadoop.fs.Path;
031import org.apache.hadoop.fs.permission.FsPermission;
032import org.apache.hadoop.fs.permission.PermissionStatus;
033import org.apache.hadoop.hdfs.DFSUtil;
034import org.apache.hadoop.hdfs.protocol.Block;
035import org.apache.hadoop.hdfs.protocol.QuotaExceededException;
036import org.apache.hadoop.hdfs.server.namenode.INodeReference.DstReference;
037import org.apache.hadoop.hdfs.server.namenode.INodeReference.WithName;
038import org.apache.hadoop.hdfs.server.namenode.snapshot.FileWithSnapshot;
039import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeDirectoryWithSnapshot;
040import org.apache.hadoop.hdfs.server.namenode.snapshot.Snapshot;
041import org.apache.hadoop.hdfs.util.ChunkedArrayList;
042import org.apache.hadoop.hdfs.util.Diff;
043import org.apache.hadoop.util.StringUtils;
044
045import com.google.common.annotations.VisibleForTesting;
046import com.google.common.base.Preconditions;
047
048/**
049 * We keep an in-memory representation of the file/block hierarchy.
050 * This is a base INode class containing common fields for file and 
051 * directory inodes.
052 */
053@InterfaceAudience.Private
054public abstract class INode implements INodeAttributes, Diff.Element<byte[]> {
055  public static final Log LOG = LogFactory.getLog(INode.class);
056
057  /** parent is either an {@link INodeDirectory} or an {@link INodeReference}.*/
058  private INode parent = null;
059
060  INode(INode parent) {
061    this.parent = parent;
062  }
063
064  /** Get inode id */
065  public abstract long getId();
066
067  /**
068   * Check whether this is the root inode.
069   */
070  final boolean isRoot() {
071    return getLocalNameBytes().length == 0;
072  }
073
074  /** Get the {@link PermissionStatus} */
075  abstract PermissionStatus getPermissionStatus(Snapshot snapshot);
076
077  /** The same as getPermissionStatus(null). */
078  final PermissionStatus getPermissionStatus() {
079    return getPermissionStatus(null);
080  }
081
082  /**
083   * @param snapshot
084   *          if it is not null, get the result from the given snapshot;
085   *          otherwise, get the result from the current inode.
086   * @return user name
087   */
088  abstract String getUserName(Snapshot snapshot);
089
090  /** The same as getUserName(null). */
091  @Override
092  public final String getUserName() {
093    return getUserName(null);
094  }
095
096  /** Set user */
097  abstract void setUser(String user);
098
099  /** Set user */
100  final INode setUser(String user, Snapshot latest, INodeMap inodeMap)
101      throws QuotaExceededException {
102    final INode nodeToUpdate = recordModification(latest, inodeMap);
103    nodeToUpdate.setUser(user);
104    return nodeToUpdate;
105  }
106  /**
107   * @param snapshot
108   *          if it is not null, get the result from the given snapshot;
109   *          otherwise, get the result from the current inode.
110   * @return group name
111   */
112  abstract String getGroupName(Snapshot snapshot);
113
114  /** The same as getGroupName(null). */
115  @Override
116  public final String getGroupName() {
117    return getGroupName(null);
118  }
119
120  /** Set group */
121  abstract void setGroup(String group);
122
123  /** Set group */
124  final INode setGroup(String group, Snapshot latest, INodeMap inodeMap)
125      throws QuotaExceededException {
126    final INode nodeToUpdate = recordModification(latest, inodeMap);
127    nodeToUpdate.setGroup(group);
128    return nodeToUpdate;
129  }
130
131  /**
132   * @param snapshot
133   *          if it is not null, get the result from the given snapshot;
134   *          otherwise, get the result from the current inode.
135   * @return permission.
136   */
137  abstract FsPermission getFsPermission(Snapshot snapshot);
138  
139  /** The same as getFsPermission(null). */
140  @Override
141  public final FsPermission getFsPermission() {
142    return getFsPermission(null);
143  }
144
145  /** Set the {@link FsPermission} of this {@link INode} */
146  abstract void setPermission(FsPermission permission);
147
148  /** Set the {@link FsPermission} of this {@link INode} */
149  INode setPermission(FsPermission permission, Snapshot latest,
150      INodeMap inodeMap) throws QuotaExceededException {
151    final INode nodeToUpdate = recordModification(latest, inodeMap);
152    nodeToUpdate.setPermission(permission);
153    return nodeToUpdate;
154  }
155
156  /**
157   * @return if the given snapshot is null, return this;
158   *     otherwise return the corresponding snapshot inode.
159   */
160  public INodeAttributes getSnapshotINode(final Snapshot snapshot) {
161    return this;
162  }
163
164  /** Is this inode in the latest snapshot? */
165  public final boolean isInLatestSnapshot(final Snapshot latest) {
166    if (latest == null) {
167      return false;
168    }
169    // if parent is a reference node, parent must be a renamed node. We can 
170    // stop the check at the reference node.
171    if (parent != null && parent.isReference()) {
172      return true;
173    }
174    final INodeDirectory parentDir = getParent();
175    if (parentDir == null) { // root
176      return true;
177    }
178    if (!parentDir.isInLatestSnapshot(latest)) {
179      return false;
180    }
181    final INode child = parentDir.getChild(getLocalNameBytes(), latest);
182    if (this == child) {
183      return true;
184    }
185    if (child == null || !(child.isReference())) {
186      return false;
187    }
188    return this == child.asReference().getReferredINode();
189  }
190  
191  /** @return true if the given inode is an ancestor directory of this inode. */
192  public final boolean isAncestorDirectory(final INodeDirectory dir) {
193    for(INodeDirectory p = getParent(); p != null; p = p.getParent()) {
194      if (p == dir) {
195        return true;
196      }
197    }
198    return false;
199  }
200
201  /**
202   * When {@link #recordModification} is called on a referred node,
203   * this method tells which snapshot the modification should be
204   * associated with: the snapshot that belongs to the SRC tree of the rename
205   * operation, or the snapshot belonging to the DST tree.
206   * 
207   * @param latestInDst
208   *          the latest snapshot in the DST tree above the reference node
209   * @return True: the modification should be recorded in the snapshot that
210   *         belongs to the SRC tree. False: the modification should be
211   *         recorded in the snapshot that belongs to the DST tree.
212   */
213  public final boolean shouldRecordInSrcSnapshot(final Snapshot latestInDst) {
214    Preconditions.checkState(!isReference());
215
216    if (latestInDst == null) {
217      return true;
218    }
219    INodeReference withCount = getParentReference();
220    if (withCount != null) {
221      int dstSnapshotId = withCount.getParentReference().getDstSnapshotId();
222      if (dstSnapshotId >= latestInDst.getId()) {
223        return true;
224      }
225    }
226    return false;
227  }
228
229  /**
230   * This inode is being modified.  The previous version of the inode needs to
231   * be recorded in the latest snapshot.
232   *
233   * @param latest the latest snapshot that has been taken.
234   *        Note that it is null if no snapshots have been taken.
235   * @param inodeMap while recording modification, the inode or its parent may 
236   *                 get replaced, and the inodeMap needs to be updated.
237   * @return The current inode, which usually is the same object of this inode.
238   *         However, in some cases, this inode may be replaced with a new inode
239   *         for maintaining snapshots. The current inode is then the new inode.
240   */
241  abstract INode recordModification(final Snapshot latest,
242      final INodeMap inodeMap) throws QuotaExceededException;
243
244  /** Check whether it's a reference. */
245  public boolean isReference() {
246    return false;
247  }
248
249  /** Cast this inode to an {@link INodeReference}.  */
250  public INodeReference asReference() {
251    throw new IllegalStateException("Current inode is not a reference: "
252        + this.toDetailString());
253  }
254
255  /**
256   * Check whether it's a file.
257   */
258  public boolean isFile() {
259    return false;
260  }
261
262  /** Cast this inode to an {@link INodeFile}.  */
263  public INodeFile asFile() {
264    throw new IllegalStateException("Current inode is not a file: "
265        + this.toDetailString());
266  }
267
268  /**
269   * Check whether it's a directory
270   */
271  public boolean isDirectory() {
272    return false;
273  }
274
275  /** Cast this inode to an {@link INodeDirectory}.  */
276  public INodeDirectory asDirectory() {
277    throw new IllegalStateException("Current inode is not a directory: "
278        + this.toDetailString());
279  }
280
281  /**
282   * Check whether it's a symlink
283   */
284  public boolean isSymlink() {
285    return false;
286  }
287
288  /** Cast this inode to an {@link INodeSymlink}.  */
289  public INodeSymlink asSymlink() {
290    throw new IllegalStateException("Current inode is not a symlink: "
291        + this.toDetailString());
292  }
293
294  /**
295   * Clean the subtree under this inode and collect the blocks from the descents
296   * for further block deletion/update. The current inode can either resides in
297   * the current tree or be stored as a snapshot copy.
298   * 
299   * <pre>
300   * In general, we have the following rules. 
301   * 1. When deleting a file/directory in the current tree, we have different 
302   * actions according to the type of the node to delete. 
303   * 
304   * 1.1 The current inode (this) is an {@link INodeFile}. 
305   * 1.1.1 If {@code prior} is null, there is no snapshot taken on ancestors 
306   * before. Thus we simply destroy (i.e., to delete completely, no need to save 
307   * snapshot copy) the current INode and collect its blocks for further 
308   * cleansing.
309   * 1.1.2 Else do nothing since the current INode will be stored as a snapshot
310   * copy.
311   * 
312   * 1.2 The current inode is an {@link INodeDirectory}.
313   * 1.2.1 If {@code prior} is null, there is no snapshot taken on ancestors 
314   * before. Similarly, we destroy the whole subtree and collect blocks.
315   * 1.2.2 Else do nothing with the current INode. Recursively clean its 
316   * children.
317   * 
318   * 1.3 The current inode is a {@link FileWithSnapshot}.
319   * Call recordModification(..) to capture the current states.
320   * Mark the INode as deleted.
321   * 
322   * 1.4 The current inode is a {@link INodeDirectoryWithSnapshot}.
323   * Call recordModification(..) to capture the current states. 
324   * Destroy files/directories created after the latest snapshot 
325   * (i.e., the inodes stored in the created list of the latest snapshot).
326   * Recursively clean remaining children. 
327   *
328   * 2. When deleting a snapshot.
329   * 2.1 To clean {@link INodeFile}: do nothing.
330   * 2.2 To clean {@link INodeDirectory}: recursively clean its children.
331   * 2.3 To clean {@link FileWithSnapshot}: delete the corresponding snapshot in
332   * its diff list.
333   * 2.4 To clean {@link INodeDirectoryWithSnapshot}: delete the corresponding 
334   * snapshot in its diff list. Recursively clean its children.
335   * </pre>
336   * 
337   * @param snapshot
338   *          The snapshot to delete. Null means to delete the current
339   *          file/directory.
340   * @param prior
341   *          The latest snapshot before the to-be-deleted snapshot. When
342   *          deleting a current inode, this parameter captures the latest
343   *          snapshot.
344   * @param collectedBlocks
345   *          blocks collected from the descents for further block
346   *          deletion/update will be added to the given map.
347   * @param removedINodes
348   *          INodes collected from the descents for further cleaning up of 
349   *          inodeMap
350   * @return quota usage delta when deleting a snapshot
351   */
352  public abstract Quota.Counts cleanSubtree(final Snapshot snapshot,
353      Snapshot prior, BlocksMapUpdateInfo collectedBlocks,
354      List<INode> removedINodes, boolean countDiffChange)
355      throws QuotaExceededException;
356  
357  /**
358   * Destroy self and clear everything! If the INode is a file, this method
359   * collects its blocks for further block deletion. If the INode is a
360   * directory, the method goes down the subtree and collects blocks from the
361   * descents, and clears its parent/children references as well. The method
362   * also clears the diff list if the INode contains snapshot diff list.
363   * 
364   * @param collectedBlocks
365   *          blocks collected from the descents for further block
366   *          deletion/update will be added to this map.
367   * @param removedINodes
368   *          INodes collected from the descents for further cleaning up of
369   *          inodeMap
370   */
371  public abstract void destroyAndCollectBlocks(
372      BlocksMapUpdateInfo collectedBlocks, List<INode> removedINodes);
373
374  /** Compute {@link ContentSummary}. Blocking call */
375  public final ContentSummary computeContentSummary() {
376    return computeAndConvertContentSummary(
377        new ContentSummaryComputationContext());
378  }
379
380  /**
381   * Compute {@link ContentSummary}. 
382   */
383  public final ContentSummary computeAndConvertContentSummary(
384      ContentSummaryComputationContext summary) {
385    Content.Counts counts = computeContentSummary(summary).getCounts();
386    return new ContentSummary(counts.get(Content.LENGTH),
387        counts.get(Content.FILE) + counts.get(Content.SYMLINK),
388        counts.get(Content.DIRECTORY), getNsQuota(),
389        counts.get(Content.DISKSPACE), getDsQuota());
390  }
391
392  /**
393   * Count subtree content summary with a {@link Content.Counts}.
394   *
395   * @param summary the context object holding counts for the subtree.
396   * @return The same objects as summary.
397   */
398  public abstract ContentSummaryComputationContext computeContentSummary(
399      ContentSummaryComputationContext summary);
400
401  
402  /**
403   * Check and add namespace/diskspace consumed to itself and the ancestors.
404   * @throws QuotaExceededException if quote is violated.
405   */
406  public void addSpaceConsumed(long nsDelta, long dsDelta, boolean verify) 
407      throws QuotaExceededException {
408    if (parent != null) {
409      parent.addSpaceConsumed(nsDelta, dsDelta, verify);
410    }
411  }
412
413  /**
414   * Get the quota set for this inode
415   * @return the quota if it is set; -1 otherwise
416   */
417  public long getNsQuota() {
418    return -1;
419  }
420
421  public long getDsQuota() {
422    return -1;
423  }
424  
425  public final boolean isQuotaSet() {
426    return getNsQuota() >= 0 || getDsQuota() >= 0;
427  }
428  
429  /**
430   * Count subtree {@link Quota#NAMESPACE} and {@link Quota#DISKSPACE} usages.
431   */
432  public final Quota.Counts computeQuotaUsage() {
433    return computeQuotaUsage(new Quota.Counts(), true);
434  }
435
436  /**
437   * Count subtree {@link Quota#NAMESPACE} and {@link Quota#DISKSPACE} usages.
438   * 
439   * With the existence of {@link INodeReference}, the same inode and its
440   * subtree may be referred by multiple {@link WithName} nodes and a
441   * {@link DstReference} node. To avoid circles while quota usage computation,
442   * we have the following rules:
443   * 
444   * <pre>
445   * 1. For a {@link DstReference} node, since the node must be in the current
446   * tree (or has been deleted as the end point of a series of rename 
447   * operations), we compute the quota usage of the referred node (and its 
448   * subtree) in the regular manner, i.e., including every inode in the current
449   * tree and in snapshot copies, as well as the size of diff list.
450   * 
451   * 2. For a {@link WithName} node, since the node must be in a snapshot, we 
452   * only count the quota usage for those nodes that still existed at the 
453   * creation time of the snapshot associated with the {@link WithName} node.
454   * We do not count in the size of the diff list.  
455   * <pre>
456   * 
457   * @param counts The subtree counts for returning.
458   * @param useCache Whether to use cached quota usage. Note that 
459   *                 {@link WithName} node never uses cache for its subtree.
460   * @param lastSnapshotId {@link Snapshot#INVALID_ID} indicates the computation
461   *                       is in the current tree. Otherwise the id indicates
462   *                       the computation range for a {@link WithName} node.
463   * @return The same objects as the counts parameter.
464   */
465  public abstract Quota.Counts computeQuotaUsage(Quota.Counts counts,
466      boolean useCache, int lastSnapshotId);
467
468  public final Quota.Counts computeQuotaUsage(Quota.Counts counts,
469      boolean useCache) {
470    return computeQuotaUsage(counts, useCache, Snapshot.INVALID_ID);
471  }
472  
473  /**
474   * @return null if the local name is null; otherwise, return the local name.
475   */
476  public final String getLocalName() {
477    final byte[] name = getLocalNameBytes();
478    return name == null? null: DFSUtil.bytes2String(name);
479  }
480
481  @Override
482  public final byte[] getKey() {
483    return getLocalNameBytes();
484  }
485
486  /**
487   * Set local file name
488   */
489  public abstract void setLocalName(byte[] name);
490
491  public String getFullPathName() {
492    // Get the full path name of this inode.
493    return FSDirectory.getFullPathName(this);
494  }
495  
496  @Override
497  public String toString() {
498    return getLocalName();
499  }
500
501  @VisibleForTesting
502  public final String getObjectString() {
503    return getClass().getSimpleName() + "@"
504        + Integer.toHexString(super.hashCode());
505  }
506
507  /** @return a string description of the parent. */
508  @VisibleForTesting
509  public final String getParentString() {
510    final INodeReference parentRef = getParentReference();
511    if (parentRef != null) {
512      return "parentRef=" + parentRef.getLocalName() + "->";
513    } else {
514      final INodeDirectory parentDir = getParent();
515      if (parentDir != null) {
516        return "parentDir=" + parentDir.getLocalName() + "/";
517      } else {
518        return "parent=null";
519      }
520    }
521  }
522
523  @VisibleForTesting
524  public String toDetailString() {
525    return toString() + "(" + getObjectString() + "), " + getParentString();
526  }
527
528  /** @return the parent directory */
529  public final INodeDirectory getParent() {
530    return parent == null? null
531        : parent.isReference()? getParentReference().getParent(): parent.asDirectory();
532  }
533
534  /**
535   * @return the parent as a reference if this is a referred inode;
536   *         otherwise, return null.
537   */
538  public INodeReference getParentReference() {
539    return parent == null || !parent.isReference()? null: (INodeReference)parent;
540  }
541
542  /** Set parent directory */
543  public final void setParent(INodeDirectory parent) {
544    this.parent = parent;
545  }
546
547  /** Set container. */
548  public final void setParentReference(INodeReference parent) {
549    this.parent = parent;
550  }
551
552  /** Clear references to other objects. */
553  public void clear() {
554    setParent(null);
555  }
556
557  /**
558   * @param snapshot
559   *          if it is not null, get the result from the given snapshot;
560   *          otherwise, get the result from the current inode.
561   * @return modification time.
562   */
563  abstract long getModificationTime(Snapshot snapshot);
564
565  /** The same as getModificationTime(null). */
566  @Override
567  public final long getModificationTime() {
568    return getModificationTime(null);
569  }
570
571  /** Update modification time if it is larger than the current value. */
572  public abstract INode updateModificationTime(long mtime, Snapshot latest,
573      INodeMap inodeMap) throws QuotaExceededException;
574
575  /** Set the last modification time of inode. */
576  public abstract void setModificationTime(long modificationTime);
577
578  /** Set the last modification time of inode. */
579  public final INode setModificationTime(long modificationTime,
580      Snapshot latest, INodeMap inodeMap) throws QuotaExceededException {
581    final INode nodeToUpdate = recordModification(latest, inodeMap);
582    nodeToUpdate.setModificationTime(modificationTime);
583    return nodeToUpdate;
584  }
585
586  /**
587   * @param snapshot
588   *          if it is not null, get the result from the given snapshot;
589   *          otherwise, get the result from the current inode.
590   * @return access time
591   */
592  abstract long getAccessTime(Snapshot snapshot);
593
594  /** The same as getAccessTime(null). */
595  @Override
596  public final long getAccessTime() {
597    return getAccessTime(null);
598  }
599
600  /**
601   * Set last access time of inode.
602   */
603  public abstract void setAccessTime(long accessTime);
604
605  /**
606   * Set last access time of inode.
607   */
608  public final INode setAccessTime(long accessTime, Snapshot latest,
609      INodeMap inodeMap) throws QuotaExceededException {
610    final INode nodeToUpdate = recordModification(latest, inodeMap);
611    nodeToUpdate.setAccessTime(accessTime);
612    return nodeToUpdate;
613  }
614
615
616  /**
617   * Breaks file path into components.
618   * @param path
619   * @return array of byte arrays each of which represents 
620   * a single path component.
621   */
622  static byte[][] getPathComponents(String path) {
623    return getPathComponents(getPathNames(path));
624  }
625
626  /** Convert strings to byte arrays for path components. */
627  static byte[][] getPathComponents(String[] strings) {
628    if (strings.length == 0) {
629      return new byte[][]{null};
630    }
631    byte[][] bytes = new byte[strings.length][];
632    for (int i = 0; i < strings.length; i++)
633      bytes[i] = DFSUtil.string2Bytes(strings[i]);
634    return bytes;
635  }
636
637  /**
638   * Splits an absolute path into an array of path components.
639   * @param path
640   * @throws AssertionError if the given path is invalid.
641   * @return array of path components.
642   */
643  static String[] getPathNames(String path) {
644    if (path == null || !path.startsWith(Path.SEPARATOR)) {
645      throw new AssertionError("Absolute path required");
646    }
647    return StringUtils.split(path, Path.SEPARATOR_CHAR);
648  }
649
650  @Override
651  public final int compareTo(byte[] bytes) {
652    return DFSUtil.compareBytes(getLocalNameBytes(), bytes);
653  }
654
655  @Override
656  public final boolean equals(Object that) {
657    if (this == that) {
658      return true;
659    }
660    if (that == null || !(that instanceof INode)) {
661      return false;
662    }
663    return getId() == ((INode) that).getId();
664  }
665
666  @Override
667  public final int hashCode() {
668    long id = getId();
669    return (int)(id^(id>>>32));  
670  }
671  
672  /**
673   * Dump the subtree starting from this inode.
674   * @return a text representation of the tree.
675   */
676  @VisibleForTesting
677  public final StringBuffer dumpTreeRecursively() {
678    final StringWriter out = new StringWriter(); 
679    dumpTreeRecursively(new PrintWriter(out, true), new StringBuilder(), null);
680    return out.getBuffer();
681  }
682
683  @VisibleForTesting
684  public final void dumpTreeRecursively(PrintStream out) {
685    dumpTreeRecursively(new PrintWriter(out, true), new StringBuilder(), null);
686  }
687
688  /**
689   * Dump tree recursively.
690   * @param prefix The prefix string that each line should print.
691   */
692  @VisibleForTesting
693  public void dumpTreeRecursively(PrintWriter out, StringBuilder prefix,
694      Snapshot snapshot) {
695    out.print(prefix);
696    out.print(" ");
697    final String name = getLocalName();
698    out.print(name.isEmpty()? "/": name);
699    out.print("   (");
700    out.print(getObjectString());
701    out.print("), ");
702    out.print(getParentString());
703    out.print(", " + getPermissionStatus(snapshot));
704  }
705  
706  /**
707   * Information used for updating the blocksMap when deleting files.
708   */
709  public static class BlocksMapUpdateInfo {
710    /**
711     * The list of blocks that need to be removed from blocksMap
712     */
713    private List<Block> toDeleteList;
714    
715    public BlocksMapUpdateInfo(List<Block> toDeleteList) {
716      this.toDeleteList = toDeleteList == null ? new ArrayList<Block>()
717          : toDeleteList;
718    }
719    
720    public BlocksMapUpdateInfo() {
721      toDeleteList = new ChunkedArrayList<Block>();
722    }
723    
724    /**
725     * @return The list of blocks that need to be removed from blocksMap
726     */
727    public List<Block> getToDeleteList() {
728      return toDeleteList;
729    }
730    
731    /**
732     * Add a to-be-deleted block into the
733     * {@link BlocksMapUpdateInfo#toDeleteList}
734     * @param toDelete the to-be-deleted block
735     */
736    public void addDeleteBlock(Block toDelete) {
737      if (toDelete != null) {
738        toDeleteList.add(toDelete);
739      }
740    }
741    
742    /**
743     * Clear {@link BlocksMapUpdateInfo#toDeleteList}
744     */
745    public void clear() {
746      toDeleteList.clear();
747    }
748  }
749}