Add ASF measurement lifetime as strategy parameter

Refs: #5332
Change-Id: Iff42ca0870d4263f0ad8757a67b87e52dbfff9f5
diff --git a/daemon/fw/asf-measurements.cpp b/daemon/fw/asf-measurements.cpp
index 957e3a0..73b7cca 100644
--- a/daemon/fw/asf-measurements.cpp
+++ b/daemon/fw/asf-measurements.cpp
@@ -69,7 +69,7 @@
 void
 NamespaceInfo::extendFaceInfoLifetime(FaceInfo& info, FaceId faceId)
 {
-  info.m_measurementExpiration = getScheduler().schedule(AsfMeasurements::MEASUREMENTS_LIFETIME,
+  info.m_measurementExpiration = getScheduler().schedule(m_measurementLifetime,
                                                          [=] { m_fiMap.erase(faceId); });
 }
 
@@ -105,7 +105,7 @@
   // Set or update entry lifetime
   extendLifetime(*me);
 
-  NamespaceInfo* info = me->insertStrategyInfo<NamespaceInfo>(m_rttEstimatorOpts).first;
+  NamespaceInfo* info = me->insertStrategyInfo<NamespaceInfo>(m_rttEstimatorOpts, m_measurementsLifetime).first;
   BOOST_ASSERT(info != nullptr);
   return info;
 }
@@ -129,7 +129,7 @@
   // Set or update entry lifetime
   extendLifetime(*me);
 
-  NamespaceInfo* info = me->insertStrategyInfo<NamespaceInfo>(m_rttEstimatorOpts).first;
+  NamespaceInfo* info = me->insertStrategyInfo<NamespaceInfo>(m_rttEstimatorOpts, m_measurementsLifetime).first;
   BOOST_ASSERT(info != nullptr);
   return *info;
 }
@@ -137,7 +137,7 @@
 void
 AsfMeasurements::extendLifetime(measurements::Entry& me)
 {
-  m_measurements.extendLifetime(me, MEASUREMENTS_LIFETIME);
+  m_measurements.extendLifetime(me, m_measurementsLifetime);
 }
 
 } // namespace nfd::fw::asf
diff --git a/daemon/fw/asf-measurements.hpp b/daemon/fw/asf-measurements.hpp
index 75a3c2b..77925b6 100644
--- a/daemon/fw/asf-measurements.hpp
+++ b/daemon/fw/asf-measurements.hpp
@@ -132,8 +132,9 @@
   }
 
   explicit
-  NamespaceInfo(shared_ptr<const ndn::util::RttEstimator::Options> opts)
+  NamespaceInfo(shared_ptr<const ndn::util::RttEstimator::Options> opts, time::milliseconds measurementLifetime)
     : m_rttEstimatorOpts(std::move(opts))
+    , m_measurementLifetime(measurementLifetime)
   {
   }
 
@@ -173,6 +174,7 @@
 private:
   std::unordered_map<FaceId, FaceInfo> m_fiMap;
   shared_ptr<const ndn::util::RttEstimator::Options> m_rttEstimatorOpts;
+  time::milliseconds m_measurementLifetime;
   bool m_isProbingDue = false;
   bool m_isFirstProbeScheduled = false;
 };
@@ -201,14 +203,29 @@
   NamespaceInfo&
   getOrCreateNamespaceInfo(const fib::Entry& fibEntry, const Name& prefix);
 
+  void
+  setMeasurementsLifetime(time::milliseconds measurementsLifetime)
+  {
+    // Measurement lifetime should not expire as soon as it is configured
+    BOOST_ASSERT(measurementsLifetime > 0_ms);
+    m_measurementsLifetime = measurementsLifetime;
+  }
+
+  time::milliseconds
+  getMeasurementsLifetime() const
+  {
+    return m_measurementsLifetime;
+  }
+
 private:
   void
   extendLifetime(measurements::Entry& me);
 
 public:
-  static constexpr time::microseconds MEASUREMENTS_LIFETIME = 5_min;
+  static constexpr time::milliseconds DEFAULT_MEASUREMENTS_LIFETIME = 5_min;
 
 private:
+  time::milliseconds m_measurementsLifetime = DEFAULT_MEASUREMENTS_LIFETIME;
   MeasurementsAccessor& m_measurements;
   shared_ptr<const ndn::util::RttEstimator::Options> m_rttEstimatorOpts;
 };
diff --git a/daemon/fw/asf-probing-module.cpp b/daemon/fw/asf-probing-module.cpp
index a465a5b..27c493e 100644
--- a/daemon/fw/asf-probing-module.cpp
+++ b/daemon/fw/asf-probing-module.cpp
@@ -32,7 +32,7 @@
 
 namespace nfd::fw::asf {
 
-static_assert(ProbingModule::DEFAULT_PROBING_INTERVAL < AsfMeasurements::MEASUREMENTS_LIFETIME);
+static_assert(ProbingModule::DEFAULT_PROBING_INTERVAL < AsfMeasurements::DEFAULT_MEASUREMENTS_LIFETIME);
 
 ProbingModule::ProbingModule(AsfMeasurements& measurements)
   : m_probingInterval(DEFAULT_PROBING_INTERVAL)
diff --git a/daemon/fw/asf-strategy.cpp b/daemon/fw/asf-strategy.cpp
index edb6ec1..8688729 100644
--- a/daemon/fw/asf-strategy.cpp
+++ b/daemon/fw/asf-strategy.cpp
@@ -26,6 +26,7 @@
 #include "asf-strategy.hpp"
 #include "algorithm.hpp"
 #include "common/logger.hpp"
+#include <boost/lexical_cast.hpp>
 
 namespace nfd::fw::asf {
 
@@ -48,12 +49,21 @@
                                                                       m_probing.getProbingInterval().count());
   m_probing.setProbingInterval(time::milliseconds(probingInterval));
   m_nMaxTimeouts = params.getOrDefault<size_t>("max-timeouts", m_nMaxTimeouts);
+  auto measurementsLifetime = time::milliseconds(params.getOrDefault<time::milliseconds::rep>("measurements-lifetime",
+                                                                      AsfMeasurements::DEFAULT_MEASUREMENTS_LIFETIME.count()));
+  if (measurementsLifetime <= m_probing.getProbingInterval()) {
+    NDN_THROW(std::invalid_argument("Measurements lifetime (" + boost::lexical_cast<std::string>(measurementsLifetime) +
+                                    ") should be greater than the probing interval of " +
+                                    boost::lexical_cast<std::string>(m_probing.getProbingInterval())));
+  }
+  m_measurements.setMeasurementsLifetime(measurementsLifetime);
 
   this->setInstanceName(makeInstanceName(name, getStrategyName()));
 
   NDN_LOG_DEBUG(*m_retxSuppression);
   NFD_LOG_DEBUG("probing-interval=" << m_probing.getProbingInterval()
-                << " max-timeouts=" << m_nMaxTimeouts);
+                << " max-timeouts=" << m_nMaxTimeouts
+                << " measurements-lifetime=" << m_measurements.getMeasurementsLifetime());
 }
 
 const Name&
diff --git a/daemon/fw/asf-strategy.hpp b/daemon/fw/asf-strategy.hpp
index 83bff13..b41b4ab 100644
--- a/daemon/fw/asf-strategy.hpp
+++ b/daemon/fw/asf-strategy.hpp
@@ -85,9 +85,6 @@
   void
   sendNoRouteNack(Face& face, const shared_ptr<pit::Entry>& pitEntry);
 
-private:
-  AsfMeasurements m_measurements{getMeasurements()};
-
 NFD_PUBLIC_WITH_TESTS_ELSE_PRIVATE:
   struct FaceStatsForwardingCompare
   {
@@ -96,6 +93,7 @@
   };
   using FaceStatsForwardingSet = std::set<FaceStats, FaceStatsForwardingCompare>;
 
+  AsfMeasurements m_measurements{getMeasurements()};
   std::unique_ptr<RetxSuppressionExponential> m_retxSuppression;
   ProbingModule m_probing{m_measurements};
   size_t m_nMaxTimeouts = 3;
diff --git a/docs/manpages/nfd-asf-strategy.rst b/docs/manpages/nfd-asf-strategy.rst
index 45339b8..ff7079a 100644
--- a/docs/manpages/nfd-asf-strategy.rst
+++ b/docs/manpages/nfd-asf-strategy.rst
@@ -24,6 +24,14 @@
     integer). Smaller values will result in higher overhead but faster reaction.
     The default value is 1 minute and the minimum value is 1 second.
 
+.. option:: measurements-lifetime <LIFETIME>
+
+    This optional parameter tells ASF how long to retain NamespaceInfo and FaceInfo
+    measurements if they are not actively updated. If not specified, this value defaults
+    to 5 minutes. This value is specified in (non-negative integer) milliseconds and must
+    be greater than the probing interval, as otherwise there is negligible benefit gained
+    from the additional traffic generated by ASF.
+
 .. option:: max-timeouts <TIMEOUTS>
 
     This optional parameter makes ASF switch to another appropriate face (if available)
@@ -45,8 +53,11 @@
 ``nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/asf/v=5/max-timeouts~5``
     Set the maximum number of timeouts to 5.
 
-``nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/asf/v=5/probing-interval~30000/max-timeouts~2``
-    Set the probing interval to 30 seconds and the maximum number of timeouts to 2.
+``nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/asf/v=5/measurements-lifetime~120000``
+    Set the maximum measurement lifetime to 2 minutes.
+
+``nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/asf/v=5/probing-interval~30000/max-timeouts~2/measurements-lifetime~120000``
+    Set the probing interval to 30 seconds, the maximum number of timeouts to 2, and the maximum measurement lifetime to 2 minutes.
 
 ``nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/asf/v=5/retx-suppression-multiplier~2.5/probing-interval~45000``
     Set the retransmission suppression multiplier to 2.5 and the probing interval
diff --git a/tests/daemon/fw/asf-measurements.t.cpp b/tests/daemon/fw/asf-measurements.t.cpp
index 4de0493..e44144b 100644
--- a/tests/daemon/fw/asf-measurements.t.cpp
+++ b/tests/daemon/fw/asf-measurements.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2022,  Regents of the University of California,
+ * Copyright (c) 2014-2024,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -75,14 +75,14 @@
 BOOST_FIXTURE_TEST_CASE(NamespaceInfo, GlobalIoTimeFixture)
 {
   using fw::asf::NamespaceInfo;
-  NamespaceInfo info(nullptr);
+  NamespaceInfo info(nullptr, fw::asf::AsfMeasurements::DEFAULT_MEASUREMENTS_LIFETIME);
 
   BOOST_CHECK(info.getFaceInfo(1234) == nullptr);
 
   auto& faceInfo = info.getOrCreateFaceInfo(1234);
   BOOST_CHECK(info.getFaceInfo(1234) == &faceInfo);
 
-  this->advanceClocks(fw::asf::AsfMeasurements::MEASUREMENTS_LIFETIME + 1_s);
+  this->advanceClocks(fw::asf::AsfMeasurements::DEFAULT_MEASUREMENTS_LIFETIME + 1_s);
   BOOST_CHECK(info.getFaceInfo(1234) == nullptr); // expired
 }
 
diff --git a/tests/daemon/fw/asf-strategy.t.cpp b/tests/daemon/fw/asf-strategy.t.cpp
index dce7118..17a19a7 100644
--- a/tests/daemon/fw/asf-strategy.t.cpp
+++ b/tests/daemon/fw/asf-strategy.t.cpp
@@ -561,18 +561,33 @@
   auto strategy = checkValidity("", true);
   BOOST_TEST(strategy->m_probing.getProbingInterval() == 60_s);
   BOOST_TEST(strategy->m_nMaxTimeouts == 3);
-  strategy = checkValidity("/probing-interval~30000/max-timeouts~5", true);
+  BOOST_TEST(strategy->m_measurements.getMeasurementsLifetime() == 5_min);
+  strategy = checkValidity("/probing-interval~30000/max-timeouts~5/measurements-lifetime~120000", true);
   BOOST_TEST(strategy->m_probing.getProbingInterval() == 30_s);
   BOOST_TEST(strategy->m_nMaxTimeouts == 5);
+  BOOST_TEST(strategy->m_measurements.getMeasurementsLifetime() == 2_min);
   strategy = checkValidity("/max-timeouts~5/probing-interval~30000", true);
   BOOST_TEST(strategy->m_probing.getProbingInterval() == 30_s);
   BOOST_TEST(strategy->m_nMaxTimeouts == 5);
+  BOOST_TEST(strategy->m_measurements.getMeasurementsLifetime() == 5_min);
+  strategy = checkValidity("/max-timeouts~5/measurements-lifetime~120000", true);
+  BOOST_TEST(strategy->m_nMaxTimeouts == 5);
+  BOOST_TEST(strategy->m_measurements.getMeasurementsLifetime() == 2_min);
+  strategy = checkValidity("/probing-interval~30000/measurements-lifetime~120000", true);
+  BOOST_TEST(strategy->m_probing.getProbingInterval() == 30_s);
+  BOOST_TEST(strategy->m_measurements.getMeasurementsLifetime() == 2_min);
   strategy = checkValidity("/probing-interval~1000", true);
   BOOST_TEST(strategy->m_probing.getProbingInterval() == 1_s);
   BOOST_TEST(strategy->m_nMaxTimeouts == 3);
+  BOOST_TEST(strategy->m_measurements.getMeasurementsLifetime() == 5_min);
   strategy = checkValidity("/max-timeouts~0", true);
   BOOST_TEST(strategy->m_probing.getProbingInterval() == 60_s);
   BOOST_TEST(strategy->m_nMaxTimeouts == 0);
+  BOOST_TEST(strategy->m_measurements.getMeasurementsLifetime() == 5_min);
+  strategy = checkValidity("/measurements-lifetime~120000", true);
+  BOOST_TEST(strategy->m_probing.getProbingInterval() == 60_s);
+  BOOST_TEST(strategy->m_nMaxTimeouts == 3);
+  BOOST_TEST(strategy->m_measurements.getMeasurementsLifetime() == 2_min);
   BOOST_TEST(strategy->m_retxSuppression->m_initialInterval == fw::RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL);
   BOOST_TEST(strategy->m_retxSuppression->m_maxInterval == fw::RetxSuppressionExponential::DEFAULT_MAX_INTERVAL);
   BOOST_TEST(strategy->m_retxSuppression->m_multiplier == fw::RetxSuppressionExponential::DEFAULT_MULTIPLIER);
@@ -585,6 +600,15 @@
   checkValidity("/max-timeouts~1/probing-interval~-30000", false);
   checkValidity("/probing-interval~foo", false);
   checkValidity("/max-timeouts~1~2", false);
+  checkValidity("/measurements-lifetime~1000", false); //Minimum is 60s by default
+  //Measurement lifetime must be greater than probing interval
+  checkValidity("/measurements-lifetime~1000/probing-interval~30000", false);
+  checkValidity("/measurements-lifetime~-120000", false);
+  checkValidity("/measurements-lifetime~ -120000", false);
+  checkValidity("/measurements-lifetime~0-120000", false);
+  checkValidity("/max-timeouts~1/measurements-lifetime~-120000", false);
+  checkValidity("/probing-interval~30000/measurements-lifetime~-120000", false);
+  checkValidity("/max-timeouts~1/probing-interval~30000/measurements-lifetime~-120000", false);
 }
 
 BOOST_AUTO_TEST_CASE(FaceRankingForForwarding)