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}