blob: a68c541538c38d4b00303df8cac7792efa2a8622 [file] [log] [blame]
Junxiao Shi0fcb41e2014-01-24 10:29:43 -07001/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
2/**
3 * Copyright (C) 2014 Named Data Networking Project
4 * See COPYING for copyright and distribution information.
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -08005 *
6 * Author: Ilya Moiseenko <iliamo@ucla.edu>
Junxiao Shi0fcb41e2014-01-24 10:29:43 -07007 */
8
Junxiao Shi0fcb41e2014-01-24 10:29:43 -07009#include "cs.hpp"
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -080010#include <ndn-cpp-dev/util/crypto.hpp>
11
12#define SKIPLIST_MAX_LAYERS 32
13#define SKIPLIST_PROBABILITY 50 // 50% ( p = 1/2 )
14
15NFD_LOG_INIT("ContentStore");
Alexander Afanasyevb927a3a2014-01-24 10:41:47 -080016
Alexander Afanasyev18bbf812014-01-29 01:40:23 -080017namespace nfd {
Junxiao Shi0fcb41e2014-01-24 10:29:43 -070018
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -080019Cs::Cs(int nMaxPackets)
20 : m_nMaxPackets(nMaxPackets)
Junxiao Shi0fcb41e2014-01-24 10:29:43 -070021{
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -080022 srand (time::now());
23 SkipListLayer* zeroLayer = new SkipListLayer();
24 m_skipList.push_back(zeroLayer);
Junxiao Shi0fcb41e2014-01-24 10:29:43 -070025}
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -080026
Junxiao Shi0fcb41e2014-01-24 10:29:43 -070027Cs::~Cs()
28{
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -080029 /// \todo Fix memory leak
30}
31
32size_t
33Cs::size() const
34{
35 return (*m_skipList.begin())->size(); // size of the first layer in a skip list
36}
37
38void
39Cs::setLimit(size_t nMaxPackets)
40{
41 m_nMaxPackets = nMaxPackets;
42
43 while (isFull())
44 {
45 evictItem();
46 }
47}
48
49size_t
50Cs::getLimit() const
51{
52 return m_nMaxPackets;
53}
54
55//Reference: "Skip Lists: A Probabilistic Alternative to Balanced Trees" by W.Pugh
56std::pair< shared_ptr<cs::Entry>, bool>
57Cs::insertToSkipList(const Data& data, bool isUnsolicited)
58{
59 NFD_LOG_INFO("insertToSkipList() " << data.getName());
60 NFD_LOG_DEBUG("SkipList size " << size());
61
62 shared_ptr<cs::Entry> entry = make_shared<cs::Entry>(data, isUnsolicited);
63
64 bool insertInFront = false;
65 bool isIterated = false;
66 SkipList::reverse_iterator topLayer = m_skipList.rbegin();
67 SkipListLayer::iterator updateTable[SKIPLIST_MAX_LAYERS];
68 SkipListLayer::iterator head = (*topLayer)->begin();
69
70 if ( !(*topLayer)->empty() )
71 {
72 //start from the upper layer towards bottom
73 int layer = m_skipList.size() - 1;
74 for (SkipList::reverse_iterator rit = topLayer; rit != m_skipList.rend(); ++rit)
75 {
76 //if we didn't do any iterations on the higher layers, start from the begin() again
77 if ( !isIterated )
78 head = (*rit)->begin();
79
80 updateTable[layer] = head;
81
82 if (head != (*rit)->end())
83 {
84 // it can happen when begin() contains the element in front of which we need to insert
85 if ( !isIterated && ((*head)->getName() >= entry->getName()) )
86 {
87 --updateTable[layer];
88 insertInFront = true;
89 }
90 else
91 {
92 SkipListLayer::iterator it = head;
93
94 while ((*it)->getName() < entry->getName())
95 {
96 head = it;
97 updateTable[layer] = it;
98 isIterated = true;
99
100 ++it;
101 if (it == (*rit)->end())
102 break;
103 }
104 }
105 }
106
107 if (layer > 0)
108 head = (*head)->getIterators().find(layer - 1)->second; // move HEAD to the lower layer
109
110 layer--;
111 }
112 }
113 else
114 {
115 updateTable[0] = (*topLayer)->begin(); //initialization
116 }
117
118 head = updateTable[0];
119 ++head; // look at the next slot to check if it contains a duplicate
120
121 bool isCsEmpty = (size() == 0);
122 bool isInBoundaries = (head != (*m_skipList.begin())->end());
123 bool isNameIdentical = false;
124 if (!isCsEmpty && isInBoundaries)
125 {
126 isNameIdentical = (*head)->getName() == entry->getName();
127 }
128
129 //check if this is a duplicate packet
130 if (isNameIdentical)
131 {
132 NFD_LOG_DEBUG("Duplicate name (with digest)");
133
134 (*head)->setData(data, entry->getDigest()); //updates stale time
135
136 return std::make_pair(*head, false);
137 }
138
139 NFD_LOG_DEBUG("Not a duplicate");
140
141 int randomLayer = pickRandomLayer();
142
143 if (randomLayer > (m_skipList.size() - 1))
144 {
145 while ( (m_skipList.size() - 1) < randomLayer)
146 {
147 SkipListLayer* newLayer = new SkipListLayer();
148 m_skipList.push_back(newLayer);
149
150 updateTable[(m_skipList.size() - 1)] = newLayer->begin();
151 }
152 }
153
154 int layer = 0;
155 for (SkipList::iterator i = m_skipList.begin(); i != m_skipList.end() && layer <= randomLayer; ++i)
156 {
157 if (updateTable[layer] == (*i)->end() && !insertInFront)
158 {
159 (*i)->push_back(entry);
160 SkipListLayer::iterator last = (*i)->end();
161 --last;
162 entry->setIterator(layer, last);
163
164 NFD_LOG_DEBUG("pushback " << &(*last));
165 }
166 else if (updateTable[layer] == (*i)->end() && insertInFront)
167 {
168 (*i)->push_front(entry);
169 entry->setIterator(layer, (*i)->begin());
170
171 NFD_LOG_DEBUG("pushfront ");
172 }
173 else
174 {
175 NFD_LOG_DEBUG("insertafter");
176 ++updateTable[layer]; // insert after
177 SkipListLayer::iterator position = (*i)->insert(updateTable[layer], entry);
178 entry->setIterator(layer, position); // save iterator where item was inserted
179 }
180 layer++;
181 }
182
183 printSkipList();
184
185 return std::make_pair(entry, true);
Junxiao Shi0fcb41e2014-01-24 10:29:43 -0700186}
187
188bool
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -0800189Cs::insert(const Data& data, bool isUnsolicited)
Junxiao Shi0fcb41e2014-01-24 10:29:43 -0700190{
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -0800191 NFD_LOG_INFO("insert() " << data.getName());
192
193 if (isFull())
194 {
195 evictItem();
196 }
197
198 //pointer and insertion status
199 std::pair< shared_ptr<cs::Entry>, bool> entry = insertToSkipList(data, isUnsolicited);
200
201 //new entry
202 if (entry.first && (entry.second == true))
203 {
204 m_contentByArrival.push(entry.first);
205 m_contentByStaleness.push(entry.first);
206
207 if (entry.first->isUnsolicited())
208 m_unsolicitedContent.push(entry.first);
209
210 return true;
211 }
212
Junxiao Shi0fcb41e2014-01-24 10:29:43 -0700213 return false;
214}
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -0800215
216int
217Cs::pickRandomLayer() const
Junxiao Shi0fcb41e2014-01-24 10:29:43 -0700218{
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -0800219 int layer = -1;
220 int randomValue;
221
222 do
223 {
224 layer++;
225 randomValue = rand() % 100 + 1;
226 }
227 while ( (randomValue < SKIPLIST_PROBABILITY) && (layer < SKIPLIST_MAX_LAYERS) );
228
229 return layer;
230}
231
232bool
233Cs::isFull() const
234{
235 if (size() >= m_nMaxPackets) //size of the first layer vs. max size
236 return true;
237
238 return false;
239}
240
241bool
242Cs::eraseFromSkipList(shared_ptr<cs::Entry> entry)
243{
244 NFD_LOG_INFO("eraseFromSkipList() " << entry->getName());
245 NFD_LOG_DEBUG("SkipList size " << size());
246
247 bool isErased = false;
248
249 int layer = m_skipList.size() - 1;
250 for (SkipList::reverse_iterator rit = m_skipList.rbegin(); rit != m_skipList.rend(); ++rit)
251 {
252 const std::map<int, std::list< shared_ptr<cs::Entry> >::iterator>& iterators = entry->getIterators();
253 std::map<int, std::list< shared_ptr<cs::Entry> >::iterator>::const_iterator it = iterators.find(layer);
254 if (it != iterators.end())
255 {
256 (*rit)->erase(it->second);
257 entry->removeIterator(layer);
258 isErased = true;
259 }
260
261 layer--;
262 }
263
264 printSkipList();
265
266 //remove layers that do not contain any elements (starting from the second layer)
267 for (SkipList::iterator it = (++m_skipList.begin()); it != m_skipList.end();)
268 {
269 if ((*it)->empty())
270 {
271 it = m_skipList.erase(it);
272 }
273 else
274 ++it;
275 }
276
277 return isErased;
278}
279
280bool
281Cs::evictItem()
282{
283 NFD_LOG_INFO("evictItem()");
284
285 //because there is a possibility that this item is in a queue, but no longer in skiplist
286 while ( !m_unsolicitedContent.empty() )
287 {
288 NFD_LOG_DEBUG("Evict from unsolicited queue");
289
290 shared_ptr<cs::Entry> entry = m_unsolicitedContent.front();
291 m_unsolicitedContent.pop();
292 bool isErased = eraseFromSkipList(entry);
293
294 if (isErased)
295 return true;
296 }
297
298 //because there is a possibility that this item is in a queue, but no longer in skiplist
299 int nIterations = size() * 0.01; // 1% of the Content Store
300 while ( !m_contentByStaleness.empty() )
301 {
302 NFD_LOG_DEBUG("Evict from staleness queue");
303
304 shared_ptr<cs::Entry> entry = m_contentByStaleness.top();
305
306 //because stale time could be updated by the duplicate packet
307 if (entry->getStaleTime() < time::now())
308 {
309 m_contentByStaleness.pop();
310 bool isErased = eraseFromSkipList(entry);
311
312 if (isErased)
313 return true;
314 }
315 else if ( (entry->getStaleTime() > time::now()) && entry->wasRefreshedByDuplicate() )
316 {
317 m_contentByStaleness.pop();
318 m_contentByStaleness.push(entry); // place in a right order
319
320 nIterations--;
321 // if 1% of the CS are non-expired refreshed CS entries (allocated sequentially),
322 // then stop to prevent too many iterations
323 if ( nIterations <= 0 )
324 break;
325 }
326 else //no further item will be expired, stop
327 {
328 break;
329 }
330 }
331
332 //because there is a possibility that this item is in a queue, but no longer in skiplist
333 while ( !m_contentByArrival.empty() )
334 {
335 NFD_LOG_DEBUG("Evict from arrival queue");
336
337 shared_ptr<cs::Entry> entry = m_contentByArrival.front();
338 m_contentByArrival.pop();
339 bool isErased = eraseFromSkipList(entry);
340
341 if (isErased)
342 return true;
343 }
344
345 return false;
346}
347
348const Data*
349Cs::find(const Interest& interest) const
350{
351 NFD_LOG_INFO("find() " << interest.getName());
352
353 bool isIterated = false;
354 SkipList::const_reverse_iterator topLayer = m_skipList.rbegin();
355 SkipListLayer::iterator head = (*topLayer)->begin();
356
357 if ( !(*topLayer)->empty() )
358 {
359 //start from the upper layer towards bottom
360 int layer = m_skipList.size() - 1;
361 for (SkipList::const_reverse_iterator rit = topLayer; rit != m_skipList.rend(); ++rit)
362 {
363 //if we didn't do any iterations on the higher layers, start from the begin() again
364 if (!isIterated)
365 head = (*rit)->begin();
366
367 if (head != (*rit)->end())
368 {
369 // it happens when begin() contains the element we want to find
370 if ( !isIterated && (interest.getName().isPrefixOf((*head)->getName())) )
371 {
372 if (layer > 0)
373 {
374 layer--;
375 continue; // try lower layer
376 }
377 else
378 {
379 isIterated = true;
380 }
381 }
382 else
383 {
384 SkipListLayer::iterator it = head;
385
386 while ( (*it)->getName() < interest.getName() )
387 {
388 NFD_LOG_DEBUG((*it)->getName() << " < " << interest.getName());
389 head = it;
390 isIterated = true;
391
392 ++it;
393 if (it == (*rit)->end())
394 break;
395 }
396 }
397 }
398
399 if (layer > 0)
400 {
401 head = (*head)->getIterators().find(layer - 1)->second; // move HEAD to the lower layer
402 }
403 else //if we reached the first layer
404 {
405 if ( isIterated )
406 return selectChild(interest, head);
407 }
408
409 layer--;
410 }
411 }
412
Alexander Afanasyevc9765df2014-01-25 19:24:27 -0800413 return 0;
Junxiao Shi0fcb41e2014-01-24 10:29:43 -0700414}
415
Ilya Moiseenko76cf77a2014-03-05 14:35:51 -0800416// because skip list is a probabilistic data structure and the way it is traversed,
417// there is no guarantee that startingPoint is an element preceding the leftmost child
418const Data*
419Cs::selectChild(const Interest& interest, SkipListLayer::iterator startingPoint) const
420{
421 BOOST_ASSERT( startingPoint != (*m_skipList.begin())->end() );
422
423 if (startingPoint != (*m_skipList.begin())->begin())
424 {
425 BOOST_ASSERT( (*startingPoint)->getName() < interest.getName() );
426 }
427
428 NFD_LOG_INFO("selectChild() " << interest.getChildSelector() << " " << (*startingPoint)->getName());
429
430 bool hasLeftmostSelector = (interest.getChildSelector() <= 0);
431 bool hasRightmostSelector = !hasLeftmostSelector;
432
433 if (hasLeftmostSelector)
434 {
435 bool doesInterestContainDigest = recognizeInterestWithDigest(interest, *startingPoint);
436 bool isInPrefix = false;
437
438 if (doesInterestContainDigest)
439 {
440 isInPrefix = interest.getName().getPrefix(-1).isPrefixOf((*startingPoint)->getName());
441 }
442 else
443 {
444 isInPrefix = interest.getName().isPrefixOf((*startingPoint)->getName());
445 }
446
447 if (isInPrefix)
448 {
449 if (doesComplyWithSelectors(interest, *startingPoint))
450 {
451 return &(*startingPoint)->getData();
452 }
453 }
454 }
455
456 //iterate to the right
457 SkipListLayer::iterator rightmost = startingPoint;
458 if (startingPoint != (*m_skipList.begin())->end())
459 {
460 SkipListLayer::iterator rightmostCandidate = startingPoint;
461 Name currentChildPrefix("");
462
463 while (true)
464 {
465 ++rightmostCandidate;
466
467 bool isInBoundaries = (rightmostCandidate != (*m_skipList.begin())->end());
468 bool isInPrefix = false;
469 bool doesInterestContainDigest = false;
470 if (isInBoundaries)
471 {
472 doesInterestContainDigest = recognizeInterestWithDigest(interest, *rightmostCandidate);
473
474 if (doesInterestContainDigest)
475 {
476 isInPrefix = interest.getName().getPrefix(-1).isPrefixOf((*rightmostCandidate)->getName());
477 }
478 else
479 {
480 isInPrefix = interest.getName().isPrefixOf((*rightmostCandidate)->getName());
481 }
482 }
483
484 if (isInPrefix)
485 {
486 if (doesComplyWithSelectors(interest, *rightmostCandidate))
487 {
488 if (hasLeftmostSelector)
489 {
490 return &(*rightmostCandidate)->getData();
491 }
492
493 if (hasRightmostSelector)
494 {
495 if (doesInterestContainDigest)
496 {
497 // get prefix which is one component longer than Interest name (without digest)
498 const Name& childPrefix = (*rightmostCandidate)->getName().getPrefix(interest.getName().size());
499 NFD_LOG_DEBUG("Child prefix" << childPrefix);
500
501 if ( currentChildPrefix.empty() || (childPrefix != currentChildPrefix) )
502 {
503 currentChildPrefix = childPrefix;
504 rightmost = rightmostCandidate;
505 }
506 }
507 else
508 {
509 // get prefix which is one component longer than Interest name
510 const Name& childPrefix = (*rightmostCandidate)->getName().getPrefix(interest.getName().size() + 1);
511 NFD_LOG_DEBUG("Child prefix" << childPrefix);
512
513 if ( currentChildPrefix.empty() || (childPrefix != currentChildPrefix) )
514 {
515 currentChildPrefix = childPrefix;
516 rightmost = rightmostCandidate;
517 }
518 }
519 }
520 }
521 }
522 else
523 break;
524 }
525 }
526
527 if (rightmost != startingPoint)
528 {
529 return &(*rightmost)->getData();
530 }
531
532 if (hasRightmostSelector) // if rightmost was not found, try starting point
533 {
534 bool doesInterestContainDigest = recognizeInterestWithDigest(interest, *startingPoint);
535 bool isInPrefix = false;
536
537 if (doesInterestContainDigest)
538 {
539 isInPrefix = interest.getName().getPrefix(-1).isPrefixOf((*startingPoint)->getName());
540 }
541 else
542 {
543 isInPrefix = interest.getName().isPrefixOf((*startingPoint)->getName());
544 }
545
546 if (isInPrefix)
547 {
548 if (doesComplyWithSelectors(interest, *startingPoint))
549 {
550 return &(*startingPoint)->getData();
551 }
552 }
553 }
554
555 return 0;
556}
557
558bool
559Cs::doesComplyWithSelectors(const Interest& interest, shared_ptr<cs::Entry> entry) const
560{
561 NFD_LOG_INFO("doesComplyWithSelectors()");
562
563 /// \todo The following detection is not correct
564 /// 1. If data name ends with 32-octet component doesn't mean that this component is digest
565 /// 2. Only min/max selectors (both 0) can be specified, all other selectors do not make sense
566 /// for interests with digest (though not sure if we need to enforce this)
567 bool doesInterestContainDigest = recognizeInterestWithDigest(interest, entry);
568 if ( doesInterestContainDigest )
569 {
570 const ndn::name::Component& last = interest.getName().get(-1);
571 const ndn::ConstBufferPtr& digest = entry->getDigest();
572
573 BOOST_ASSERT(digest->size() == last.value_size());
574 BOOST_ASSERT(digest->size() == ndn::crypto::SHA256_DIGEST_SIZE);
575
576 if (std::memcmp(digest->buf(), last.value(), ndn::crypto::SHA256_DIGEST_SIZE) != 0)
577 {
578 NFD_LOG_DEBUG("violates implicit digest");
579 return false;
580 }
581 }
582
583 if ( !doesInterestContainDigest )
584 {
585 if (interest.getMinSuffixComponents() >= 0)
586 {
587 int minDataNameLength = interest.getName().size() + interest.getMinSuffixComponents();
588
589 bool isSatisfied = minDataNameLength <= entry->getName().size();
590 if ( !isSatisfied )
591 {
592 NFD_LOG_DEBUG("violates minComponents");
593 return false;
594 }
595 }
596
597 if (interest.getMaxSuffixComponents() >= 0)
598 {
599 int maxDataNameLength = interest.getName().size() + interest.getMaxSuffixComponents();
600
601 bool isSatisfied = maxDataNameLength >= entry->getName().size();
602 if ( !isSatisfied )
603 {
604 NFD_LOG_DEBUG("violates maxComponents");
605 return false;
606 }
607 }
608 }
609
610 if (interest.getMustBeFresh() && entry->getStaleTime() < time::now())
611 {
612 NFD_LOG_DEBUG("violates mustBeFresh");
613 return false;
614 }
615
616 if ( doesInterestContainDigest )
617 {
618 const ndn::name::Component& lastComponent = entry->getName().get(-1);
619
620 if ( !lastComponent.empty() )
621 {
622 if (interest.getExclude().isExcluded(lastComponent))
623 {
624 NFD_LOG_DEBUG("violates exclusion");
625 return false;
626 }
627 }
628 }
629 else
630 {
631 if (entry->getName().size() >= interest.getName().size() + 1)
632 {
633 const ndn::name::Component& nextComponent = entry->getName().get(interest.getName().size());
634
635 if ( !nextComponent.empty() )
636 {
637 if (interest.getExclude().isExcluded(nextComponent))
638 {
639 NFD_LOG_DEBUG("violates exclusion");
640 return false;
641 }
642 }
643 }
644 }
645
646
647 NFD_LOG_DEBUG("complies!");
648 return true;
649}
650
651bool
652Cs::recognizeInterestWithDigest(const Interest& interest, shared_ptr<cs::Entry> entry) const
653{
654 // only when min selector is not specified or specified with value of 0
655 // and Interest's name length is exactly the length of the name of CS entry
656 if (interest.getMinSuffixComponents() <= 0 &&
657 interest.getName().size() == (entry->getName().size()))
658 {
659 const ndn::name::Component& last = interest.getName().get(-1);
660 if (last.value_size() == ndn::crypto::SHA256_DIGEST_SIZE)
661 {
662 NFD_LOG_DEBUG("digest recognized");
663 return true;
664 }
665 }
666
667 return false;
668}
669
670void
671Cs::erase(const Name& exactName)
672{
673 NFD_LOG_INFO("insert() " << exactName);
674 NFD_LOG_DEBUG("SkipList size " << size());
675
676 bool isIterated = false;
677 SkipListLayer::iterator updateTable[SKIPLIST_MAX_LAYERS];
678 SkipList::reverse_iterator topLayer = m_skipList.rbegin();
679 SkipListLayer::iterator head = (*topLayer)->begin();
680
681 if ( !(*topLayer)->empty() )
682 {
683 //start from the upper layer towards bottom
684 int layer = m_skipList.size() - 1;
685 for (SkipList::reverse_iterator rit = topLayer; rit != m_skipList.rend(); ++rit)
686 {
687 //if we didn't do any iterations on the higher layers, start from the begin() again
688 if ( !isIterated )
689 head = (*rit)->begin();
690
691 updateTable[layer] = head;
692
693 if (head != (*rit)->end())
694 {
695 // it can happen when begin() contains the element we want to remove
696 if ( !isIterated && ((*head)->getName() == exactName) )
697 {
698 eraseFromSkipList(*head);
699 return;
700 }
701 else
702 {
703 SkipListLayer::iterator it = head;
704
705 while ((*it)->getName() < exactName)
706 {
707 head = it;
708 updateTable[layer] = it;
709 isIterated = true;
710
711 ++it;
712 if ( it == (*rit)->end() )
713 break;
714 }
715 }
716 }
717
718 if (layer > 0)
719 head = (*head)->getIterators().find(layer - 1)->second; // move HEAD to the lower layer
720
721 layer--;
722 }
723 }
724 else
725 {
726 return;
727 }
728
729 head = updateTable[0];
730 ++head; // look at the next slot to check if it contains the item we want to remove
731
732 bool isCsEmpty = (size() == 0);
733 bool isInBoundaries = (head != (*m_skipList.begin())->end());
734 bool isNameIdentical = false;
735 if (!isCsEmpty && isInBoundaries)
736 {
737 NFD_LOG_DEBUG("Identical? " << (*head)->getName());
738 isNameIdentical = (*head)->getName() == exactName;
739 }
740
741 if (isNameIdentical)
742 {
743 NFD_LOG_DEBUG("Found target " << (*head)->getName());
744 eraseFromSkipList(*head);
745 }
746
747 printSkipList();
748}
749
750void
751Cs::printSkipList() const
752{
753 NFD_LOG_INFO("print()");
754 //start from the upper layer towards bottom
755 int layer = m_skipList.size() - 1;
756 for (SkipList::const_reverse_iterator rit = m_skipList.rbegin(); rit != m_skipList.rend(); ++rit)
757 {
758 for (SkipListLayer::iterator it = (*rit)->begin(); it != (*rit)->end(); ++it)
759 {
760 NFD_LOG_DEBUG("Layer " << layer << " " << (*it)->getName());
761 }
762 layer--;
763 }
764}
Junxiao Shi0fcb41e2014-01-24 10:29:43 -0700765
Alexander Afanasyev18bbf812014-01-29 01:40:23 -0800766} //namespace nfd