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 */
018 package org.apache.hadoop.hdfs.server.namenode;
019
020 import java.io.PrintWriter;
021 import java.util.ArrayList;
022 import java.util.Collections;
023 import java.util.Comparator;
024 import java.util.List;
025
026 import org.apache.hadoop.fs.permission.FsPermission;
027 import org.apache.hadoop.fs.permission.PermissionStatus;
028 import org.apache.hadoop.hdfs.protocol.QuotaExceededException;
029 import org.apache.hadoop.hdfs.server.namenode.snapshot.DirectoryWithSnapshotFeature;
030 import org.apache.hadoop.hdfs.server.namenode.snapshot.Snapshot;
031
032 import com.google.common.base.Preconditions;
033
034 /**
035 * An anonymous reference to an inode.
036 *
037 * This class and its subclasses are used to support multiple access paths.
038 * A file/directory may have multiple access paths when it is stored in some
039 * snapshots and it is renamed/moved to other locations.
040 *
041 * For example,
042 * (1) Support we have /abc/foo, say the inode of foo is inode(id=1000,name=foo)
043 * (2) create snapshot s0 for /abc
044 * (3) mv /abc/foo /xyz/bar, i.e. inode(id=1000,name=...) is renamed from "foo"
045 * to "bar" and its parent becomes /xyz.
046 *
047 * Then, /xyz/bar and /abc/.snapshot/s0/foo are two different access paths to
048 * the same inode, inode(id=1000,name=bar).
049 *
050 * With references, we have the following
051 * - /abc has a child ref(id=1001,name=foo).
052 * - /xyz has a child ref(id=1002)
053 * - Both ref(id=1001,name=foo) and ref(id=1002) point to another reference,
054 * ref(id=1003,count=2).
055 * - Finally, ref(id=1003,count=2) points to inode(id=1000,name=bar).
056 *
057 * Note 1: For a reference without name, e.g. ref(id=1002), it uses the name
058 * of the referred inode.
059 * Note 2: getParent() always returns the parent in the current state, e.g.
060 * inode(id=1000,name=bar).getParent() returns /xyz but not /abc.
061 */
062 public abstract class INodeReference extends INode {
063 /**
064 * Try to remove the given reference and then return the reference count.
065 * If the given inode is not a reference, return -1;
066 */
067 public static int tryRemoveReference(INode inode) {
068 if (!inode.isReference()) {
069 return -1;
070 }
071 return removeReference(inode.asReference());
072 }
073
074 /**
075 * Remove the given reference and then return the reference count.
076 * If the referred inode is not a WithCount, return -1;
077 */
078 private static int removeReference(INodeReference ref) {
079 final INode referred = ref.getReferredINode();
080 if (!(referred instanceof WithCount)) {
081 return -1;
082 }
083
084 WithCount wc = (WithCount) referred;
085 wc.removeReference(ref);
086 return wc.getReferenceCount();
087 }
088
089 /**
090 * When destroying a reference node (WithName or DstReference), we call this
091 * method to identify the snapshot which is the latest snapshot before the
092 * reference node's creation.
093 */
094 static int getPriorSnapshot(INodeReference ref) {
095 WithCount wc = (WithCount) ref.getReferredINode();
096 WithName wn = null;
097 if (ref instanceof DstReference) {
098 wn = wc.getLastWithName();
099 } else if (ref instanceof WithName) {
100 wn = wc.getPriorWithName((WithName) ref);
101 }
102 if (wn != null) {
103 INode referred = wc.getReferredINode();
104 if (referred.isFile() && referred.asFile().isWithSnapshot()) {
105 return referred.asFile().getDiffs().getPrior(wn.lastSnapshotId);
106 } else if (referred.isDirectory()) {
107 DirectoryWithSnapshotFeature sf = referred.asDirectory()
108 .getDirectoryWithSnapshotFeature();
109 if (sf != null) {
110 return sf.getDiffs().getPrior(wn.lastSnapshotId);
111 }
112 }
113 }
114 return Snapshot.NO_SNAPSHOT_ID;
115 }
116
117 private INode referred;
118
119 public INodeReference(INode parent, INode referred) {
120 super(parent);
121 this.referred = referred;
122 }
123
124 public final INode getReferredINode() {
125 return referred;
126 }
127
128 public final void setReferredINode(INode referred) {
129 this.referred = referred;
130 }
131
132 @Override
133 public final boolean isReference() {
134 return true;
135 }
136
137 @Override
138 public final INodeReference asReference() {
139 return this;
140 }
141
142 @Override
143 public final boolean isFile() {
144 return referred.isFile();
145 }
146
147 @Override
148 public final INodeFile asFile() {
149 return referred.asFile();
150 }
151
152 @Override
153 public final boolean isDirectory() {
154 return referred.isDirectory();
155 }
156
157 @Override
158 public final INodeDirectory asDirectory() {
159 return referred.asDirectory();
160 }
161
162 @Override
163 public final boolean isSymlink() {
164 return referred.isSymlink();
165 }
166
167 @Override
168 public final INodeSymlink asSymlink() {
169 return referred.asSymlink();
170 }
171
172 @Override
173 public byte[] getLocalNameBytes() {
174 return referred.getLocalNameBytes();
175 }
176
177 @Override
178 public void setLocalName(byte[] name) {
179 referred.setLocalName(name);
180 }
181
182 @Override
183 public final long getId() {
184 return referred.getId();
185 }
186
187 @Override
188 public final PermissionStatus getPermissionStatus(int snapshotId) {
189 return referred.getPermissionStatus(snapshotId);
190 }
191
192 @Override
193 public final String getUserName(int snapshotId) {
194 return referred.getUserName(snapshotId);
195 }
196
197 @Override
198 final void setUser(String user) {
199 referred.setUser(user);
200 }
201
202 @Override
203 public final String getGroupName(int snapshotId) {
204 return referred.getGroupName(snapshotId);
205 }
206
207 @Override
208 final void setGroup(String group) {
209 referred.setGroup(group);
210 }
211
212 @Override
213 public final FsPermission getFsPermission(int snapshotId) {
214 return referred.getFsPermission(snapshotId);
215 }
216
217 @Override
218 final AclFeature getAclFeature(int snapshotId) {
219 return referred.getAclFeature(snapshotId);
220 }
221
222 @Override
223 final void addAclFeature(AclFeature aclFeature) {
224 referred.addAclFeature(aclFeature);
225 }
226
227 @Override
228 final void removeAclFeature() {
229 referred.removeAclFeature();
230 }
231
232 @Override
233 public final short getFsPermissionShort() {
234 return referred.getFsPermissionShort();
235 }
236
237 @Override
238 void setPermission(FsPermission permission) {
239 referred.setPermission(permission);
240 }
241
242 @Override
243 public long getPermissionLong() {
244 return referred.getPermissionLong();
245 }
246
247 @Override
248 public final long getModificationTime(int snapshotId) {
249 return referred.getModificationTime(snapshotId);
250 }
251
252 @Override
253 public final INode updateModificationTime(long mtime, int latestSnapshotId)
254 throws QuotaExceededException {
255 return referred.updateModificationTime(mtime, latestSnapshotId);
256 }
257
258 @Override
259 public final void setModificationTime(long modificationTime) {
260 referred.setModificationTime(modificationTime);
261 }
262
263 @Override
264 public final long getAccessTime(int snapshotId) {
265 return referred.getAccessTime(snapshotId);
266 }
267
268 @Override
269 public final void setAccessTime(long accessTime) {
270 referred.setAccessTime(accessTime);
271 }
272
273 @Override
274 final INode recordModification(int latestSnapshotId)
275 throws QuotaExceededException {
276 referred.recordModification(latestSnapshotId);
277 // reference is never replaced
278 return this;
279 }
280
281 @Override // used by WithCount
282 public Quota.Counts cleanSubtree(int snapshot, int prior,
283 BlocksMapUpdateInfo collectedBlocks, final List<INode> removedINodes,
284 final boolean countDiffChange) throws QuotaExceededException {
285 return referred.cleanSubtree(snapshot, prior, collectedBlocks,
286 removedINodes, countDiffChange);
287 }
288
289 @Override // used by WithCount
290 public void destroyAndCollectBlocks(
291 BlocksMapUpdateInfo collectedBlocks, final List<INode> removedINodes) {
292 if (removeReference(this) <= 0) {
293 referred.destroyAndCollectBlocks(collectedBlocks, removedINodes);
294 }
295 }
296
297 @Override
298 public ContentSummaryComputationContext computeContentSummary(
299 ContentSummaryComputationContext summary) {
300 return referred.computeContentSummary(summary);
301 }
302
303 @Override
304 public Quota.Counts computeQuotaUsage(Quota.Counts counts, boolean useCache,
305 int lastSnapshotId) {
306 return referred.computeQuotaUsage(counts, useCache, lastSnapshotId);
307 }
308
309 @Override
310 public final INodeAttributes getSnapshotINode(int snapshotId) {
311 return referred.getSnapshotINode(snapshotId);
312 }
313
314 @Override
315 public Quota.Counts getQuotaCounts() {
316 return referred.getQuotaCounts();
317 }
318
319 @Override
320 public final void clear() {
321 super.clear();
322 referred = null;
323 }
324
325 @Override
326 public void dumpTreeRecursively(PrintWriter out, StringBuilder prefix,
327 final int snapshot) {
328 super.dumpTreeRecursively(out, prefix, snapshot);
329 if (this instanceof DstReference) {
330 out.print(", dstSnapshotId=" + ((DstReference) this).dstSnapshotId);
331 }
332 if (this instanceof WithCount) {
333 out.print(", count=" + ((WithCount)this).getReferenceCount());
334 }
335 out.println();
336
337 final StringBuilder b = new StringBuilder();
338 for(int i = 0; i < prefix.length(); i++) {
339 b.append(' ');
340 }
341 b.append("->");
342 getReferredINode().dumpTreeRecursively(out, b, snapshot);
343 }
344
345 public int getDstSnapshotId() {
346 return Snapshot.CURRENT_STATE_ID;
347 }
348
349 /** An anonymous reference with reference count. */
350 public static class WithCount extends INodeReference {
351
352 private final List<WithName> withNameList = new ArrayList<WithName>();
353
354 /**
355 * Compare snapshot with IDs, where null indicates the current status thus
356 * is greater than any non-null snapshot.
357 */
358 public static final Comparator<WithName> WITHNAME_COMPARATOR
359 = new Comparator<WithName>() {
360 @Override
361 public int compare(WithName left, WithName right) {
362 return left.lastSnapshotId - right.lastSnapshotId;
363 }
364 };
365
366 public WithCount(INodeReference parent, INode referred) {
367 super(parent, referred);
368 Preconditions.checkArgument(!referred.isReference());
369 referred.setParentReference(this);
370 }
371
372 public int getReferenceCount() {
373 int count = withNameList.size();
374 if (getParentReference() != null) {
375 count++;
376 }
377 return count;
378 }
379
380 /** Increment and then return the reference count. */
381 public void addReference(INodeReference ref) {
382 if (ref instanceof WithName) {
383 WithName refWithName = (WithName) ref;
384 int i = Collections.binarySearch(withNameList, refWithName,
385 WITHNAME_COMPARATOR);
386 Preconditions.checkState(i < 0);
387 withNameList.add(-i - 1, refWithName);
388 } else if (ref instanceof DstReference) {
389 setParentReference(ref);
390 }
391 }
392
393 /** Decrement and then return the reference count. */
394 public void removeReference(INodeReference ref) {
395 if (ref instanceof WithName) {
396 int i = Collections.binarySearch(withNameList, (WithName) ref,
397 WITHNAME_COMPARATOR);
398 if (i >= 0) {
399 withNameList.remove(i);
400 }
401 } else if (ref == getParentReference()) {
402 setParent(null);
403 }
404 }
405
406 WithName getLastWithName() {
407 return withNameList.size() > 0 ?
408 withNameList.get(withNameList.size() - 1) : null;
409 }
410
411 WithName getPriorWithName(WithName post) {
412 int i = Collections.binarySearch(withNameList, post, WITHNAME_COMPARATOR);
413 if (i > 0) {
414 return withNameList.get(i - 1);
415 } else if (i == 0 || i == -1) {
416 return null;
417 } else {
418 return withNameList.get(-i - 2);
419 }
420 }
421 }
422
423 /** A reference with a fixed name. */
424 public static class WithName extends INodeReference {
425
426 private final byte[] name;
427
428 /**
429 * The id of the last snapshot in the src tree when this WithName node was
430 * generated. When calculating the quota usage of the referred node, only
431 * the files/dirs existing when this snapshot was taken will be counted for
432 * this WithName node and propagated along its ancestor path.
433 */
434 private final int lastSnapshotId;
435
436 public WithName(INodeDirectory parent, WithCount referred, byte[] name,
437 int lastSnapshotId) {
438 super(parent, referred);
439 this.name = name;
440 this.lastSnapshotId = lastSnapshotId;
441 referred.addReference(this);
442 }
443
444 @Override
445 public final byte[] getLocalNameBytes() {
446 return name;
447 }
448
449 @Override
450 public final void setLocalName(byte[] name) {
451 throw new UnsupportedOperationException("Cannot set name: " + getClass()
452 + " is immutable.");
453 }
454
455 public int getLastSnapshotId() {
456 return lastSnapshotId;
457 }
458
459 @Override
460 public final ContentSummaryComputationContext computeContentSummary(
461 ContentSummaryComputationContext summary) {
462 //only count diskspace for WithName
463 final Quota.Counts q = Quota.Counts.newInstance();
464 computeQuotaUsage(q, false, lastSnapshotId);
465 summary.getCounts().add(Content.DISKSPACE, q.get(Quota.DISKSPACE));
466 return summary;
467 }
468
469 @Override
470 public final Quota.Counts computeQuotaUsage(Quota.Counts counts,
471 boolean useCache, int lastSnapshotId) {
472 // if this.lastSnapshotId < lastSnapshotId, the rename of the referred
473 // node happened before the rename of its ancestor. This should be
474 // impossible since for WithName node we only count its children at the
475 // time of the rename.
476 Preconditions.checkState(lastSnapshotId == Snapshot.CURRENT_STATE_ID
477 || this.lastSnapshotId >= lastSnapshotId);
478 final INode referred = this.getReferredINode().asReference()
479 .getReferredINode();
480 // We will continue the quota usage computation using the same snapshot id
481 // as time line (if the given snapshot id is valid). Also, we cannot use
482 // cache for the referred node since its cached quota may have already
483 // been updated by changes in the current tree.
484 int id = lastSnapshotId != Snapshot.CURRENT_STATE_ID ?
485 lastSnapshotId : this.lastSnapshotId;
486 return referred.computeQuotaUsage(counts, false, id);
487 }
488
489 @Override
490 public Quota.Counts cleanSubtree(final int snapshot, int prior,
491 final BlocksMapUpdateInfo collectedBlocks,
492 final List<INode> removedINodes, final boolean countDiffChange)
493 throws QuotaExceededException {
494 // since WithName node resides in deleted list acting as a snapshot copy,
495 // the parameter snapshot must be non-null
496 Preconditions.checkArgument(snapshot != Snapshot.CURRENT_STATE_ID);
497 // if prior is NO_SNAPSHOT_ID, we need to check snapshot belonging to the
498 // previous WithName instance
499 if (prior == Snapshot.NO_SNAPSHOT_ID) {
500 prior = getPriorSnapshot(this);
501 }
502
503 if (prior != Snapshot.NO_SNAPSHOT_ID
504 && Snapshot.ID_INTEGER_COMPARATOR.compare(snapshot, prior) <= 0) {
505 return Quota.Counts.newInstance();
506 }
507
508 Quota.Counts counts = getReferredINode().cleanSubtree(snapshot, prior,
509 collectedBlocks, removedINodes, false);
510 INodeReference ref = getReferredINode().getParentReference();
511 if (ref != null) {
512 ref.addSpaceConsumed(-counts.get(Quota.NAMESPACE),
513 -counts.get(Quota.DISKSPACE), true);
514 }
515
516 if (snapshot < lastSnapshotId) {
517 // for a WithName node, when we compute its quota usage, we only count
518 // in all the nodes existing at the time of the corresponding rename op.
519 // Thus if we are deleting a snapshot before/at the snapshot associated
520 // with lastSnapshotId, we do not need to update the quota upwards.
521 counts = Quota.Counts.newInstance();
522 }
523 return counts;
524 }
525
526 @Override
527 public void destroyAndCollectBlocks(BlocksMapUpdateInfo collectedBlocks,
528 final List<INode> removedINodes) {
529 int snapshot = getSelfSnapshot();
530 if (removeReference(this) <= 0) {
531 getReferredINode().destroyAndCollectBlocks(collectedBlocks,
532 removedINodes);
533 } else {
534 int prior = getPriorSnapshot(this);
535 INode referred = getReferredINode().asReference().getReferredINode();
536
537 if (snapshot != Snapshot.NO_SNAPSHOT_ID) {
538 if (prior != Snapshot.NO_SNAPSHOT_ID && snapshot <= prior) {
539 // the snapshot to be deleted has been deleted while traversing
540 // the src tree of the previous rename operation. This usually
541 // happens when rename's src and dst are under the same
542 // snapshottable directory. E.g., the following operation sequence:
543 // 1. create snapshot s1 on /test
544 // 2. rename /test/foo/bar to /test/foo2/bar
545 // 3. create snapshot s2 on /test
546 // 4. rename foo2 again
547 // 5. delete snapshot s2
548 return;
549 }
550 try {
551 Quota.Counts counts = referred.cleanSubtree(snapshot, prior,
552 collectedBlocks, removedINodes, false);
553 INodeReference ref = getReferredINode().getParentReference();
554 if (ref != null) {
555 ref.addSpaceConsumed(-counts.get(Quota.NAMESPACE),
556 -counts.get(Quota.DISKSPACE), true);
557 }
558 } catch (QuotaExceededException e) {
559 LOG.error("should not exceed quota while snapshot deletion", e);
560 }
561 }
562 }
563 }
564
565 private int getSelfSnapshot() {
566 INode referred = getReferredINode().asReference().getReferredINode();
567 int snapshot = Snapshot.NO_SNAPSHOT_ID;
568 if (referred.isFile() && referred.asFile().isWithSnapshot()) {
569 snapshot = referred.asFile().getDiffs().getPrior(lastSnapshotId);
570 } else if (referred.isDirectory()) {
571 DirectoryWithSnapshotFeature sf = referred.asDirectory()
572 .getDirectoryWithSnapshotFeature();
573 if (sf != null) {
574 snapshot = sf.getDiffs().getPrior(lastSnapshotId);
575 }
576 }
577 return snapshot;
578 }
579 }
580
581 public static class DstReference extends INodeReference {
582 /**
583 * Record the latest snapshot of the dst subtree before the rename. For
584 * later operations on the moved/renamed files/directories, if the latest
585 * snapshot is after this dstSnapshot, changes will be recorded to the
586 * latest snapshot. Otherwise changes will be recorded to the snapshot
587 * belonging to the src of the rename.
588 *
589 * {@link Snapshot#NO_SNAPSHOT_ID} means no dstSnapshot (e.g., src of the
590 * first-time rename).
591 */
592 private final int dstSnapshotId;
593
594 @Override
595 public final int getDstSnapshotId() {
596 return dstSnapshotId;
597 }
598
599 public DstReference(INodeDirectory parent, WithCount referred,
600 final int dstSnapshotId) {
601 super(parent, referred);
602 this.dstSnapshotId = dstSnapshotId;
603 referred.addReference(this);
604 }
605
606 @Override
607 public Quota.Counts cleanSubtree(int snapshot, int prior,
608 BlocksMapUpdateInfo collectedBlocks, List<INode> removedINodes,
609 final boolean countDiffChange) throws QuotaExceededException {
610 if (snapshot == Snapshot.CURRENT_STATE_ID
611 && prior == Snapshot.NO_SNAPSHOT_ID) {
612 Quota.Counts counts = Quota.Counts.newInstance();
613 this.computeQuotaUsage(counts, true);
614 destroyAndCollectBlocks(collectedBlocks, removedINodes);
615 return counts;
616 } else {
617 // if prior is NO_SNAPSHOT_ID, we need to check snapshot belonging to
618 // the previous WithName instance
619 if (prior == Snapshot.NO_SNAPSHOT_ID) {
620 prior = getPriorSnapshot(this);
621 }
622 // if prior is not NO_SNAPSHOT_ID, and prior is not before the
623 // to-be-deleted snapshot, we can quit here and leave the snapshot
624 // deletion work to the src tree of rename
625 if (snapshot != Snapshot.CURRENT_STATE_ID
626 && prior != Snapshot.NO_SNAPSHOT_ID
627 && Snapshot.ID_INTEGER_COMPARATOR.compare(snapshot, prior) <= 0) {
628 return Quota.Counts.newInstance();
629 }
630 return getReferredINode().cleanSubtree(snapshot, prior,
631 collectedBlocks, removedINodes, countDiffChange);
632 }
633 }
634
635 /**
636 * {@inheritDoc}
637 * <br/>
638 * To destroy a DstReference node, we first remove its link with the
639 * referred node. If the reference number of the referred node is <= 0, we
640 * destroy the subtree of the referred node. Otherwise, we clean the
641 * referred node's subtree and delete everything created after the last
642 * rename operation, i.e., everything outside of the scope of the prior
643 * WithName nodes.
644 */
645 @Override
646 public void destroyAndCollectBlocks(
647 BlocksMapUpdateInfo collectedBlocks, final List<INode> removedINodes) {
648 if (removeReference(this) <= 0) {
649 getReferredINode().destroyAndCollectBlocks(collectedBlocks,
650 removedINodes);
651 } else {
652 // we will clean everything, including files, directories, and
653 // snapshots, that were created after this prior snapshot
654 int prior = getPriorSnapshot(this);
655 // prior must be non-null, otherwise we do not have any previous
656 // WithName nodes, and the reference number will be 0.
657 Preconditions.checkState(prior != Snapshot.NO_SNAPSHOT_ID);
658 // identify the snapshot created after prior
659 int snapshot = getSelfSnapshot(prior);
660
661 INode referred = getReferredINode().asReference().getReferredINode();
662 if (referred.isFile()) {
663 // if referred is a file, it must be a file with snapshot since we did
664 // recordModification before the rename
665 INodeFile file = referred.asFile();
666 Preconditions.checkState(file.isWithSnapshot());
667 // make sure we mark the file as deleted
668 file.getFileWithSnapshotFeature().deleteCurrentFile();
669 try {
670 // when calling cleanSubtree of the referred node, since we
671 // compute quota usage updates before calling this destroy
672 // function, we use true for countDiffChange
673 referred.cleanSubtree(snapshot, prior, collectedBlocks,
674 removedINodes, true);
675 } catch (QuotaExceededException e) {
676 LOG.error("should not exceed quota while snapshot deletion", e);
677 }
678 } else if (referred.isDirectory()) {
679 // similarly, if referred is a directory, it must be an
680 // INodeDirectory with snapshot
681 INodeDirectory dir = referred.asDirectory();
682 Preconditions.checkState(dir.isWithSnapshot());
683 try {
684 DirectoryWithSnapshotFeature.destroyDstSubtree(dir, snapshot,
685 prior, collectedBlocks, removedINodes);
686 } catch (QuotaExceededException e) {
687 LOG.error("should not exceed quota while snapshot deletion", e);
688 }
689 }
690 }
691 }
692
693 private int getSelfSnapshot(final int prior) {
694 WithCount wc = (WithCount) getReferredINode().asReference();
695 INode referred = wc.getReferredINode();
696 int lastSnapshot = Snapshot.CURRENT_STATE_ID;
697 if (referred.isFile() && referred.asFile().isWithSnapshot()) {
698 lastSnapshot = referred.asFile().getDiffs().getLastSnapshotId();
699 } else if (referred.isDirectory()) {
700 DirectoryWithSnapshotFeature sf = referred.asDirectory()
701 .getDirectoryWithSnapshotFeature();
702 if (sf != null) {
703 lastSnapshot = sf.getLastSnapshotId();
704 }
705 }
706 if (lastSnapshot != Snapshot.CURRENT_STATE_ID && lastSnapshot != prior) {
707 return lastSnapshot;
708 } else {
709 return Snapshot.CURRENT_STATE_ID;
710 }
711 }
712 }
713 }