Browse Source

[master] Merge branch 'trac3970'

Marcin Siodelski 9 years ago
parent
commit
bc85030553

+ 0 - 1
src/lib/dhcp_ddns/Makefile.am

@@ -34,7 +34,6 @@ libkea_dhcp_ddns_la_SOURCES += dhcp_ddns_log.cc dhcp_ddns_log.h
 libkea_dhcp_ddns_la_SOURCES += ncr_io.cc ncr_io.h
 libkea_dhcp_ddns_la_SOURCES += ncr_msg.cc ncr_msg.h
 libkea_dhcp_ddns_la_SOURCES += ncr_udp.cc ncr_udp.h
-libkea_dhcp_ddns_la_SOURCES += watch_socket.cc watch_socket.h
 
 nodist_libkea_dhcp_ddns_la_SOURCES = dhcp_ddns_messages.cc dhcp_ddns_messages.h
 

+ 4 - 10
src/lib/dhcp_ddns/dhcp_ddns_messages.mes

@@ -89,14 +89,8 @@ caught in the application's send completion handler.  This is a programmatic
 error that needs to be reported.  Dependent upon the nature of the error the
 client may or may not continue operating normally.
 
-% DHCP_DDNS_WATCH_SINK_CLOSE_ERROR Sink-side watch socket failed to close: %1
+% DHCP_DDNS_UDP_SENDER_WATCH_SOCKET_CLOSE_ERROR watch socket failed to close: %1
 This is an error message that indicates the application was unable to close
-the inbound side of a NCR sender's watch socket.  While technically possible
-this error is highly unlikely to occur and should not impair the application's
-ability to process requests.
-
-% DHCP_DDNS_WATCH_SOURCE_CLOSE_ERROR Source-side watch socket failed to close: %1
-This is an error message that indicates the application was unable to close
-the outbound side of a NCR sender's watch socket.  While technically possible
-this error is highly unlikely to occur and should not impair the application's
-ability to process requests.
+the inbound or outbound side of a NCR sender's watch socket. While technically
+possible the error is highly unlikely to occur and should not impair the
+application's ability to process requests.

+ 14 - 2
src/lib/dhcp_ddns/ncr_udp.cc

@@ -260,7 +260,8 @@ NameChangeUDPSender::open(isc::asiolink::IOService& io_service) {
 
     send_callback_->setDataSource(server_endpoint_);
 
-    watch_socket_.reset(new WatchSocket());
+    closeWatchSocket();
+    watch_socket_.reset(new util::WatchSocket());
 }
 
 void
@@ -288,6 +289,7 @@ NameChangeUDPSender::close() {
 
     socket_.reset();
 
+    closeWatchSocket();
     watch_socket_.reset();
 }
 
@@ -372,7 +374,17 @@ NameChangeUDPSender::ioReady() {
     return (false);
 }
 
-
+void
+NameChangeUDPSender::closeWatchSocket() {
+    if (watch_socket_) {
+        std::string error_string;
+        watch_socket_->closeSocket(error_string);
+        if (!error_string.empty()) {
+            LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_UDP_SENDER_WATCH_SOCKET_CLOSE_ERROR)
+                .arg(error_string);
+        }
+    }
+}
 
 }; // end of isc::dhcp_ddns namespace
 }; // end of isc namespace

+ 10 - 3
src/lib/dhcp_ddns/ncr_udp.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -112,8 +112,8 @@
 #include <asiolink/udp_endpoint.h>
 #include <asiolink/udp_socket.h>
 #include <dhcp_ddns/ncr_io.h>
-#include <dhcp_ddns/watch_socket.h>
 #include <util/buffer.h>
+#include <util/watch_socket.h>
 
 #include <boost/shared_array.hpp>
 
@@ -548,6 +548,13 @@ public:
     virtual bool ioReady();
 
 private:
+
+    /// @brief Closes watch socket if the socket is open.
+    ///
+    /// This method closes watch socket if its instance exists and if the
+    /// socket is open. An error message is logged when this operation fails.
+    void closeWatchSocket();
+
     /// @brief IP address from which to send.
     isc::asiolink::IOAddress ip_address_;
 
@@ -579,7 +586,7 @@ private:
     bool reuse_address_;
 
     /// @brief Pointer to WatchSocket instance supplying the "select-fd".
-    WatchSocketPtr watch_socket_;
+    util::WatchSocketPtr watch_socket_;
 };
 
 } // namespace isc::dhcp_ddns

+ 0 - 1
src/lib/dhcp_ddns/tests/Makefile.am

@@ -30,7 +30,6 @@ libdhcp_ddns_unittests_SOURCES  = run_unittests.cc
 libdhcp_ddns_unittests_SOURCES += ncr_unittests.cc
 libdhcp_ddns_unittests_SOURCES += ncr_udp_unittests.cc
 libdhcp_ddns_unittests_SOURCES += test_utils.cc test_utils.h
-libdhcp_ddns_unittests_SOURCES += watch_socket_unittests.cc
 
 libdhcp_ddns_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
 

+ 3 - 3
src/lib/dhcp_ddns/tests/ncr_udp_unittests.cc

@@ -362,7 +362,7 @@ TEST(NameChangeUDPSenderBasicTest, basicSendTests) {
     int select_fd = sender.getSelectFd();
 
     // Verify select_fd is valid and currently shows no ready to read.
-    ASSERT_NE(dhcp_ddns::WatchSocket::SOCKET_NOT_VALID, select_fd);
+    ASSERT_NE(util::WatchSocket::SOCKET_NOT_VALID, select_fd);
 
     // Make sure select_fd does evaluates to not ready via select and
     // that ioReady() method agrees.
@@ -747,7 +747,7 @@ TEST(NameChangeUDPSenderBasicTest, watchClosedBeforeSendRequest) {
     close(sender.getSelectFd());
 
     // Send should fail as we interferred by closing the select-fd.
-    ASSERT_THROW(sender.sendRequest(ncr), WatchSocketError);
+    ASSERT_THROW(sender.sendRequest(ncr), util::WatchSocketError);
 
     // Verify we didn't invoke the handler.
     EXPECT_EQ(0, ncr_handler.pass_count_);
@@ -827,7 +827,7 @@ TEST(NameChangeUDPSenderBasicTest, watchSocketBadRead) {
     // Interfere by reading part of the marker from the select-fd.
     uint32_t buf = 0;
     ASSERT_EQ((read (select_fd, &buf, 1)), 1);
-    ASSERT_NE(WatchSocket::MARKER, buf);
+    ASSERT_NE(util::WatchSocket::MARKER, buf);
 
     // Run one handler. This should execute the send completion handler
     // after sending the message.  Duing completion handling clearing the

+ 1 - 0
src/lib/dhcpsrv/Makefile.am

@@ -120,6 +120,7 @@ libkea_dhcpsrv_la_SOURCES += srv_config.cc srv_config.h
 libkea_dhcpsrv_la_SOURCES += subnet.cc subnet.h
 libkea_dhcpsrv_la_SOURCES += subnet_id.h
 libkea_dhcpsrv_la_SOURCES += subnet_selector.h
+libkea_dhcpsrv_la_SOURCES += timer_mgr.cc timer_mgr.h
 libkea_dhcpsrv_la_SOURCES += triplet.h
 libkea_dhcpsrv_la_SOURCES += utils.h
 libkea_dhcpsrv_la_SOURCES += writable_host_data_source.h

+ 3 - 3
src/lib/dhcpsrv/d2_client_mgr.cc

@@ -30,7 +30,7 @@ namespace dhcp {
 
 D2ClientMgr::D2ClientMgr() : d2_client_config_(new D2ClientConfig()),
     name_change_sender_(), private_io_service_(),
-    registered_select_fd_(dhcp_ddns::WatchSocket::SOCKET_NOT_VALID) {
+    registered_select_fd_(util::WatchSocket::SOCKET_NOT_VALID) {
     // Default constructor initializes with a disabled configuration.
 }
 
@@ -275,9 +275,9 @@ D2ClientMgr::amSending() const {
 void
 D2ClientMgr::stopSender() {
     /// Unregister sender's select-fd.
-    if (registered_select_fd_ != dhcp_ddns::WatchSocket::SOCKET_NOT_VALID) {
+    if (registered_select_fd_ != util::WatchSocket::SOCKET_NOT_VALID) {
         IfaceMgr::instance().deleteExternalSocket(registered_select_fd_);
-        registered_select_fd_ = dhcp_ddns::WatchSocket::SOCKET_NOT_VALID;
+        registered_select_fd_ = util::WatchSocket::SOCKET_NOT_VALID;
     }
 
     // If its not null, call stop.

+ 1 - 1
src/lib/dhcpsrv/d2_client_mgr.h

@@ -409,7 +409,7 @@ protected:
     /// @brief Fetches the select-fd that is currently registered.
     ///
     /// @return The currently registered select-fd or
-    /// dhcp_ddns::WatchSocket::SOCKET_NOT_VALID.
+    /// util::WatchSocket::SOCKET_NOT_VALID.
     ///
     /// @note This is only exposed for testing purposes.
     int getRegisteredSelectFd();

+ 50 - 0
src/lib/dhcpsrv/dhcpsrv_messages.mes

@@ -544,6 +544,56 @@ lease from the PostgreSQL database for the specified address.
 A debug message issued when the server is attempting to update IPv6
 lease from the PostgreSQL database for the specified address.
 
+% DHCPSRV_TIMERMGR_REGISTER_TIMER registering timer: %1, using interval: %2 ms
+A debug message issued when the new interval timer is registered in
+the Timer Manager. This timer will have a callback function
+associated with it, and this function will be executed according
+to the interval specified. The unique name of the timer and the
+interval at which the callback function will be executed is
+included in the message.
+
+% DHCPSRV_TIMERMGR_RUN_TIMER_OPERATION running operation for timer: %1
+A debug message issued when the Timer Manager is about to
+run a periodic operation associated with the given timer.
+An example of such operation is a periodic cleanup of
+expired leases. The name of the timer is included in the
+message.
+
+% DHCPSRV_TIMERMGR_START_THREAD starting thread for timers
+A debug message issued when the Timer Manager is starting a
+worker thread to run started timers. The worker thread is
+typically started right after all timers have been registered
+and runs until timers need to be reconfigured, e.g. their
+interval is changed, new timers are registered or existing
+timers are unregistered.
+
+% DHCPSRV_TIMERMGR_START_TIMER starting timer: %1
+A debug message issued when the registered interval timer is
+being started. If this operation is successful the timer will
+periodically execute the operation associated with it. The
+name of the started timer is included in the message.
+
+% DHCPSRV_TIMERMGR_STOP_THREAD stopping thread for timers
+A debug message issued when the Timer Manager is stopping
+the worker thread which executes interval timers. When the
+thread is stopped no timers will be executed. The thread is
+typically stopped at the server reconfiguration or when the
+server shuts down.
+
+% DHCPSRV_TIMERMGR_STOP_TIMER stopping timer: %1
+A debug message issued when the registered interval timer is
+being stopped. The timer remains registered and can be restarted
+if necessary. The name of the timer is included in the message.
+
+% DHCPSRV_TIMERMGR_UNREGISTER_ALL_TIMERS unregistering all timers
+A debug message issued when all registered interval timers are
+being unregistered from the Timer Manager.
+
+% DHCPSRV_TIMERMGR_UNREGISTER_TIMER unregistering timer: %1
+A debug message issued when one of the registered interval timers
+is unregistered from the Timer Manager. The name of the timer is
+included in the message.
+
 % DHCPSRV_UNEXPECTED_NAME database access parameters passed through '%1', expected 'lease-database'
 The parameters for access the lease database were passed to the server through
 the named configuration parameter, but the code was expecting them to be

+ 1 - 0
src/lib/dhcpsrv/tests/Makefile.am

@@ -104,6 +104,7 @@ libdhcpsrv_unittests_SOURCES += subnet_unittest.cc
 libdhcpsrv_unittests_SOURCES += test_get_callout_handle.cc test_get_callout_handle.h
 libdhcpsrv_unittests_SOURCES += triplet_unittest.cc
 libdhcpsrv_unittests_SOURCES += test_utils.cc test_utils.h
+libdhcpsrv_unittests_SOURCES += timer_mgr_unittest.cc
 
 libdhcpsrv_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
 if HAVE_MYSQL

+ 473 - 0
src/lib/dhcpsrv/tests/timer_mgr_unittest.cc

@@ -0,0 +1,473 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asio.hpp>
+#include <config.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcpsrv/timer_mgr.h>
+#include <exceptions/exceptions.h>
+#include <boost/bind.hpp>
+#include <gtest/gtest.h>
+#include <sstream>
+#include <unistd.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::asiolink;
+
+namespace {
+
+/// @brief Test fixture class for @c TimerMgr.
+class TimerMgrTest : public ::testing::Test {
+private:
+    /// @brief Prepares the class for a test.
+    virtual void SetUp();
+
+    /// @brief Cleans up after the test.
+    virtual void TearDown();
+
+    /// @brief IO service used by the test fixture class.
+    IOService io_service_;
+
+    /// @brief Boolean flag which indicates that the timeout
+    /// for the @c doWait function has been reached.
+    bool timeout_;
+
+public:
+
+    /// @brief Wrapper method for registering a new timer.
+    ///
+    /// This method registers a new timer in the @c TimerMgr. It associates a
+    /// @c timerCallback method with a timer. This method registers a number of
+    /// calls to the particular timer in the @c calls_count_ map.
+    ///
+    /// @param timer_name Unique timer name.
+    /// @param timer_interval Timer interval.
+    /// @param mode Interval timer mode, which defaults to
+    /// @c IntervalTimer::ONE_SHOT.
+    void registerTimer(const std::string& timer_name, const long timer_interval,
+                       const IntervalTimer::Mode& timer_mode = IntervalTimer::ONE_SHOT);
+
+    /// @brief Wait for one or many ready handlers.
+    ///
+    /// @param timeout Wait timeout in milliseconds.
+    /// @param call_receive Indicates if the @c IfaceMgr::receive6
+    /// should be called to run pending callbacks and clear
+    /// watch sockets.
+    void doWait(const long timeout, const bool call_receive = true);
+
+    /// @brief Generic callback for timers under test.
+    ///
+    /// This callback increases the calls count for specified timer name.
+    ///
+    /// @param timer_name Name of the timer for which callback counter should
+    /// be increased.
+    void timerCallback(const std::string& timer_name);
+
+    /// @brief Create a generic callback function for the timer.
+    ///
+    /// This is just a wrapped to make it a bit more convenient
+    /// in the test.
+    boost::function<void ()> makeCallback(const std::string& timer_name);
+
+    /// @brief Callback for timeout.
+    ///
+    /// This callback indicates the test timeout by setting the
+    /// @c timeout_ member.
+    void timeoutCallback();
+
+    /// @brief Type definition for a map holding calls counters for
+    /// timers.
+    typedef std::map<std::string, unsigned int> CallsCount;
+
+    /// @brief Holds the calls count for test timers.
+    ///
+    /// The key of this map holds the timer names. The value holds the number
+    /// of calls to the timer handlers.
+    CallsCount calls_count_;
+
+};
+
+void
+TimerMgrTest::SetUp() {
+    calls_count_.clear();
+    timeout_ = false;
+    // Make sure there are no dangling threads.
+    TimerMgr::instance().stopThread();
+}
+
+void
+TimerMgrTest::TearDown() {
+    // Make sure there are no dangling threads.
+    TimerMgr::instance().stopThread();
+    // Remove all timers.
+    TimerMgr::instance().unregisterTimers();
+}
+
+void
+TimerMgrTest::registerTimer(const std::string& timer_name, const long timer_interval,
+                            const IntervalTimer::Mode& timer_mode) {
+    TimerMgr& timer_mgr = TimerMgr::instance();
+
+    // Register the timer with the generic callback that counts the
+    // number of callback invocations.
+    ASSERT_NO_THROW(
+        timer_mgr.registerTimer(timer_name, makeCallback(timer_name), timer_interval,
+                                timer_mode)
+    );
+
+    calls_count_[timer_name] = 0;
+
+}
+
+void
+TimerMgrTest::doWait(const long timeout, const bool call_receive) {
+    IntervalTimer timeout_timer(io_service_);
+    timeout_timer.setup(boost::bind(&TimerMgrTest::timeoutCallback, this), timeout,
+                        IntervalTimer::ONE_SHOT);
+
+    // The timeout flag will be set by the timeoutCallback if the test
+    // lasts for too long. In this case we will return from here.
+    while (!timeout_) {
+        if (call_receive) {
+            // Block for one 1 millisecond.
+            IfaceMgr::instance().receive6(0, 1000);
+        }
+        // Run ready handlers from the local IO service to execute
+        // the timeout callback if necessary.
+        io_service_.get_io_service().poll_one();
+    }
+
+    timeout_ = false;
+}
+
+void
+TimerMgrTest::timerCallback(const std::string& timer_name) {
+    // Accumulate the number of calls to the timer handler.
+    ++calls_count_[timer_name];
+
+    // The timer installed is the ONE_SHOT timer, so we have
+    // to reschedule the timer.
+    TimerMgr::instance().setup(timer_name);
+}
+
+boost::function<void ()>
+TimerMgrTest::makeCallback(const std::string& timer_name) {
+    return (boost::bind(&TimerMgrTest::timerCallback, this, timer_name));
+}
+
+
+void
+TimerMgrTest::timeoutCallback() {
+    // Timeout occurred. Stop and reset IO service and mark
+    // the timeout flag.
+    io_service_.stop();
+    io_service_.get_io_service().reset();
+    timeout_ = true;
+}
+
+// This test checks that certain errors are returned when invalid
+// parameters are specified when registering a timer, or when
+// the registration can't be made.
+TEST_F(TimerMgrTest, registerTimer) {
+    TimerMgr& timer_mgr = TimerMgr::instance();
+
+    // Empty timer name is not allowed.
+    ASSERT_THROW(timer_mgr.registerTimer("", makeCallback("timer1"), 1,
+                                         IntervalTimer::ONE_SHOT),
+                 BadValue);
+
+    // Add a timer with a correct name.
+    ASSERT_NO_THROW(timer_mgr.registerTimer("timer2", makeCallback("timer2"), 1,
+                                         IntervalTimer::ONE_SHOT));
+    // Adding the timer with the same name as the existing timer is not
+    // allowed.
+    ASSERT_THROW(timer_mgr.registerTimer("timer2", makeCallback("timer2"), 1,
+                                         IntervalTimer::ONE_SHOT),
+                 BadValue);
+
+    // Start worker thread.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+
+    // Can't register the timer when the thread is running.
+    ASSERT_THROW(timer_mgr.registerTimer("timer1", makeCallback("timer1"), 1,
+                                         IntervalTimer::ONE_SHOT),
+                 InvalidOperation);
+
+    // Stop the thread and retry.
+    ASSERT_NO_THROW(timer_mgr.stopThread());
+    EXPECT_NO_THROW(timer_mgr.registerTimer("timer1", makeCallback("timer1"), 1,
+                                            IntervalTimer::ONE_SHOT));
+
+}
+
+// This test verifies that it is possible to unregister a timer from
+// the TimerMgr.
+TEST_F(TimerMgrTest, unregisterTimer) {
+    TimerMgr& timer_mgr = TimerMgr::instance();
+
+    // Register a timer and start it.
+    ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 1));
+    ASSERT_NO_THROW(timer_mgr.setup("timer1"));
+    ASSERT_NO_THROW(timer_mgr.startThread());
+
+    // Wait for the timer to execute several times.
+    doWait(100);
+
+    // Stop the thread but execute pending callbacks.
+    ASSERT_NO_THROW(timer_mgr.stopThread(true));
+
+    // Remember how many times the timer's callback was executed.
+    const unsigned int calls_count = calls_count_["timer1"];
+    ASSERT_GT(calls_count, 0);
+
+    // Check that an attempt to unregister a non-existing timer would
+    // result in exeception.
+    EXPECT_THROW(timer_mgr.unregisterTimer("timer2"), BadValue);
+
+    // Now unregister the correct one.
+    ASSERT_NO_THROW(timer_mgr.unregisterTimer("timer1"));
+
+    // Start the thread again and wait another 100ms.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+    doWait(100);
+    ASSERT_NO_THROW(timer_mgr.stopThread(true));
+
+    // The number of calls for the timer1 shouldn't change as the
+    // timer had been unregistered.
+    EXPECT_EQ(calls_count_["timer1"], calls_count);
+}
+
+// This test verifies taht it is possible to unregister all timers.
+/// @todo This test is disabled because it may occassionally hang
+/// due to bug in the ASIO implementation shipped with Kea.
+/// Replacing it with the ASIO implementation from BOOST does
+/// solve the problem. See ticket #4009. Until this ticket is
+/// implemented, the test should remain disabled.
+TEST_F(TimerMgrTest, DISABLED_unregisterTimers) {
+    TimerMgr& timer_mgr = TimerMgr::instance();
+
+    // Register 10 timers.
+    for (int i = 1; i <= 20; ++i) {
+        std::ostringstream s;
+        s << "timer" << i;
+        ASSERT_NO_FATAL_FAILURE(registerTimer(s.str(), 1))
+            << "fatal failure occurred while registering "
+            << s.str();
+        ASSERT_NO_THROW(timer_mgr.setup(s.str()))
+            << "exception thrown while calling setup() for the "
+            << s.str();
+    }
+
+    // Start worker thread and wait for 500ms.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+    doWait(500);
+    ASSERT_NO_THROW(timer_mgr.stopThread(true));
+
+    // Make sure that all timers have been executed at least once.
+    for (CallsCount::iterator it = calls_count_.begin();
+         it != calls_count_.end(); ++it) {
+        unsigned int calls_count = it->second;
+        ASSERT_GT(calls_count, 0)
+            << "expected calls counter for timer"
+            << (std::distance(calls_count_.begin(), it) + 1)
+            << " greater than 0";
+    }
+
+    // Copy counters for all timers.
+    CallsCount calls_count(calls_count_);
+
+    // Let's unregister all timers.
+    ASSERT_NO_THROW(timer_mgr.unregisterTimers());
+
+    // Start worker thread again and wait for 500ms.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+    doWait(500);
+    ASSERT_NO_THROW(timer_mgr.stopThread(true));
+
+    // The calls counter shouldn't change because there are
+    // no timers registered.
+    EXPECT_TRUE(calls_count == calls_count_);
+}
+
+// This test checks that it is not possible to unregister timers
+// while the thread is running.
+TEST_F(TimerMgrTest, unregisterTimerWhileRunning) {
+    TimerMgr& timer_mgr = TimerMgr::instance();
+
+    // Register two timers.
+    ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 1));
+    ASSERT_NO_FATAL_FAILURE(registerTimer("timer2", 1));
+
+    // Start the thread and make sure we can't unregister them.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+    EXPECT_THROW(timer_mgr.unregisterTimer("timer1"), InvalidOperation);
+    EXPECT_THROW(timer_mgr.unregisterTimers(), InvalidOperation);
+
+    // No need to stop the thread as it will be stopped by the
+    // test fixture destructor.
+}
+
+// This test verifies that the timer execution can be cancelled.
+TEST_F(TimerMgrTest, cancel) {
+    TimerMgr& timer_mgr = TimerMgr::instance();
+
+    // Register timer.
+    ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 1));
+
+    // We can start the worker thread before we even kick in the timers.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+
+    // Kick in the timer and wait for 500ms.
+    ASSERT_NO_THROW(timer_mgr.setup("timer1"));
+    doWait(500);
+
+    // Cancelling non-existing timer should fail.
+    EXPECT_THROW(timer_mgr.cancel("timer2"), BadValue);
+
+    // Cancelling the good one should pass, even when the worker
+    // thread is running.
+    ASSERT_NO_THROW(timer_mgr.cancel("timer1"));
+
+    // Remember how many calls have been invoked and wait for
+    // another 500ms.
+    unsigned int calls_count = calls_count_["timer1"];
+    doWait(500);
+
+    // The number of calls shouldn't change because the timer had been
+    // cancelled.
+    ASSERT_EQ(calls_count, calls_count_["timer1"]);
+
+    // Setup the timer again.
+    ASSERT_NO_THROW(timer_mgr.setup("timer1"));
+    doWait(500);
+
+    // New calls should be recorded.
+    EXPECT_GT(calls_count_["timer1"], calls_count);
+}
+
+// This test verifies that the callbacks for the scheduled timers are
+// actually called.
+TEST_F(TimerMgrTest, scheduleTimers) {
+    TimerMgr& timer_mgr = TimerMgr::instance();
+
+    // Register two timers: 'timer1' and 'timer2'. The first timer will
+    // be executed at the 1ms interval. The second one at the 5ms
+    // interval.
+    ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 1));
+    ASSERT_NO_FATAL_FAILURE(registerTimer("timer2", 5));
+
+    // We can start the worker thread before we even kick in the timers.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+
+    // Kick in the timers. The timers have been registered so there
+    // should be no exception.
+    ASSERT_NO_THROW(timer_mgr.setup("timer1"));
+    ASSERT_NO_THROW(timer_mgr.setup("timer2"));
+
+    // Run IfaceMgr::receive6() in the loop for 500ms. This function
+    // will read data from the watch sockets created when the timers
+    // were registered. The data is delivered to the watch sockets
+    // at the interval of the timers, which should break the blocking
+    // call to receive6(). As a result, the callbacks associated
+    // with the watch sockets should be called.
+    doWait(500);
+
+    // Stop the worker thread, which would halt the execution of
+    // the timers.
+    timer_mgr.stopThread(true);
+
+    // We have been running the timer for 500ms at the interval of
+    // 1 ms. The maximum number of callbacks is 500. However, the
+    // callback itself takes time. Stoping the thread takes time.
+    // So, the real number differs significantly. We don't know
+    // exactly how many have been executed. It should be more
+    // than 10 for sure. But we really made up the numbers here.
+    EXPECT_GT(calls_count_["timer1"], 25);
+    // For the second timer it should be more than 5.
+    EXPECT_GT(calls_count_["timer2"], 5);
+
+    // Because the interval of the 'timer1' is lower than the
+    // interval of the 'timer2' the number of calls should
+    // be higher for the 'timer1'.
+    EXPECT_GT(calls_count_["timer1"], calls_count_["timer2"]);
+
+    // Remember the number of calls from 'timer1' and 'timer2'.
+    unsigned int calls_count_timer1 = calls_count_["timer1"];
+    unsigned int calls_count_timer2 = calls_count_["timer2"];
+
+    // Unregister the 'timer1'.
+    ASSERT_NO_THROW(timer_mgr.unregisterTimer("timer1"));
+
+    // Restart the thread.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+
+    // Wait another 500ms. The 'timer1' was unregistered so it
+    // should not make any more calls. The 'timer2' should still
+    // work as previously.
+    doWait(500);
+
+    // The number of calls shouldn't have changed.
+    EXPECT_EQ(calls_count_timer1, calls_count_["timer1"]);
+    // There should be some new calls registered for the 'timer2'.
+    EXPECT_GT(calls_count_["timer2"], calls_count_timer2);
+}
+
+// This test verifies that it is possible to force that the pending
+// timer callbacks are executed when the worker thread is stopped.
+TEST_F(TimerMgrTest, stopThreadWithRunningHandlers) {
+    TimerMgr& timer_mgr = TimerMgr::instance();
+
+    // Register 'timer1'.
+    ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 1));
+
+    // We can start the worker thread before we even kick in the timers.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+
+    // Kick in the timer.
+    ASSERT_NO_THROW(timer_mgr.setup("timer1"));
+
+    // Run the thread for 100ms. This should run some timers. The 'false'
+    // value indicates that the IfaceMgr::receive6 is not called, so the
+    // watch socket is never cleared.
+    doWait(100, false);
+
+    // There should be no calls registered for the timer1.
+    EXPECT_EQ(0, calls_count_["timer1"]);
+
+    // Stop the worker thread without completing pending callbacks.
+    ASSERT_NO_THROW(timer_mgr.stopThread(false));
+
+    // There should be still not be any calls registered.
+    EXPECT_EQ(0, calls_count_["timer1"]);
+
+    // We can restart the worker thread before we even kick in the timers.
+    ASSERT_NO_THROW(timer_mgr.startThread());
+
+    // Run the thread for 100ms. This should run some timers. The 'false'
+    // value indicates that the IfaceMgr::receive6 is not called, so the
+    // watch socket is never cleared.
+    doWait(100, false);
+
+    // There should be no calls registered for the timer1.
+    EXPECT_EQ(0, calls_count_["timer1"]);
+
+    // Stop the worker thread with completing pending callbacks.
+    ASSERT_NO_THROW(timer_mgr.stopThread(true));
+
+    // There should be one call registered.
+    EXPECT_EQ(1, calls_count_["timer1"]);
+}
+
+} // end of anonymous namespace

+ 300 - 0
src/lib/dhcpsrv/timer_mgr.cc

@@ -0,0 +1,300 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asio.hpp>
+#include <asiolink/io_service.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcpsrv/dhcpsrv_log.h>
+#include <dhcpsrv/timer_mgr.h>
+#include <exceptions/exceptions.h>
+#include <boost/bind.hpp>
+#include <utility>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::util;
+using namespace isc::util::thread;
+
+namespace isc {
+namespace dhcp {
+
+TimerMgr&
+TimerMgr::instance() {
+    static TimerMgr timer_mgr;
+    return (timer_mgr);
+}
+
+TimerMgr::TimerMgr()
+    : io_service_(new IOService()), thread_(),
+      registered_timers_() {
+}
+
+TimerMgr::~TimerMgr() {
+    // Stop the thread, but do not unregister any timers. Unregistering
+    // the timers could cause static deinitialization fiasco between the
+    // TimerMgr and IfaceMgr. By now, the caller should have unregistered
+    // the timers.
+    stopThread();
+}
+
+void
+TimerMgr::registerTimer(const std::string& timer_name,
+                        const IntervalTimer::Callback& callback,
+                        const long interval,
+                        const IntervalTimer::Mode& scheduling_mode) {
+
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+              DHCPSRV_TIMERMGR_REGISTER_TIMER)
+        .arg(timer_name)
+        .arg(interval);
+
+    // Timer name must not be empty.
+    if (timer_name.empty()) {
+        isc_throw(BadValue, "registered timer name must not be empty");
+    }
+
+    // Must not register two timers under the same name.
+    if (registered_timers_.find(timer_name) != registered_timers_.end()) {
+        isc_throw(BadValue, "trying to register duplicate timer '"
+                  << timer_name << "'");
+    }
+
+    // Must not register new timer when the worker thread is running. Note
+    // that worker thread is using IO service and trying to register a new
+    // timer while IO service is being used would result in hang.
+    if (thread_) {
+        isc_throw(InvalidOperation, "unable to register new timer when the"
+                  " timer manager's worker thread is running");
+    }
+
+    // Create a structure holding the configuration for the timer. It will
+    // create the instance if the IntervalTimer and WatchSocket. It will
+    // also hold the callback, interval and scheduling mode parameters.
+    // This may throw a WatchSocketError if the socket creation fails.
+    TimerInfoPtr timer_info(new TimerInfo(getIOService(), callback,
+                                          interval, scheduling_mode));
+
+    // Register the WatchSocket in the IfaceMgr and register our own callback
+    // to be executed when the data is received over this socket. The only time
+    // this may fail is when the socket failed to open which would have caused
+    // an exception in the previous call. So we should be safe here.
+    IfaceMgr::instance().addExternalSocket(timer_info->watch_socket_.getSelectFd(),
+                                           boost::bind(&TimerMgr::ifaceMgrCallback,
+                                                       this, timer_name));
+
+    // Actually register the timer.
+    registered_timers_.insert(std::pair<std::string, TimerInfoPtr>(timer_name,
+                                                                   timer_info));
+}
+
+void
+TimerMgr::unregisterTimer(const std::string& timer_name) {
+
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+              DHCPSRV_TIMERMGR_UNREGISTER_TIMER)
+        .arg(timer_name);
+
+    if (thread_) {
+        isc_throw(InvalidOperation, "unable to unregister timer "
+                  << timer_name << " while worker thread is running");
+    }
+
+    // Find the timer with specified name.
+    TimerInfoMap::iterator timer_info_it = registered_timers_.find(timer_name);
+
+    // Check if the timer has been registered.
+    if (timer_info_it == registered_timers_.end()) {
+        isc_throw(BadValue, "unable to unregister non existing timer '"
+                  << timer_name << "'");
+    }
+
+    // Cancel any pending asynchronous operation and stop the timer.
+    cancel(timer_name);
+
+    const TimerInfoPtr& timer_info = timer_info_it->second;
+
+    // Unregister the watch socket from the IfaceMgr.
+    IfaceMgr::instance().deleteExternalSocket(timer_info->watch_socket_.getSelectFd());
+
+    // Remove the timer.
+    registered_timers_.erase(timer_info_it);
+}
+
+void
+TimerMgr::unregisterTimers() {
+
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+              DHCPSRV_TIMERMGR_UNREGISTER_ALL_TIMERS);
+
+    // Copy the map holding timers configuration. This is required so as
+    // we don't cut the branch which we're sitting on when we will be
+    // erasing the timers. We're going to iterate over the register timers
+    // and remove them with the call to unregisterTimer function. But this
+    // function will remove them from the register_timers_ map. If we
+    // didn't work on the copy here, our iterator would invalidate. The
+    // TimerInfo structure is copyable and since it is using the shared
+    // pointers the copy is not expensive. Also this function is called when
+    // the process terminates so it is not critical for performance.
+    TimerInfoMap registered_timers_copy(registered_timers_);
+
+    // Iterate over the existing timers and unregister them.
+    for (TimerInfoMap::iterator timer_info_it = registered_timers_copy.begin();
+         timer_info_it != registered_timers_copy.end(); ++timer_info_it) {
+        unregisterTimer(timer_info_it->first);
+    }
+}
+
+void
+TimerMgr::setup(const std::string& timer_name) {
+
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+              DHCPSRV_TIMERMGR_START_TIMER)
+        .arg(timer_name);
+
+   // Check if the specified timer exists.
+   TimerInfoMap::const_iterator timer_info_it = registered_timers_.find(timer_name);
+   if (timer_info_it == registered_timers_.end()) {
+       isc_throw(BadValue, "unable to setup timer '" << timer_name << "': "
+                 "no such timer registered");
+   }
+
+   // Schedule the execution of the timer using the parameters supplied
+   // during the registration.
+   const TimerInfoPtr& timer_info = timer_info_it->second;
+   timer_info->interval_timer_.setup(boost::bind(&TimerMgr::timerCallback, this, timer_name),
+                                     timer_info->interval_,
+                                     timer_info->scheduling_mode_);
+}
+
+void
+TimerMgr::cancel(const std::string& timer_name) {
+
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+              DHCPSRV_TIMERMGR_STOP_TIMER)
+        .arg(timer_name);
+
+    // Find the timer of our interest.
+    TimerInfoMap::const_iterator timer_info_it = registered_timers_.find(timer_name);
+    if (timer_info_it == registered_timers_.end()) {
+        isc_throw(BadValue, "unable to cancel timer '" << timer_name << "': "
+                  "no such timer registered");
+    }
+    // Cancel the timer.
+    timer_info_it->second->interval_timer_.cancel();
+    // Clear watch socket, if ready.
+    timer_info_it->second->watch_socket_.clearReady();
+}
+
+void
+TimerMgr::startThread() {
+    // Do not start the thread if the thread is already there.
+    if (!thread_) {
+        // Only log it if we really start the thread.
+        LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+                  DHCPSRV_TIMERMGR_START_THREAD);
+
+        // The thread will simply run IOService::run(), which is a blocking call
+        // to keep running handlers for all timers according to how they have
+        // been scheduled.
+        thread_.reset(new Thread(boost::bind(&IOService::run, &getIOService())));
+    }
+}
+
+void
+TimerMgr::stopThread(const bool run_pending_callbacks) {
+    // If thread is not running, this is no-op.
+    if (thread_) {
+        // Only log it if we really have something to stop.
+        LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+                  DHCPSRV_TIMERMGR_STOP_THREAD);
+
+        // Stop the IO Service. This will break the IOService::run executed in the
+        // worker thread. The thread will now terminate.
+        getIOService().stop();
+        // Some of the watch sockets may be already marked as ready and
+        // have some pending callbacks to be executed. If the caller
+        // wants us to run the callbacks we clear the sockets and run
+        // them. If pending callbacks shouldn't be executed, this will
+        // only clear the sockets (which should be substantially faster).
+        clearReadySockets(run_pending_callbacks);
+        // Wait for the thread to terminate.
+        thread_->wait();
+        // Set the thread pointer to NULL to indicate that the thread is not running.
+        thread_.reset();
+        // IO Service has to be reset so as we can call run() on it again if we
+        // desire (using the startThread()).
+        getIOService().get_io_service().reset();
+    }
+}
+IOService&
+TimerMgr::getIOService() const {
+    return (*io_service_);
+}
+
+void
+TimerMgr::timerCallback(const std::string& timer_name) {
+    // Find the specified timer setup.
+    TimerInfoMap::iterator timer_info_it = registered_timers_.find(timer_name);
+    if (timer_info_it != registered_timers_.end()) {
+        // We will mark watch socket ready - write data to a socket to
+        // interrupt the execution of the select() function. This is
+        // executed from the worker thread.
+        const TimerInfoPtr& timer_info = timer_info_it->second;
+        timer_info->watch_socket_.markReady();
+    }
+}
+
+void
+TimerMgr::ifaceMgrCallback(const std::string& timer_name) {
+    // Find the specified timer setup.
+    TimerInfoMap::iterator timer_info_it = registered_timers_.find(timer_name);
+    if (timer_info_it != registered_timers_.end()) {
+        // We're executing a callback function from the Interface Manager.
+        // This callback function is executed when the call to select() is
+        // interrupted as a result of receiving some data over the watch
+        // socket. We have to clear the socket which has been marked as
+        // ready. Then execute the callback function supplied by the
+        // TimerMgr user to perform custom actions on the expiration of
+        // the given timer.
+        handleReadySocket(timer_info_it, true);
+    }
+}
+
+void
+TimerMgr::clearReadySockets(const bool run_pending_callbacks) {
+    for (TimerInfoMap::iterator timer_info_it = registered_timers_.begin();
+         timer_info_it != registered_timers_.end(); ++timer_info_it) {
+        handleReadySocket(timer_info_it, run_pending_callbacks);
+   }
+}
+
+template<typename Iterator>
+void
+TimerMgr::handleReadySocket(Iterator timer_info_iterator,
+                            const bool run_callback) {
+    timer_info_iterator->second->watch_socket_.clearReady();
+
+    if (run_callback) {
+        // Running user-defined operation for the timer. Logging it
+        // on the slightly lower debug level as there may be many
+        // such traces.
+        LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
+                  DHCPSRV_TIMERMGR_RUN_TIMER_OPERATION)
+            .arg(timer_info_iterator->first);
+        timer_info_iterator->second->user_callback_();
+    }
+}
+
+} // end of namespace isc::dhcp
+} // end of namespace isc

+ 387 - 0
src/lib/dhcpsrv/timer_mgr.h

@@ -0,0 +1,387 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef TIMER_MGR_H
+#define TIMER_MGR_H
+
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <util/threads/thread.h>
+#include <util/watch_socket.h>
+#include <boost/noncopyable.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <map>
+#include <string>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Manages a pool of asynchronous interval timers for DHCP server.
+///
+/// This class holds a pool of asynchronous interval timers which are
+/// capable of interrupting the blocking call to @c select() function in
+/// the main threads of the DHCP servers. The main thread can then
+/// timely execute the callback function associated with the particular
+/// timer.
+///
+/// This class is useful for performing periodic actions at the specified
+/// intervals, e.g. act upon expired leases (leases reclamation) or
+/// return declined leases back to the address pool. Other applications
+/// may be added in the future.
+///
+/// The @c TimerMgr is a singleton, thus its instance is available from
+/// different places in the server code. This is convenient because timers
+/// can be installed by different configuration parsers or they can be
+/// re-scheduled from the callback functions.
+///
+/// The timer is registered using the @c TimerMgr::registerTimer method.
+/// Each registered timer has a unique name. It is not possible to register
+/// multiple timers with the same name. Each registered timer is associated
+/// with the callback function supplied by the caller. This callback function
+/// performs the tasks to be executed periodically according to the timer's
+/// interval.
+///
+/// The registered timer's interval does not begin to elapse until the
+/// @c TimerMgr::setup method is called for it.
+///
+/// The @c TimerMgr uses worker thread to run the timers. The thread is
+/// started and stopped using the @c TimerMgr::startThread and
+/// @c TimerMgr::stopThread respectively. The thread calls the blocking
+/// @c IOService::run. All the registered timers are associated with
+/// this instance of the @c IOService that the thread is running.
+/// When the timer elapses a generic callback function is executed
+/// @c TimerMgr::timerCallback with the parameter giving the name
+/// of the timer for which it has been executed.
+///
+/// Every registered timer is associated with an instance of the
+/// @c util::WatchSocket object. The socket is registered in the
+/// @c IfaceMgr as an "external" socket. When the generic callback
+/// function is invoked for the timer, it obtains the instance of the
+/// @c util::WatchSocket and marks it "ready". This call effectively
+/// writes the data to a socket (pipe) which interrupts the call
+/// to the @c select() function in the main thread. When the
+/// @c IfaceMgr (in the main thread) detects data transmitted over
+/// the external socket it will invoke a callback function
+/// associated with this socket. This is the
+/// @c TimerMgr::ifaceMgrCallback associated with the socket when the
+/// timer is registered. This callback function is executed/ in the
+/// main thread. It clears the socket, which unblocks the worker
+/// thread. It also invokes the user callback function specified
+/// for a given timer.
+///
+/// The @c TimerMgr::timerCallback function searches for the
+/// registered timer for which it has been called. This may cause
+/// race conditions between the worker thread and the main thread
+/// if the latter is modifying the collection of the registered
+/// timers. Therefore, the @c TimerMgr does not allow for
+/// registering or unregistering the timers when the worker thread
+/// is running. The worker thread must be stopped first.
+///
+/// @warning The application (DHCP server) is responsible for
+///  unregistering the timers before it terminates:
+/// @code
+///     TimerMgr::instance().unregisterTimers();
+/// @endcode
+///
+/// to avoid the static deinitialization fiasco between the @c TimerMgr
+/// and @c IfaceMgr. Note that the @c TimerMgr destructor doesn't
+/// unregister the timers to avoid referencing the @c IfaceMgr
+/// instance which may not exist at this point. If the timers are
+/// not unregistered before the application terminates this will
+/// likely result in segmentation fault on some systems.
+///
+class TimerMgr : public boost::noncopyable {
+public:
+
+    /// @brief Returns sole instance of the @c TimerMgr singleton.
+    static TimerMgr& instance();
+
+    /// @name Registering, unregistering and scheduling the timers.
+    //@{
+
+    /// @brief Registers new timers in the @c TimerMgr.
+    ///
+    /// This method must not be called while the worker thread is running,
+    /// as it modifies the internal data structure holding registered
+    /// timers, which is also accessed from the worker thread via the
+    /// callback. Inserting new element to this data structure and
+    /// reading it at the same time would yield undefined behavior.
+    ///
+    /// In order to prevent race conditions between the worker thread and
+    /// this method a mutex could be introduced. However, locking the mutex
+    /// would be required for all callback invocations, which could have
+    /// negative impact on the performance.
+    ///
+    /// @param timer_name Unique name for the timer.
+    /// @param callback Pointer to the callback function to be invoked
+    /// when the timer elapses, e.g. function processing expired leases
+    /// in the DHCP server.
+    /// @param interval Timer interval in milliseconds.
+    /// @param scheduling_mode Scheduling mode of the timer as described in
+    /// @c asiolink::IntervalTimer::Mode.
+    ///
+    /// @throw BadValue if the timer name is invalid or duplicate.
+    /// @throw InvalidOperation if the worker thread is running.
+    void registerTimer(const std::string& timer_name,
+                       const asiolink::IntervalTimer::Callback& callback,
+                       const long interval,
+                       const asiolink::IntervalTimer::Mode& scheduling_mode);
+
+    /// @brief Unregisters specified timer.
+    ///
+    /// This method cancels the timer if it is setup. It removes the external
+    /// socket from the @c IfaceMgr and closes it. It finally removes the
+    /// timer from the internal collection of timers.
+    ///
+    /// This method must not be called while the worker thread is running,
+    /// as it modifies the internal data structure holding registered
+    /// timers, which is also accessed from the worker thread via the
+    /// callback. Removing element from this data structure and
+    /// reading it at the same time would yield undefined behavior.
+    ///
+    /// In order to prevent race conditions between the worker thread and
+    /// this method a mutex could be introduced. However, locking the mutex
+    /// would be required for all callback invocations which could have
+    /// negative impact on the performance.
+    ///
+    /// @param timer_name Name of the timer to be unregistered.
+    ///
+    /// @throw BadValue if the specified timer hasn't been registered.
+    void unregisterTimer(const std::string& timer_name);
+
+    /// @brief Unregisters all timers.
+    ///
+    /// This method must be explicitly called prior to termination of the
+    /// process.
+    void unregisterTimers();
+
+    /// @brief Schedules the execution of the interval timer.
+    ///
+    /// This method schedules the timer, i.e. the callback will be executed
+    /// after specified interval elapses. The interval has been specified
+    /// during timer registration. Depending on the mode selected during the
+    /// timer registration, the callback will be executed once after it has
+    /// been scheduled or until it is cancelled. Though, in the former case
+    /// the timer can be re-scheduled in the callback function.
+    ///
+    /// @param timer_name Unique timer name.
+    ///
+    /// @throw BadValue if the timer hasn't been registered.
+    void setup(const std::string& timer_name);
+
+    /// @brief Cancels the execution of the interval timer.
+    ///
+    /// This method has no effect if the timer hasn't been scheduled with
+    /// the @c TimerMgr::setup method.
+    ///
+    /// @param timer_name Unique timer name.
+    ///
+    /// @throw BadValue if the timer hasn't been registered.
+    void cancel(const std::string& timer_name);
+
+    //@}
+
+    /// @name Starting and stopping the worker thread.
+    //@{
+
+    /// @brief Starts worker thread
+    ///
+    /// This method has no effect if the thread has already been started.
+    void startThread();
+
+    /// @brief Stops worker thread.
+    ///
+    /// When the thread is being stopped, it is possible that some of the
+    /// timers have elapsed and marked their respective watch sockets
+    /// as "ready", but the sockets haven't been yet cleared in the
+    /// main thread and the installed callbacks haven't been executed.
+    /// It is possible to control whether those pending callbacks should
+    /// be executed or not before the call to @c stopThread ends.
+    /// If the thread is being stopped as a result of the DHCP server
+    /// reconfiguration running pending callback may take significant
+    /// amount of time, e.g. when operations on the lease database are
+    /// involved. If this is a concern, the function parameter should
+    /// be left at its default value. In this case, however, it is
+    /// important to note that callbacks installed on ONE_SHOT timers
+    /// often reschedule the timer. If such callback is not executed
+    /// the timer will have to be setup by the application when the
+    /// thread is started again.
+    ///
+    /// Setting the @c run_pending_callbacks to true will guarantee
+    /// that all callbacks for which the timers have already elapsed
+    /// (and marked their watch sockets as ready) will be executed
+    /// prior to the return from @c stopThread method. However, this
+    /// should be avoided if the longer execution time of the
+    /// @c stopThread function is a concern.
+    ///
+    /// This method has no effect if the thread is not running.
+    ///
+    /// @param run_pending_callbacks Indicates if the pending callbacks
+    /// should be executed (if true).
+    void stopThread(const bool run_pending_callbacks = false);
+
+    //@}
+
+    /// @brief Returns a reference to IO service used by the @c TimerMgr.
+    asiolink::IOService& getIOService() const;
+
+private:
+
+    /// @name Constructor and destructor.
+    //@{
+    ///
+    /// @brief Private default constructor.
+    ///
+    /// The @c TimerMgr is a singleton class which instance must be created
+    /// using the @c TimerMgr::instance method. Private constructor enforces
+    /// construction via @c TimerMgr::instance.
+    TimerMgr();
+
+    /// @brief Private destructor.
+    ///
+    /// Stops the worker thread if it is running. It doesn't unregister any
+    /// timers to avoid static deinitialization fiasco with the @c IfaceMgr.
+    ~TimerMgr();
+
+    //@}
+
+    /// @name Internal callbacks.
+    //@{
+    ///
+    /// @brief Callback function to be executed for each interval timer when
+    /// its scheduled interval elapses.
+    ///
+    /// This method marks the @c util::Watch socket associated with the
+    /// timer as ready (writes data to a pipe). This method will block until
+    /// @c TimerMgr::ifaceMgrCallback is executed from the main thread by the
+    /// @c IfaceMgr.
+    ///
+    /// @param timer_name Unique timer name to be passed to the callback.
+    void timerCallback(const std::string& timer_name);
+
+    /// @brief Callback function installed on the @c IfaceMgr and associated
+    /// with the particular timer.
+    ///
+    /// This callback function is executed by the @c IfaceMgr when the data
+    /// over the specific @c util::WatchSocket is received. This method clears
+    /// the socket (reads the data from the pipe) and executes the callback
+    /// supplied when the timer was registered.
+    ///
+    /// @param timer_name Unique timer name.
+    void ifaceMgrCallback(const std::string& timer_name);
+
+    //@}
+
+    /// @name Methods to handle ready sockets.
+    //@{
+    ///
+    /// @brief Clear ready sockets and optionally run callbacks.
+    ///
+    /// This method is called by the @c TimerMgr::stopThread method
+    /// to clear watch sockets which may be marked as ready. It will
+    /// also optionally run callbacks installed for the timers which
+    /// marked sockets as ready.
+    ///
+    /// @param run_pending_callbacks Indicates if the callbacks should
+    /// be executed for the sockets being cleared (if true).
+    void clearReadySockets(const bool run_pending_callbacks);
+
+    /// @brief Clears a socket and optionally runs a callback.
+    ///
+    /// This method clears the ready socket pointed to by the specified
+    /// iterator. If the @c run_callback is set, the callback will
+    /// also be executed.
+    ///
+    /// @param timer_info_iterator Iterator pointing to the timer
+    /// configuration structure.
+    /// @param run_callback Boolean value indicating if the callback
+    /// should be executed for the socket being cleared (if true).
+    ///
+    /// @tparam Iterator Iterator pointing to the timer configuration
+    /// structure.
+    template<typename Iterator>
+    void handleReadySocket(Iterator timer_info_iterator, const bool run_callback);
+
+    //@}
+
+    /// @brief Pointer to the io service.
+    asiolink::IOServicePtr io_service_;
+
+    /// @brief Pointer to the worker thread.
+    ///
+    /// This is initially set to NULL until the thread is started using the
+    /// @c TimerMgr::startThread. The @c TimerMgr::stopThread sets it back
+    /// to NULL.
+    boost::scoped_ptr<util::thread::Thread> thread_;
+
+    /// @brief Structure holding information for a single timer.
+    ///
+    /// This structure holds the instance of the watch socket being used to
+    /// signal that the timer is "ready". It also holds the instance of the
+    /// interval timer and other parameters pertaining to it.
+    struct TimerInfo {
+        /// @brief Instance of the watch socket.
+        util::WatchSocket watch_socket_;
+
+        /// @brief Instance of the interval timer.
+        asiolink::IntervalTimer interval_timer_;
+
+        /// @brief Holds the pointer to the callback supplied when registering
+        /// the timer.
+        asiolink::IntervalTimer::Callback user_callback_;
+
+        /// @brief Interval timer interval supplied during registration.
+        long interval_;
+
+        /// @brief Interval timer scheduling mode supplied during registration.
+        asiolink::IntervalTimer::Mode scheduling_mode_;
+
+        /// @brief Constructor.
+        ///
+        /// @param io_service Reference to the IO service to be used by the
+        /// interval timer created.
+        /// @param user_callback Pointer to the callback function supplied
+        /// during the timer registration.
+        /// @param interval Timer interval in milliseconds.
+        /// @param mode Interval timer scheduling mode.
+        TimerInfo(asiolink::IOService& io_service,
+                  const asiolink::IntervalTimer::Callback& user_callback,
+                  const long interval,
+                  const asiolink::IntervalTimer::Mode& mode)
+            : watch_socket_(),
+              interval_timer_(io_service),
+              user_callback_(user_callback),
+              interval_(interval),
+              scheduling_mode_(mode) { };
+    };
+
+    /// @brief A type definition for the pointer to @c TimerInfo structure.
+    typedef boost::shared_ptr<TimerInfo> TimerInfoPtr;
+
+    /// @brief A type definition for the map holding timers configuration.
+    typedef std::map<std::string, TimerInfoPtr> TimerInfoMap;
+
+    /// @brief Holds mapping of the timer name to the watch socket, timer
+    /// instance and other parameters pertaining to the timer.
+    ///
+    /// Each registered timer has a unique name which is used as a key to
+    /// the map. The timer is associated with an instance of the @c WatchSocket
+    /// which is marked ready when the interval for the particular elapses.
+    TimerInfoMap registered_timers_;
+};
+
+} // end of namespace isc::dhcp
+} // end of namespace isc
+
+#endif // TIMER_MGR_H

+ 1 - 0
src/lib/util/Makefile.am

@@ -25,6 +25,7 @@ libkea_util_la_SOURCES += range_utilities.h
 libkea_util_la_SOURCES += signal_set.cc signal_set.h
 libkea_util_la_SOURCES += stopwatch.cc stopwatch.h
 libkea_util_la_SOURCES += stopwatch_impl.cc stopwatch_impl.h
+libkea_util_la_SOURCES += watch_socket.cc watch_socket.h
 libkea_util_la_SOURCES += encode/base16_from_binary.h
 libkea_util_la_SOURCES += encode/base32hex.h encode/base64.h
 libkea_util_la_SOURCES += encode/base32hex_from_binary.h

+ 2 - 0
src/lib/util/tests/Makefile.am

@@ -50,6 +50,8 @@ run_unittests_SOURCES += time_utilities_unittest.cc
 run_unittests_SOURCES += range_utilities_unittest.cc
 run_unittests_SOURCES += signal_set_unittest.cc
 run_unittests_SOURCES += stopwatch_unittest.cc
+run_unittests_SOURCES += watch_socket_unittests.cc
+
 
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)

+ 49 - 4
src/lib/dhcp_ddns/tests/watch_socket_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2015  Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -12,8 +12,7 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 #include <config.h>
-#include <dhcp_ddns/watch_socket.h>
-#include <test_utils.h>
+#include <util/watch_socket.h>
 
 #include <gtest/gtest.h>
 
@@ -27,10 +26,33 @@
 
 using namespace std;
 using namespace isc;
-using namespace isc::dhcp_ddns;
+using namespace isc::util;
 
 namespace {
 
+/// @brief Returns the result of select() given an fd to check for read status.
+///
+/// @param fd_to_check The file descriptor to test
+///
+/// @return Returns less than one on an error, 0 if the fd is not ready to
+/// read, > 0 if it is ready to read. 
+int selectCheck(int fd_to_check) {
+    fd_set read_fds;
+    int maxfd = 0;
+
+    FD_ZERO(&read_fds);
+
+    // Add this socket to listening set
+    FD_SET(fd_to_check,  &read_fds);
+    maxfd = fd_to_check;
+
+    struct timeval select_timeout;
+    select_timeout.tv_sec = 0;
+    select_timeout.tv_usec = 0;
+
+    return (select(maxfd + 1, &read_fds, NULL, NULL, &select_timeout));
+}
+
 /// @brief Tests the basic functionality of WatchSocket.
 TEST(WatchSocketTest, basics) {
     WatchSocketPtr watch;
@@ -209,4 +231,27 @@ TEST(WatchSocketTest, badReadOnClear) {
     ASSERT_THROW(watch->markReady(), WatchSocketError);
 }
 
+/// @brief Checks if the socket can be explicitly closed.
+TEST(WatchSocketTest, explicitClose) {
+    WatchSocketPtr watch;
+
+    // Create new instance of the socket.
+    ASSERT_NO_THROW(watch.reset(new WatchSocket()));
+    ASSERT_TRUE(watch);
+
+    // Make sure it has been opened by checking that its descriptor
+    // is valid.
+    EXPECT_NE(watch->getSelectFd(), WatchSocket::SOCKET_NOT_VALID);
+
+    // Close the socket.
+    std::string error_string;
+    ASSERT_TRUE(watch->closeSocket(error_string));
+
+    // Make sure that the descriptor is now invalid which indicates
+    // that the socket has been closed.
+    EXPECT_EQ(WatchSocket::SOCKET_NOT_VALID, watch->getSelectFd());
+    // No errors should be reported.
+    EXPECT_TRUE(error_string.empty());
+}
+
 } // end of anonymous namespace

+ 27 - 13
src/lib/dhcp_ddns/watch_socket.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -14,15 +14,17 @@
 
 /// @file watch_socket.cc
 
-#include <dhcp_ddns/dhcp_ddns_log.h>
-#include <dhcp_ddns/watch_socket.h>
+//#include <dhcp_ddns/dhcp_ddns_log.h>
+#include <util/watch_socket.h>
 
 #include <fcntl.h>
 #include <errno.h>
+#include <sstream>
+#include <string.h>
 #include <sys/select.h>
 
 namespace isc {
-namespace dhcp_ddns {
+namespace util {
 
 
 const int WatchSocket::SOCKET_NOT_VALID;
@@ -116,17 +118,17 @@ WatchSocket::clearReady() {
     }
 }
 
-void
-WatchSocket::closeSocket() {
+bool
+WatchSocket::closeSocket(std::string& error_string) {
+    std::ostringstream s;
     // Close the pipe fds.  Technically a close can fail (hugely unlikely)
     // but there's no recovery for it either.  If one does fail we log it
     // and go on. Plus this is called by the destructor and no one likes
     // destructors that throw.
     if (source_ != SOCKET_NOT_VALID) {
         if (close(source_)) {
-            const char* errstr = strerror(errno);
-            LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_WATCH_SOURCE_CLOSE_ERROR)
-                      .arg(errstr);
+            // An error occured.
+            s << "Could not close source: " << strerror(errno);
         }
 
         source_ = SOCKET_NOT_VALID;
@@ -134,13 +136,25 @@ WatchSocket::closeSocket() {
 
     if (sink_ != SOCKET_NOT_VALID) {
         if (close(sink_)) {
-            const char* errstr = strerror(errno);
-            LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_WATCH_SINK_CLOSE_ERROR)
-                      .arg(errstr);
+            // An error occured.
+            if (error_string.empty()) {
+                s << "could not close sink: " << strerror(errno);
+            }
         }
 
         sink_ = SOCKET_NOT_VALID;
     }
+
+    error_string = s.str();
+
+    // If any errors have been reported, return false.
+    return (error_string.empty() ? true : false);
+}
+
+void
+WatchSocket::closeSocket() {
+    std::string error_string;
+    closeSocket(error_string);
 }
 
 int
@@ -148,5 +162,5 @@ WatchSocket::getSelectFd() {
     return (sink_);
 }
 
-} // namespace isc::dhcp_ddns
+} // namespace isc::util
 } // namespace isc

+ 20 - 7
src/lib/dhcp_ddns/watch_socket.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -18,13 +18,14 @@
 /// @file watch_socket.h Defines the class, WatchSocket.
 
 #include <exceptions/exceptions.h>
-
+#include <boost/noncopyable.hpp>
 #include <boost/shared_ptr.hpp>
 
 #include <stdint.h>
+#include <string>
 
 namespace isc {
-namespace dhcp_ddns {
+namespace util {
 
 /// @brief Exception thrown if an error occurs during IO source open.
 class WatchSocketError : public isc::Exception {
@@ -51,7 +52,7 @@ public:
 /// such as close, read, or altering behavior flags with fcntl or ioctl can have
 /// unpredictable results.  It is intended strictly use with functions such as select()
 /// poll() or their variants.
-class WatchSocket {
+class WatchSocket : public boost::noncopyable {
 public:
     /// @brief Value used to signify an invalid descriptor.
     static const int SOCKET_NOT_VALID = -1;
@@ -114,11 +115,23 @@ public:
     /// pipe.
     int getSelectFd();
 
+    /// @brief Closes the descriptors associated with the socket.
+    ///
+    /// This method is used to close the socket and capture errors that
+    /// may occur during this operation.
+    ///
+    /// @param [out] error_string Holds the error string if closing
+    /// the socket failed. It will hold empty string otherwise.
+    ///
+    /// @return true if the operation was successful, false otherwise.
+    bool closeSocket(std::string& error_string);
+
 private:
+
     /// @brief Closes the descriptors associated with the socket.
     ///
-    /// Used internally in the destructor and if an error occurs marking or
-    /// clearing the socket.
+    /// This method is called by the class destructor and it ignores
+    /// any errors that may occur while closing the sockets.
     void closeSocket();
 
     /// @brief The end of the pipe to which the marker is written
@@ -132,7 +145,7 @@ private:
 /// @brief Defines a smart pointer to an instance of a WatchSocket.
 typedef boost::shared_ptr<WatchSocket> WatchSocketPtr;
 
-} // namespace isc::dhcp_ddns
+} // namespace isc::util
 } // namespace isc
 
 #endif