util: keep Scheduler running if a callback throws

refs #3722

Change-Id: Icf19e1a59d67e351b210a60079b27b9b0a06fae6
diff --git a/src/util/scheduler.cpp b/src/util/scheduler.cpp
index 536061c..15200c9 100644
--- a/src/util/scheduler.cpp
+++ b/src/util/scheduler.cpp
@@ -19,9 +19,8 @@
  * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
  */
 
-#include "common.hpp"
-
 #include "scheduler.hpp"
+#include <boost/scope_exit.hpp>
 
 namespace ndn {
 namespace util {
@@ -140,6 +139,11 @@
 
   m_isEventExecuting = true;
 
+  BOOST_SCOPE_EXIT_ALL(this) {
+    m_isEventExecuting = false;
+    this->scheduleNext();
+  };
+
   // process all expired events
   time::steady_clock::TimePoint now = time::steady_clock::now();
   while (!m_queue.empty()) {
@@ -153,9 +157,6 @@
     info->isExpired = true;
     info->callback();
   }
-
-  m_isEventExecuting = false;
-  this->scheduleNext();
 }
 
 } // namespace scheduler
diff --git a/src/util/scheduler.hpp b/src/util/scheduler.hpp
index c908ec0..cad5c09 100644
--- a/src/util/scheduler.hpp
+++ b/src/util/scheduler.hpp
@@ -137,7 +137,7 @@
 
   /**
    * \brief Schedule a one-time event after the specified delay
-   * \returns EventId that can be used to cancel the scheduled event
+   * \return EventId that can be used to cancel the scheduled event
    */
   EventId
   scheduleEvent(const time::nanoseconds& after, const EventCallback& callback);
@@ -161,6 +161,12 @@
   void
   scheduleNext();
 
+  /**
+   * \brief Execute expired events
+   * \note If an event callback throws, the exception is propagated to the thread running the
+   *       io_service. In case there are other expired events, they will be processed in the next
+   *       invocation of this method.
+   */
   void
   executeEvent(const boost::system::error_code& code);
 
diff --git a/tests/unit-tests/util/scheduler.t.cpp b/tests/unit-tests/util/scheduler.t.cpp
index dfc2826..72db284 100644
--- a/tests/unit-tests/util/scheduler.t.cpp
+++ b/tests/unit-tests/util/scheduler.t.cpp
@@ -78,6 +78,21 @@
   BOOST_CHECK_EQUAL(count2, 1);
 }
 
+BOOST_AUTO_TEST_CASE(CallbackException)
+{
+  class MyException : public std::exception
+  {
+  };
+  scheduler.scheduleEvent(time::milliseconds(10), [] { BOOST_THROW_EXCEPTION(MyException()); });
+
+  bool isCallbackInvoked = false;
+  scheduler.scheduleEvent(time::milliseconds(20), [&isCallbackInvoked] { isCallbackInvoked = true; });
+
+  BOOST_CHECK_THROW(this->advanceClocks(time::milliseconds(6), 2), MyException);
+  this->advanceClocks(time::milliseconds(6), 2);
+  BOOST_CHECK(isCallbackInvoked);
+}
+
 BOOST_AUTO_TEST_CASE(CancelEmptyEvent)
 {
   EventId i;