fw: NccStrategy

refs #1242

Change-Id: I21f870728b7b361547adbde9d64357c93aeb1ed9
diff --git a/daemon/common.hpp b/daemon/common.hpp
index 2e0ff22..650541e 100644
--- a/daemon/common.hpp
+++ b/daemon/common.hpp
@@ -53,6 +53,7 @@
 using boost::make_shared;
 using boost::static_pointer_cast;
 using boost::dynamic_pointer_cast;
+using boost::weak_ptr;
 using boost::function;
 using boost::bind;
 
diff --git a/daemon/fw/ncc-strategy.cpp b/daemon/fw/ncc-strategy.cpp
new file mode 100644
index 0000000..d08260c
--- /dev/null
+++ b/daemon/fw/ncc-strategy.cpp
@@ -0,0 +1,281 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (C) 2014 Named Data Networking Project
+ * See COPYING for copyright and distribution information.
+ */
+
+#include "ncc-strategy.hpp"
+
+namespace nfd {
+namespace fw {
+
+NccStrategy::NccStrategy(Forwarder& forwarder)
+  : Strategy(forwarder)
+{
+}
+
+NccStrategy::~NccStrategy()
+{
+}
+
+const time::Duration NccStrategy::DEFER_FIRST_WITHOUT_BEST_FACE = time::microseconds(4000);
+const time::Duration NccStrategy::DEFER_RANGE_WITHOUT_BEST_FACE = time::microseconds(75000);
+const time::Duration NccStrategy::MEASUREMENTS_LIFETIME = time::seconds(16);
+
+void
+NccStrategy::afterReceiveInterest(const Face& inFace,
+                                  const Interest& interest,
+                                  shared_ptr<fib::Entry> fibEntry,
+                                  shared_ptr<pit::Entry> pitEntry)
+{
+  const fib::NextHopList& nexthops = fibEntry->getNextHops();
+  if (nexthops.size() == 0) {
+    this->rejectPendingInterest(pitEntry);
+    return;
+  }
+
+  shared_ptr<PitEntryInfo> pitEntryInfo =
+    pitEntry->getOrCreateStrategyInfo<PitEntryInfo>();
+  bool isNewInterest = pitEntryInfo->m_isNewInterest;
+  if (!isNewInterest) {
+    return;
+  }
+  pitEntryInfo->m_isNewInterest = false;
+
+  shared_ptr<MeasurementsEntryInfo> measurementsEntryInfo =
+    this->getMeasurementsEntryInfo(pitEntry);
+
+  time::Duration deferFirst = DEFER_FIRST_WITHOUT_BEST_FACE;
+  time::Duration deferRange = DEFER_RANGE_WITHOUT_BEST_FACE;
+  size_t nUpstreams = nexthops.size();
+
+  shared_ptr<Face> bestFace = measurementsEntryInfo->getBestFace();
+  if (static_cast<bool>(bestFace) && fibEntry->hasNextHop(bestFace) &&
+      pitEntry->canForwardTo(bestFace)) {
+    // TODO Should we use `randlow = 100 + nrand48(h->seed) % 4096U;` ?
+    deferFirst = measurementsEntryInfo->m_prediction;
+    deferRange = static_cast<time::Duration>((deferFirst + time::microseconds(1)) / 2);
+    --nUpstreams;
+    this->sendInterest(pitEntry, bestFace);
+    pitEntryInfo->m_bestFaceTimeout = scheduler::schedule(
+      measurementsEntryInfo->m_prediction,
+      bind(&NccStrategy::timeoutOnBestFace, this, weak_ptr<pit::Entry>(pitEntry)));
+  }
+  else {
+    // use first nexthop
+    this->sendInterest(pitEntry, nexthops.begin()->getFace());
+    // TODO avoid sending to inFace
+  }
+
+  shared_ptr<Face> previousFace = measurementsEntryInfo->m_previousFace.lock();
+  if (static_cast<bool>(previousFace) && fibEntry->hasNextHop(previousFace) &&
+      pitEntry->canForwardTo(previousFace)) {
+    --nUpstreams;
+  }
+
+  pitEntryInfo->m_maxInterval = std::max(time::microseconds(1),
+    static_cast<time::Duration>((2 * deferRange + nUpstreams - 1) / nUpstreams));
+  pitEntryInfo->m_propagateTimer = scheduler::schedule(deferFirst,
+    bind(&NccStrategy::doPropagate, this,
+         weak_ptr<pit::Entry>(pitEntry), weak_ptr<fib::Entry>(fibEntry)));
+}
+
+void
+NccStrategy::doPropagate(weak_ptr<pit::Entry> pitEntryWeak, weak_ptr<fib::Entry> fibEntryWeak)
+{
+  shared_ptr<pit::Entry> pitEntry = pitEntryWeak.lock();
+  if (!static_cast<bool>(pitEntry)) {
+    return;
+  }
+  shared_ptr<fib::Entry> fibEntry = fibEntryWeak.lock();
+  if (!static_cast<bool>(fibEntry)) {
+    return;
+  }
+
+  shared_ptr<PitEntryInfo> pitEntryInfo =
+    pitEntry->getStrategyInfo<PitEntryInfo>();
+  BOOST_ASSERT(static_cast<bool>(pitEntryInfo));
+
+  shared_ptr<MeasurementsEntryInfo> measurementsEntryInfo =
+    this->getMeasurementsEntryInfo(pitEntry);
+
+  shared_ptr<Face> previousFace = measurementsEntryInfo->m_previousFace.lock();
+  if (static_cast<bool>(previousFace) && fibEntry->hasNextHop(previousFace) &&
+      pitEntry->canForwardTo(previousFace)) {
+    this->sendInterest(pitEntry, previousFace);
+  }
+
+  const fib::NextHopList& nexthops = fibEntry->getNextHops();
+  bool isForwarded = false;
+  for (fib::NextHopList::const_iterator it = nexthops.begin(); it != nexthops.end(); ++it) {
+    shared_ptr<Face> face = it->getFace();
+    if (pitEntry->canForwardTo(face)) {
+      isForwarded = true;
+      this->sendInterest(pitEntry, face);
+      break;
+    }
+  }
+
+  if (isForwarded) {
+    static unsigned short seed[3];
+    time::Duration deferNext = static_cast<time::Duration>(
+                               nrand48(seed) % pitEntryInfo->m_maxInterval);
+    pitEntryInfo->m_propagateTimer = scheduler::schedule(deferNext,
+    bind(&NccStrategy::doPropagate, this,
+         weak_ptr<pit::Entry>(pitEntry), weak_ptr<fib::Entry>(fibEntry)));
+  }
+}
+
+void
+NccStrategy::timeoutOnBestFace(weak_ptr<pit::Entry> pitEntryWeak)
+{
+  shared_ptr<pit::Entry> pitEntry = pitEntryWeak.lock();
+  if (!static_cast<bool>(pitEntry)) {
+    return;
+  }
+  shared_ptr<measurements::Entry> measurementsEntry = this->getMeasurements().get(*pitEntry);
+
+  for (int i = 0; i < UPDATE_MEASUREMENTS_N_LEVELS; ++i) {
+    if (!static_cast<bool>(measurementsEntry)) {
+      // going out of this strategy's namespace
+      return;
+    }
+    this->getMeasurements().extendLifetime(*measurementsEntry, MEASUREMENTS_LIFETIME);
+
+    shared_ptr<MeasurementsEntryInfo> measurementsEntryInfo =
+      this->getMeasurementsEntryInfo(measurementsEntry);
+    measurementsEntryInfo->adjustPredictUp();
+
+    measurementsEntry = this->getMeasurements().getParent(measurementsEntry);
+  }
+}
+
+void
+NccStrategy::beforeSatisfyPendingInterest(shared_ptr<pit::Entry> pitEntry,
+                                          const Face& inFace, const Data& data)
+{
+  shared_ptr<measurements::Entry> measurementsEntry = this->getMeasurements().get(*pitEntry);
+
+  for (int i = 0; i < UPDATE_MEASUREMENTS_N_LEVELS; ++i) {
+    if (!static_cast<bool>(measurementsEntry)) {
+      // going out of this strategy's namespace
+      return;
+    }
+    this->getMeasurements().extendLifetime(*measurementsEntry, MEASUREMENTS_LIFETIME);
+
+    shared_ptr<MeasurementsEntryInfo> measurementsEntryInfo =
+      this->getMeasurementsEntryInfo(measurementsEntry);
+    measurementsEntryInfo->updateBestFace(inFace);
+
+    measurementsEntry = this->getMeasurements().getParent(measurementsEntry);
+  }
+
+  shared_ptr<PitEntryInfo> pitEntryInfo =
+    pitEntry->getStrategyInfo<PitEntryInfo>();
+  scheduler::cancel(pitEntryInfo->m_propagateTimer);
+}
+
+shared_ptr<NccStrategy::MeasurementsEntryInfo>
+NccStrategy::getMeasurementsEntryInfo(shared_ptr<pit::Entry> entry)
+{
+  shared_ptr<measurements::Entry> measurementsEntry = this->getMeasurements().get(*entry);
+  return this->getMeasurementsEntryInfo(measurementsEntry);
+}
+
+shared_ptr<NccStrategy::MeasurementsEntryInfo>
+NccStrategy::getMeasurementsEntryInfo(shared_ptr<measurements::Entry> entry)
+{
+  shared_ptr<MeasurementsEntryInfo> info = entry->getStrategyInfo<MeasurementsEntryInfo>();
+  if (static_cast<bool>(info)) {
+    return info;
+  }
+
+  info = make_shared<MeasurementsEntryInfo>();
+  entry->setStrategyInfo(info);
+
+  shared_ptr<measurements::Entry> parentEntry = this->getMeasurements().getParent(entry);
+  if (static_cast<bool>(parentEntry)) {
+    shared_ptr<MeasurementsEntryInfo> parentInfo = this->getMeasurementsEntryInfo(parentEntry);
+    BOOST_ASSERT(static_cast<bool>(parentInfo));
+    info->inheritFrom(*parentInfo);
+  }
+
+  return info;
+}
+
+
+const time::Duration NccStrategy::MeasurementsEntryInfo::INITIAL_PREDICTION =
+                                                         time::microseconds(8192);
+const time::Duration NccStrategy::MeasurementsEntryInfo::MIN_PREDICTION =
+                                                         time::microseconds(127);
+const time::Duration NccStrategy::MeasurementsEntryInfo::MAX_PREDICTION =
+                                                         time::microseconds(160000);
+
+NccStrategy::MeasurementsEntryInfo::MeasurementsEntryInfo()
+  : m_prediction(INITIAL_PREDICTION)
+{
+}
+
+void
+NccStrategy::MeasurementsEntryInfo::inheritFrom(const MeasurementsEntryInfo& other)
+{
+  this->operator=(other);
+}
+
+shared_ptr<Face>
+NccStrategy::MeasurementsEntryInfo::getBestFace(void) {
+  shared_ptr<Face> best = m_bestFace.lock();
+  if (static_cast<bool>(best)) {
+    return best;
+  }
+  m_bestFace = best = m_previousFace.lock();
+  return best;
+}
+
+void
+NccStrategy::MeasurementsEntryInfo::updateBestFace(const Face& face) {
+  if (m_bestFace.expired()) {
+    m_bestFace = const_cast<Face&>(face).shared_from_this();
+    return;
+  }
+  shared_ptr<Face> bestFace = m_bestFace.lock();
+  if (bestFace.get() == &face) {
+    this->adjustPredictDown();
+  }
+  else {
+    m_previousFace = m_bestFace;
+    m_bestFace = const_cast<Face&>(face).shared_from_this();
+  }
+}
+
+void
+NccStrategy::MeasurementsEntryInfo::adjustPredictDown() {
+  m_prediction = std::max(MIN_PREDICTION,
+    static_cast<time::Duration>(m_prediction - (m_prediction >> ADJUST_PREDICT_DOWN_SHIFT)));
+}
+
+void
+NccStrategy::MeasurementsEntryInfo::adjustPredictUp() {
+  m_prediction = std::min(MAX_PREDICTION,
+    static_cast<time::Duration>(m_prediction + (m_prediction >> ADJUST_PREDICT_UP_SHIFT)));
+}
+
+void
+NccStrategy::MeasurementsEntryInfo::ageBestFace() {
+  m_previousFace = m_bestFace;
+  m_bestFace.reset();
+}
+
+NccStrategy::PitEntryInfo::PitEntryInfo()
+  : m_isNewInterest(true)
+{
+}
+
+NccStrategy::PitEntryInfo::~PitEntryInfo()
+{
+  scheduler::cancel(m_bestFaceTimeout);
+  scheduler::cancel(m_propagateTimer);
+}
+
+} // namespace fw
+} // namespace nfd
diff --git a/daemon/fw/ncc-strategy.hpp b/daemon/fw/ncc-strategy.hpp
new file mode 100644
index 0000000..5d1042f
--- /dev/null
+++ b/daemon/fw/ncc-strategy.hpp
@@ -0,0 +1,117 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (C) 2014 Named Data Networking Project
+ * See COPYING for copyright and distribution information.
+ */
+
+#ifndef NFD_FW_NCC_STRATEGY_HPP
+#define NFD_FW_NCC_STRATEGY_HPP
+
+#include "strategy.hpp"
+#include "forwarder.hpp"
+#include "strategy-info.hpp"
+
+namespace nfd {
+namespace fw {
+
+/** \brief a forwarding strategy similar to CCNx 0.7.2
+ */
+class NccStrategy : public Strategy
+{
+public:
+  explicit
+  NccStrategy(Forwarder& forwarder);
+
+  virtual
+  ~NccStrategy();
+
+  virtual void
+  afterReceiveInterest(const Face& inFace,
+                       const Interest& interest,
+                       shared_ptr<fib::Entry> fibEntry,
+                       shared_ptr<pit::Entry> pitEntry);
+
+  virtual void
+  beforeSatisfyPendingInterest(shared_ptr<pit::Entry> pitEntry,
+                               const Face& inFace, const Data& data);
+
+protected:
+  /// StrategyInfo on measurements::Entry
+  class MeasurementsEntryInfo : public StrategyInfo
+  {
+  public:
+    MeasurementsEntryInfo();
+
+    void
+    inheritFrom(const MeasurementsEntryInfo& other);
+
+    shared_ptr<Face>
+    getBestFace();
+
+    void
+    updateBestFace(const Face& face);
+
+    void
+    adjustPredictUp();
+
+  private:
+    void
+    adjustPredictDown();
+
+    void
+    ageBestFace();
+
+  public:
+    weak_ptr<Face> m_bestFace;
+    weak_ptr<Face> m_previousFace;
+    time::Duration m_prediction;
+
+    static const time::Duration INITIAL_PREDICTION;
+    static const time::Duration MIN_PREDICTION;
+    static const int ADJUST_PREDICT_DOWN_SHIFT = 7;
+    static const time::Duration MAX_PREDICTION;
+    static const int ADJUST_PREDICT_UP_SHIFT = 3;
+  };
+
+  /// StrategyInfo on pit::Entry
+  class PitEntryInfo : public StrategyInfo
+  {
+  public:
+    PitEntryInfo();
+
+    virtual
+    ~PitEntryInfo();
+
+  public:
+    bool m_isNewInterest;
+    EventId m_bestFaceTimeout;
+    EventId m_propagateTimer;
+    time::Duration m_maxInterval;
+  };
+
+protected:
+  shared_ptr<MeasurementsEntryInfo>
+  getMeasurementsEntryInfo(shared_ptr<measurements::Entry> entry);
+
+  shared_ptr<MeasurementsEntryInfo>
+  getMeasurementsEntryInfo(shared_ptr<pit::Entry> entry);
+
+  /// propagate to another upstream
+  void
+  doPropagate(weak_ptr<pit::Entry> pitEntryWeak, weak_ptr<fib::Entry> fibEntryWeak);
+
+  /// best face did not reply within prediction
+  void
+  timeoutOnBestFace(weak_ptr<pit::Entry> pitEntryWeak);
+
+protected:
+  static const time::Duration DEFER_FIRST_WITHOUT_BEST_FACE;
+  static const time::Duration DEFER_RANGE_WITHOUT_BEST_FACE;
+  static const int UPDATE_MEASUREMENTS_N_LEVELS = 2;
+  static const time::Duration MEASUREMENTS_LIFETIME;
+};
+
+} // namespace fw
+} // namespace nfd
+
+#endif // NFD_FW_NCC_STRATEGY_HPP
diff --git a/daemon/table/fib-entry.cpp b/daemon/table/fib-entry.cpp
index 61b9827..733782b 100644
--- a/daemon/table/fib-entry.cpp
+++ b/daemon/table/fib-entry.cpp
@@ -20,6 +20,14 @@
   return nexthop.getFace() == face;
 }
 
+bool
+Entry::hasNextHop(shared_ptr<Face> face) const
+{
+  NextHopList::const_iterator it = std::find_if(m_nextHops.begin(), m_nextHops.end(),
+    bind(&predicate_NextHop_eq_Face, _1, face));
+  return it != m_nextHops.end();
+}
+
 void
 Entry::addNextHop(shared_ptr<Face> face, int32_t cost)
 {
diff --git a/daemon/table/fib-entry.hpp b/daemon/table/fib-entry.hpp
index 6adddd8..f5c0fae 100644
--- a/daemon/table/fib-entry.hpp
+++ b/daemon/table/fib-entry.hpp
@@ -38,12 +38,12 @@
   const Name&
   getPrefix() const;
   
-  /** \brief gives the nexthops explicitly defined on this entry
-   *  This list does not include inherited nexthops.
-   */
   const NextHopList&
   getNextHops() const;
   
+  bool
+  hasNextHop(shared_ptr<Face> face) const;
+  
   /// adds a nexthop
   void
   addNextHop(shared_ptr<Face> face, int32_t cost);
diff --git a/daemon/table/measurements.cpp b/daemon/table/measurements.cpp
index ed01018..8caf343 100644
--- a/daemon/table/measurements.cpp
+++ b/daemon/table/measurements.cpp
@@ -19,6 +19,11 @@
 
 Measurements::~Measurements()
 {
+  for (std::map<Name, shared_ptr<measurements::Entry> >::iterator it = m_table.begin();
+       it != m_table.end(); ++it) {
+    shared_ptr<measurements::Entry> entry = it->second;
+    scheduler::cancel(entry->m_cleanup);
+  }
 }
 
 shared_ptr<measurements::Entry>
diff --git a/daemon/table/pit-entry.cpp b/daemon/table/pit-entry.cpp
index 7a719df..2d016ca 100644
--- a/daemon/table/pit-entry.cpp
+++ b/daemon/table/pit-entry.cpp
@@ -33,6 +33,38 @@
   return m_outRecords;
 }
 
+static inline bool
+predicate_FaceRecord_Face(const FaceRecord& faceRecord, shared_ptr<Face> face)
+{
+  return faceRecord.getFace() == face;
+}
+
+static inline bool
+predicate_FaceRecord_ne_Face_and_unexpired(const FaceRecord& faceRecord,
+  shared_ptr<Face> face, time::Point now)
+{
+  return faceRecord.getFace() != face && faceRecord.getExpiry() >= now;
+}
+
+bool
+Entry::canForwardTo(shared_ptr<Face> face) const
+{
+  OutRecordCollection::const_iterator outIt = std::find_if(
+    m_outRecords.begin(), m_outRecords.end(),
+    bind(&predicate_FaceRecord_Face, _1, face));
+  bool hasUnexpiredOutRecord = outIt != m_outRecords.end() &&
+                               outIt->getExpiry() >= time::now();
+  if (hasUnexpiredOutRecord) {
+    return false;
+  }
+  
+  InRecordCollection::const_iterator inIt = std::find_if(
+    m_inRecords.begin(), m_inRecords.end(),
+    bind(&predicate_FaceRecord_ne_Face_and_unexpired, _1, face, time::now()));
+  bool hasUnexpiredOtherInRecord = inIt != m_inRecords.end();
+  return hasUnexpiredOtherInRecord;
+}
+
 bool
 Entry::addNonce(uint32_t nonce)
 {
@@ -42,12 +74,6 @@
   return insertResult.second;
 }
 
-static inline bool
-predicate_FaceRecord_Face(const FaceRecord& faceRecord, shared_ptr<Face> face)
-{
-  return faceRecord.getFace() == face;
-}
-
 InRecordCollection::iterator
 Entry::insertOrUpdateInRecord(shared_ptr<Face> face, const Interest& interest)
 {
diff --git a/daemon/table/pit-entry.hpp b/daemon/table/pit-entry.hpp
index 979af6c..c9cfdac 100644
--- a/daemon/table/pit-entry.hpp
+++ b/daemon/table/pit-entry.hpp
@@ -24,61 +24,68 @@
  */
 typedef std::list<OutRecord> OutRecordCollection;
 
-/** \class Entry
- *  \brief represents a PIT entry
+/** \brief represents a PIT entry
  */
 class Entry : public StrategyInfoHost, noncopyable
 {
 public:
   explicit
   Entry(const Interest& interest);
-  
+
   const Interest&
   getInterest() const;
-  
-  /** \return{ Interest Name }
+
+  /** \return Interest Name
    */
   const Name&
   getName() const;
-  
+
   const InRecordCollection&
   getInRecords() const;
-  
+
   const OutRecordCollection&
   getOutRecords() const;
-  
+
+  /** \brief decides whether Interest can be forwarded to face
+   *
+   *  \return true if OutRecord of this face does not exist or has expired,
+   *          and there is an InRecord not of this face
+   */
+  bool
+  canForwardTo(shared_ptr<Face> face) const;
+
   /** \brief records a nonce
    *
-   *  \return{ true if nonce is new; false if nonce is seen before }
+   *  \return true if nonce is new; false if nonce is seen before
    */
   bool
   addNonce(uint32_t nonce);
-  
+
   /** \brief inserts a InRecord for face, and updates it with interest
    *
    *  If InRecord for face exists, the existing one is updated.
    *  This method does not add the Nonce as a seen Nonce.
-   *  \return{ an iterator to the InRecord }
+   *  \return an iterator to the InRecord
    */
   InRecordCollection::iterator
   insertOrUpdateInRecord(shared_ptr<Face> face, const Interest& interest);
-  
+
   /// deletes all InRecords
   void
   deleteInRecords();
-  
+
   /** \brief inserts a OutRecord for face, and updates it with interest
    *
    *  If OutRecord for face exists, the existing one is updated.
-   *  \return{ an iterator to the OutRecord }
+   *  \return an iterator to the OutRecord
    */
   OutRecordCollection::iterator
   insertOrUpdateOutRecord(shared_ptr<Face> face, const Interest& interest);
-  
+
   /// deletes one OutRecord for face if exists
   void
   deleteOutRecord(shared_ptr<Face> face);
-  
+
 public:
   EventId m_unsatisfyTimer;
   EventId m_stragglerTimer;
diff --git a/daemon/table/strategy-info-host.hpp b/daemon/table/strategy-info-host.hpp
index 41199b2..272177f 100644
--- a/daemon/table/strategy-info-host.hpp
+++ b/daemon/table/strategy-info-host.hpp
@@ -25,6 +25,14 @@
   shared_ptr<T>
   getStrategyInfo();
   
+  template<typename T>
+  shared_ptr<T>
+  getOrCreateStrategyInfo();
+  
+  template<typename T, typename T1>
+  shared_ptr<T>
+  getOrCreateStrategyInfo(T1& a1);
+  
   void
   clearStrategyInfo();
 
@@ -47,6 +55,30 @@
   return static_pointer_cast<T, fw::StrategyInfo>(m_strategyInfo);
 }
 
+template<typename T>
+shared_ptr<T>
+StrategyInfoHost::getOrCreateStrategyInfo()
+{
+  shared_ptr<T> info = this->getStrategyInfo<T>();
+  if (!static_cast<bool>(info)) {
+    info = make_shared<T>();
+    this->setStrategyInfo(info);
+  }
+  return info;
+}
+
+template<typename T, typename T1>
+shared_ptr<T>
+StrategyInfoHost::getOrCreateStrategyInfo(T1& a1)
+{
+  shared_ptr<T> info = this->getStrategyInfo<T>();
+  if (!static_cast<bool>(info)) {
+    info = make_shared<T>(boost::ref(a1));
+    this->setStrategyInfo(info);
+  }
+  return info;
+}
+
 } // namespace nfd
 
 #endif // NFD_TABLE_STRATEGY_INFO_HOST_HPP
diff --git a/tests/core/limited-io.cpp b/tests/core/limited-io.cpp
new file mode 100644
index 0000000..5752685
--- /dev/null
+++ b/tests/core/limited-io.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (C) 2014 Named Data Networking Project
+ * See COPYING for copyright and distribution information.
+ */
+
+#include "limited-io.hpp"
+
+namespace nfd {
+
+const int LimitedIo::UNLIMITED_OPS = std::numeric_limits<int>::max();
+const time::Duration LimitedIo::UNLIMITED_TIME = time::nanoseconds(-1);
+
+LimitedIo::LimitedIo()
+  : m_isRunning(false)
+  , m_nOpsRemaining(0)
+{
+  resetGlobalIoService();
+}
+
+LimitedIo::StopReason
+LimitedIo::run(int nOpsLimit, time::Duration nTimeLimit)
+{
+  BOOST_ASSERT(!m_isRunning);
+  m_isRunning = true;
+  
+  m_reason = NO_WORK;
+  m_nOpsRemaining = nOpsLimit;
+  if (nTimeLimit != UNLIMITED_TIME) {
+    m_timeout = scheduler::schedule(nTimeLimit, bind(&LimitedIo::afterTimeout, this));
+  }
+  
+  getGlobalIoService().run();
+  
+  getGlobalIoService().reset();
+  scheduler::cancel(m_timeout);
+  m_isRunning = false;
+  return m_reason;
+}
+
+void
+LimitedIo::afterOp()
+{
+  --m_nOpsRemaining;
+  if (m_nOpsRemaining <= 0) {
+    m_reason = EXCEED_OPS;
+    getGlobalIoService().stop();
+  }
+}
+
+void
+LimitedIo::afterTimeout()
+{
+  m_reason = EXCEED_TIME;
+  getGlobalIoService().stop();
+}
+
+} // namespace nfd
diff --git a/tests/core/limited-io.hpp b/tests/core/limited-io.hpp
new file mode 100644
index 0000000..14dadd7
--- /dev/null
+++ b/tests/core/limited-io.hpp
@@ -0,0 +1,49 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (C) 2014 Named Data Networking Project
+ * See COPYING for copyright and distribution information.
+ */
+
+#ifndef NFD_TEST_CORE_LIMITED_IO_HPP
+#define NFD_TEST_CORE_LIMITED_IO_HPP
+
+#include "core/scheduler.hpp"
+
+namespace nfd {
+
+class LimitedIo
+{
+public:
+  LimitedIo();
+  
+  enum StopReason
+  {
+    NO_WORK,
+    EXCEED_OPS,
+    EXCEED_TIME
+  };
+  
+  StopReason
+  run(int nOpsLimit, time::Duration nTimeLimit);
+  
+  void
+  afterOp();
+  
+private:
+  void
+  afterTimeout();
+
+public:
+  static const int UNLIMITED_OPS;
+  static const time::Duration UNLIMITED_TIME;
+
+private:
+  bool m_isRunning;
+  int m_nOpsRemaining;
+  EventId m_timeout;
+  StopReason m_reason;
+};
+
+} // namespace nfd
+
+#endif // NFD_TEST_CORE_LIMITED_IO_HPP
diff --git a/tests/fw/ncc-strategy.cpp b/tests/fw/ncc-strategy.cpp
new file mode 100644
index 0000000..a258b83
--- /dev/null
+++ b/tests/fw/ncc-strategy.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (C) 2014 Named Data Networking Project
+ * See COPYING for copyright and distribution information.
+ */
+
+#include "fw/ncc-strategy.hpp"
+#include "strategy-tester.hpp"
+#include "../face/dummy-face.hpp"
+#include "../core/limited-io.hpp"
+
+#include <boost/test/unit_test.hpp>
+
+namespace nfd {
+
+BOOST_AUTO_TEST_SUITE(FwNccStrategy)
+
+// NccStrategy is fairly complex.
+// The most important property is:
+// it remembers which upstream is the fastest to return Data,
+// and favors this upstream in subsequent Interests.
+BOOST_AUTO_TEST_CASE(FavorRespondingUpstream)
+{
+  LimitedIo limitedIo;
+  Forwarder forwarder;
+  typedef StrategyTester<fw::NccStrategy> NccStrategyTester;
+  shared_ptr<NccStrategyTester> strategy = make_shared<NccStrategyTester>(boost::ref(forwarder));
+  strategy->onAction += bind(&LimitedIo::afterOp, &limitedIo);
+
+  shared_ptr<DummyFace> face1 = make_shared<DummyFace>();
+  shared_ptr<DummyFace> face2 = make_shared<DummyFace>();
+  shared_ptr<DummyFace> face3 = make_shared<DummyFace>();
+  forwarder.addFace(face1);
+  forwarder.addFace(face2);
+  forwarder.addFace(face3);
+
+  Fib& fib = forwarder.getFib();
+  shared_ptr<fib::Entry> fibEntry = fib.insert(Name()).first;
+  fibEntry->setStrategy(strategy);
+  fibEntry->addNextHop(face1, 10);
+  fibEntry->addNextHop(face2, 20);
+
+  Pit& pit = forwarder.getPit();
+
+  // first Interest: strategy knows nothing and follows routing
+  shared_ptr<Interest> interest1p = make_shared<Interest>("ndn:/0Jm1ajrW/%00");
+  Interest& interest1 = *interest1p;
+  interest1.setInterestLifetime(8000);
+  shared_ptr<pit::Entry> pitEntry1 = pit.insert(interest1).first;
+
+  pitEntry1->insertOrUpdateInRecord(face3, interest1);
+  strategy->afterReceiveInterest(*face3, interest1, fibEntry, pitEntry1);
+
+  // forwards to face1 because routing says it's best
+  limitedIo.run(LimitedIo::UNLIMITED_OPS, time::milliseconds(1));
+  BOOST_REQUIRE_EQUAL(strategy->m_sendInterestHistory.size(), 1);
+  BOOST_CHECK_EQUAL(strategy->m_sendInterestHistory[0].get<1>(), face1);
+
+  // forwards to face2 because face1 doesn't respond
+  limitedIo.run(LimitedIo::UNLIMITED_OPS, time::milliseconds(500));
+  BOOST_REQUIRE_EQUAL(strategy->m_sendInterestHistory.size(), 2);
+  BOOST_CHECK_EQUAL(strategy->m_sendInterestHistory[1].get<1>(), face2);
+
+  // face2 responds
+  shared_ptr<Data> data1p = make_shared<Data>("ndn:/0Jm1ajrW/%00");
+  Data& data1 = *data1p;
+  strategy->beforeSatisfyPendingInterest(pitEntry1, *face2, data1);
+  limitedIo.run(LimitedIo::UNLIMITED_OPS, time::milliseconds(500));
+
+  // second Interest: strategy knows face2 is best
+  shared_ptr<Interest> interest2p = make_shared<Interest>("ndn:/0Jm1ajrW/%00%01");
+  Interest& interest2 = *interest2p;
+  interest2.setInterestLifetime(8000);
+  shared_ptr<pit::Entry> pitEntry2 = pit.insert(interest2).first;
+
+  pitEntry2->insertOrUpdateInRecord(face3, interest2);
+  strategy->afterReceiveInterest(*face3, interest2, fibEntry, pitEntry2);
+
+  // forwards to face2 because it responds previously
+  limitedIo.run(LimitedIo::UNLIMITED_OPS, time::milliseconds(1));
+  BOOST_REQUIRE_GE(strategy->m_sendInterestHistory.size(), 3);
+  BOOST_CHECK_EQUAL(strategy->m_sendInterestHistory[2].get<1>(), face2);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // namespace nfd
diff --git a/tests/fw/strategy-tester.hpp b/tests/fw/strategy-tester.hpp
index 0ec4770..f8e0848 100644
--- a/tests/fw/strategy-tester.hpp
+++ b/tests/fw/strategy-tester.hpp
@@ -26,6 +26,9 @@
     : S(forwarder)
   {
   }
+  
+  /// fires after each Action
+  EventEmitter<> onAction;
 
 protected:
   virtual void
@@ -49,6 +52,8 @@
                                 shared_ptr<Face> outFace)
 {
   m_sendInterestHistory.push_back(SendInterestArgs(pitEntry, outFace));
+  pitEntry->insertOrUpdateOutRecord(outFace, pitEntry->getInterest());
+  onAction();
 }
 
 template<typename S>
@@ -56,6 +61,7 @@
 StrategyTester<S>::rejectPendingInterest(shared_ptr<pit::Entry> pitEntry)
 {
   m_rejectPendingInterestHistory.push_back(RejectPendingInterestArgs(pitEntry));
+  onAction();
 }
 
 
diff --git a/wscript b/wscript
index a66bfc4..b7f96c9 100644
--- a/wscript
+++ b/wscript
@@ -32,6 +32,7 @@
                                                  '-Qunused-arguments',        # only clang supports
                                                  '-Wno-tautological-compare', # suppress warnings from CryptoPP
                                                  '-Wno-unused-function',      # suppress warnings from CryptoPP
+                                                 '-fno-inline',
                                                  ])
     else:
         conf.add_supported_cxxflags (cxxflags = ['-O3', '-g', '-Wno-tautological-compare', '-Wno-unused-function'])