Browse Source

[master] Merge branch 'trac3971'

Marcin Siodelski 9 years ago
parent
commit
431d515fc3

+ 50 - 15
src/bin/dhcp4/ctrl_dhcp4_srv.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
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
 // purpose with or without fee is hereby granted, provided that the above
@@ -123,8 +123,32 @@ ControlledDhcpv4Srv::processConfig(isc::data::ConstElementPtr config) {
         return (isc::config::createAnswer(1, err.str()));
         return (isc::config::createAnswer(1, err.str()));
     }
     }
 
 
+    // We're going to modify the timers configuration. This is not allowed
+    // when the thread is running.
+    try {
+        TimerMgr::instance()->stopThread();
+    } catch (const std::exception& ex) {
+        err << "Unable to stop worker thread running timers: "
+            << ex.what() << ".";
+        return (isc::config::createAnswer(1, err.str()));
+    }
+
     ConstElementPtr answer = configureDhcp4Server(*srv, config);
     ConstElementPtr answer = configureDhcp4Server(*srv, config);
 
 
+    // Start worker thread if there are any timers installed. Note that
+    // we also start worker thread when the reconfiguration failed, because
+    // in that case we continue using an old configuration and the server
+    // should still run installed timers.
+    if (TimerMgr::instance()->timersCount() > 0) {
+        try {
+            TimerMgr::instance()->startThread();
+        } catch (const std::exception& ex) {
+            err << "Unable to start worker thread running timers: "
+                << ex.what() << ".";
+            return (isc::config::createAnswer(1, err.str()));
+        }
+    }
+
     // Check that configuration was successful. If not, do not reopen sockets
     // Check that configuration was successful. If not, do not reopen sockets
     // and don't bother with DDNS stuff.
     // and don't bother with DDNS stuff.
     try {
     try {
@@ -157,11 +181,12 @@ ControlledDhcpv4Srv::processConfig(isc::data::ConstElementPtr config) {
     CfgMgr::instance().getStagingCfg()->getCfgIface()->
     CfgMgr::instance().getStagingCfg()->getCfgIface()->
         openSockets(AF_INET, srv->getPort(), getInstance()->useBroadcast());
         openSockets(AF_INET, srv->getPort(), getInstance()->useBroadcast());
 
 
+
     return (answer);
     return (answer);
 }
 }
 
 
 ControlledDhcpv4Srv::ControlledDhcpv4Srv(uint16_t port /*= DHCP4_SERVER_PORT*/)
 ControlledDhcpv4Srv::ControlledDhcpv4Srv(uint16_t port /*= DHCP4_SERVER_PORT*/)
-    :Dhcpv4Srv(port) {
+    : Dhcpv4Srv(port), io_service_(), timer_mgr_(TimerMgr::instance()) {
     if (getInstance()) {
     if (getInstance()) {
         isc_throw(InvalidOperation,
         isc_throw(InvalidOperation,
                   "There is another Dhcpv4Srv instance already.");
                   "There is another Dhcpv4Srv instance already.");
@@ -201,19 +226,29 @@ void ControlledDhcpv4Srv::shutdown() {
 }
 }
 
 
 ControlledDhcpv4Srv::~ControlledDhcpv4Srv() {
 ControlledDhcpv4Srv::~ControlledDhcpv4Srv() {
-    cleanup();
-
-    // Close the command socket (if it exists).
-    CommandMgr::instance().closeCommandSocket();
-
-    // Deregister any registered commands
-    CommandMgr::instance().deregisterCommand("shutdown");
-    CommandMgr::instance().deregisterCommand("statistic-get");
-    CommandMgr::instance().deregisterCommand("statistic-reset");
-    CommandMgr::instance().deregisterCommand("statistic-remove");
-    CommandMgr::instance().deregisterCommand("statistic-get-all");
-    CommandMgr::instance().deregisterCommand("statistic-reset-all");
-    CommandMgr::instance().deregisterCommand("statistic-remove-all");
+    try {
+        cleanup();
+
+        // Stop worker thread running timers, if it is running.
+        timer_mgr_->stopThread();
+
+        // Close the command socket (if it exists).
+        CommandMgr::instance().closeCommandSocket();
+
+        // Deregister any registered commands
+        CommandMgr::instance().deregisterCommand("shutdown");
+        CommandMgr::instance().deregisterCommand("statistic-get");
+        CommandMgr::instance().deregisterCommand("statistic-reset");
+        CommandMgr::instance().deregisterCommand("statistic-remove");
+        CommandMgr::instance().deregisterCommand("statistic-get-all");
+        CommandMgr::instance().deregisterCommand("statistic-reset-all");
+        CommandMgr::instance().deregisterCommand("statistic-remove-all");
+
+    } catch (...) {
+        // Don't want to throw exceptions from the destructor. The server
+        // is shutting down anyway.
+        ;
+    }
 
 
     server_ = NULL; // forget this instance. Noone should call any handlers at
     server_ = NULL; // forget this instance. Noone should call any handlers at
                     // this stage.
                     // this stage.

+ 17 - 10
src/bin/dhcp4/ctrl_dhcp4_srv.h

@@ -18,6 +18,7 @@
 #include <asiolink/asiolink.h>
 #include <asiolink/asiolink.h>
 #include <cc/data.h>
 #include <cc/data.h>
 #include <cc/command_interpreter.h>
 #include <cc/command_interpreter.h>
+#include <dhcpsrv/timer_mgr.h>
 #include <dhcp4/dhcp4_srv.h>
 #include <dhcp4/dhcp4_srv.h>
 
 
 namespace isc {
 namespace isc {
@@ -102,13 +103,7 @@ public:
     }
     }
 
 
 
 
-protected:
-    /// @brief Static pointer to the sole instance of the DHCP server.
-    ///
-    /// This is required for config and command handlers to gain access to
-    /// the server
-    static ControlledDhcpv4Srv* server_;
-
+private:
     /// @brief Callback that will be called from iface_mgr when data
     /// @brief Callback that will be called from iface_mgr when data
     /// is received over control socket.
     /// is received over control socket.
     ///
     ///
@@ -117,9 +112,6 @@ protected:
     /// (that was sent from some yet unspecified sender).
     /// (that was sent from some yet unspecified sender).
     static void sessionReader(void);
     static void sessionReader(void);
 
 
-    /// @brief IOService object, used for all ASIO operations.
-    isc::asiolink::IOService io_service_;
-
     /// @brief Handler for processing 'shutdown' command
     /// @brief Handler for processing 'shutdown' command
     ///
     ///
     /// This handler processes shutdown command, which initializes shutdown
     /// This handler processes shutdown command, which initializes shutdown
@@ -157,6 +149,21 @@ protected:
     isc::data::ConstElementPtr
     isc::data::ConstElementPtr
     commandConfigReloadHandler(const std::string& command,
     commandConfigReloadHandler(const std::string& command,
                                isc::data::ConstElementPtr args);
                                isc::data::ConstElementPtr args);
+
+    /// @brief Static pointer to the sole instance of the DHCP server.
+    ///
+    /// This is required for config and command handlers to gain access to
+    /// the server
+    static ControlledDhcpv4Srv* server_;
+
+    /// @brief IOService object, used for all ASIO operations.
+    isc::asiolink::IOService io_service_;
+
+    /// @brief Instance of the @c TimerMgr.
+    ///
+    /// Shared pointer to the instance of timer @c TimerMgr is held here to
+    /// make sure that the @c TimerMgr outlives instance of this class.
+    TimerMgrPtr timer_mgr_;
 };
 };
 
 
 }; // namespace isc::dhcp
 }; // namespace isc::dhcp

+ 0 - 7
src/bin/dhcp4/dhcp4_messages.mes

@@ -312,13 +312,6 @@ be sent to the client in the DHCPACK message. The first argument
 contains the client and the transaction identification information. The
 contains the client and the transaction identification information. The
 second argument contains the allocated IPv4 address.
 second argument contains the allocated IPv4 address.
 
 
-% DHCP4_LEASE_DATABASE_TIMERS_EXEC_FAIL failed to execute timer-based functions for lease database: %1
-A warning message executed when a server process is unable to execute
-the periodic actions for the lease database. An example of the periodic
-action is a Lease File Cleanup. One of the reasons for the failure is
-a misconfiguration of the lease database, whereby the lease database
-hasn't been selected.
-
 % DHCP4_NAME_GEN_UPDATE_FAIL %1: failed to update the lease after generating name %2 for a client: %3
 % DHCP4_NAME_GEN_UPDATE_FAIL %1: failed to update the lease after generating name %2 for a client: %3
 This message indicates the failure when trying to update the lease and/or
 This message indicates the failure when trying to update the lease and/or
 options in the server's response with the hostname generated by the server
 options in the server's response with the hostname generated by the server

+ 1 - 19
src/bin/dhcp4/dhcp4_srv.cc

@@ -365,16 +365,7 @@ Dhcpv4Srv::run() {
         Pkt4Ptr rsp;
         Pkt4Ptr rsp;
 
 
         try {
         try {
-            // The lease database backend may install some timers for which
-            // the handlers need to be executed periodically. Retrieve the
-            // maximum interval at which the handlers must be executed from
-            // the lease manager.
-            uint32_t timeout = LeaseMgrFactory::instance().getIOServiceExecInterval();
-            // If the returned value is zero it means that there are no
-            // timers installed, so use a default value.
-            if (timeout == 0) {
-                timeout = 1000;
-            }
+            uint32_t timeout = 1000;
             LOG_DEBUG(packet4_logger, DBG_DHCP4_DETAIL, DHCP4_BUFFER_WAIT).arg(timeout);
             LOG_DEBUG(packet4_logger, DBG_DHCP4_DETAIL, DHCP4_BUFFER_WAIT).arg(timeout);
             query = receivePacket(timeout);
             query = receivePacket(timeout);
 
 
@@ -429,15 +420,6 @@ Dhcpv4Srv::run() {
                 .arg(e.what());
                 .arg(e.what());
         }
         }
 
 
-        // Execute ready timers for the lease database, e.g. Lease File Cleanup.
-        try {
-            LeaseMgrFactory::instance().getIOService()->poll();
-
-        } catch (const std::exception& ex) {
-            LOG_WARN(dhcp4_logger, DHCP4_LEASE_DATABASE_TIMERS_EXEC_FAIL)
-                .arg(ex.what());
-        }
-
         // Timeout may be reached or signal received, which breaks select()
         // Timeout may be reached or signal received, which breaks select()
         // with no reception occurred. No need to log anything here because
         // with no reception occurred. No need to log anything here because
         // we have logged right after the call to receivePacket().
         // we have logged right after the call to receivePacket().

+ 4 - 0
src/bin/dhcp4/json_config_parser.cc

@@ -27,6 +27,7 @@
 #include <dhcpsrv/parsers/host_reservation_parser.h>
 #include <dhcpsrv/parsers/host_reservation_parser.h>
 #include <dhcpsrv/parsers/host_reservations_list_parser.h>
 #include <dhcpsrv/parsers/host_reservations_list_parser.h>
 #include <dhcpsrv/parsers/ifaces_config_parser.h>
 #include <dhcpsrv/parsers/ifaces_config_parser.h>
+#include <dhcpsrv/timer_mgr.h>
 #include <config/command_mgr.h>
 #include <config/command_mgr.h>
 #include <util/encode/hex.h>
 #include <util/encode/hex.h>
 #include <util/strutil.h>
 #include <util/strutil.h>
@@ -461,6 +462,9 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
     // so newly recreated configuration starts with first subnet-id equal 1.
     // so newly recreated configuration starts with first subnet-id equal 1.
     Subnet::resetSubnetID();
     Subnet::resetSubnetID();
 
 
+    // Remove any existing timers.
+    TimerMgr::instance()->unregisterTimers();
+
     // Some of the values specified in the configuration depend on
     // Some of the values specified in the configuration depend on
     // other values. Typically, the values in the subnet4 structure
     // other values. Typically, the values in the subnet4 structure
     // depend on the global values. Also, option values configuration
     // depend on the global values. Also, option values configuration

+ 107 - 1
src/bin/dhcp4/tests/dhcp4_process_tests.sh.in

@@ -16,6 +16,10 @@
 CFG_FILE=@abs_top_builddir@/src/bin/dhcp4/tests/test_config.json
 CFG_FILE=@abs_top_builddir@/src/bin/dhcp4/tests/test_config.json
 # Path to the Kea log file.
 # Path to the Kea log file.
 LOG_FILE=@abs_top_builddir@/src/bin/dhcp4/tests/test.log
 LOG_FILE=@abs_top_builddir@/src/bin/dhcp4/tests/test.log
+# Path to the Kea lease file.
+LEASE_FILE=@abs_top_builddir@/src/bin/dhcp4/tests/test_leases.csv
+# Path to the Kea LFC application
+export KEA_LFC_EXECUTABLE=@abs_top_builddir@/src/bin/lfc/kea-lfc
 # Expected version
 # Expected version
 EXPECTED_VERSION="@PACKAGE_VERSION@"
 EXPECTED_VERSION="@PACKAGE_VERSION@"
 # Kea configuration to be stored in the configuration file.
 # Kea configuration to be stored in the configuration file.
@@ -31,7 +35,9 @@ CONFIG="{
         \"lease-database\":
         \"lease-database\":
         {
         {
             \"type\": \"memfile\",
             \"type\": \"memfile\",
-            \"persist\": false
+            \"name\": \"$LEASE_FILE\",
+            \"persist\": false,
+            \"lfc-interval\": 0
         },
         },
         \"subnet4\": [
         \"subnet4\": [
         {
         {
@@ -276,9 +282,109 @@ shutdown_test() {
     test_finish 0
     test_finish 0
 }
 }
 
 
+# This test verifies that DHCPv4 can be configured to run lease file cleanup
+# periodially.
+lfc_timer_test() {
+    # Log the start of the test and print test name.
+    test_start "dhcpv4_srv.lfc_timer_test"
+    # Remove dangling Kea instances and remove log files.
+    cleanup
+    # Create a configuration with the LFC enabled, by replacing the section
+    # with the lfc-interval and persist parameters.
+    LFC_CONFIG=$(printf "${CONFIG}" | sed -e 's/\"lfc-interval\": 0/\"lfc-interval\": 1/g' \
+                        | sed -e 's/\"persist\": false/\"persist\": true/g')
+    # Create new configuration file.
+    create_config "${LFC_CONFIG}"
+    # Instruct Kea to log to the specific file.
+    set_logger
+    # Start Kea.
+    start_kea ${bin_path}/${bin}
+    # Wait up to 20s for Kea to start.
+    wait_for_kea 20
+    if [ ${_WAIT_FOR_KEA} -eq 0 ]; then
+        printf "ERROR: timeout waiting for Kea to start.\n"
+        clean_exit 1
+    fi
+
+    # Check if it is still running. It could have terminated (e.g. as a result
+    # of configuration failure).
+    get_pids ${bin}
+    if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+        printf "ERROR: expected one Kea process to be started. Found %d processes\
+ started.\n" ${_GET_PIDS_NUM}
+        clean_exit 1
+    fi
+
+    # Check if Kea emits the log message indicating that LFC is started.
+    wait_for_message 10 "DHCPSRV_MEMFILE_LFC_EXECUTE" 1
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: Server did not execute LFC.\n"
+        clean_exit 1
+    fi
+
+    # Give it a short time to run.
+    sleep 1
+
+    # Modify the interval.
+    LFC_CONFIG=$(printf "${CONFIG}" | sed -e 's/\"lfc-interval\": 1/\"lfc-interval\": 2/g')
+    # Create new configuration file.
+    create_config "${LFC_CONFIG}"
+
+    # Reconfigure the server with SIGHUP.
+    send_signal 1 ${bin}
+
+    # There should be two occurrences of the DHCP4_CONFIG_COMPLETE messages.
+    # Wait for it up to 10s.
+    wait_for_message 10 "DHCP4_CONFIG_COMPLETE" 2
+
+    # After receiving SIGHUP the server should get reconfigured and the
+    # reconfiguration should be noted in the log file. We should now
+    # have two configurations logged in the log file.
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: server hasn't been reconfigured.\n"
+        clean_exit 1
+    else
+        printf "Server successfully reconfigured.\n"
+    fi
+
+    # Make sure the server is still operational.
+    get_pids ${bin}
+    if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+        printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
+        clean_exit 1
+    fi
+
+    # Wait for the LFC to run the second time.
+    wait_for_message 10 "DHCPSRV_MEMFILE_LFC_EXECUTE" 2
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: Server did not execute LFC.\n"
+        clean_exit 1
+    fi
+
+    # Send signal to Kea SIGTERM
+    send_signal 15 ${bin}
+
+    # Wait up to 10s for the server's graceful shutdown. The graceful shut down
+    # should be recorded in the log file with the appropriate message.
+    wait_for_message 10 "DHCP4_SHUTDOWN" 1
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: Server did not record shutdown in the log.\n"
+        clean_exit 1
+    fi
+
+    # Make sure the server is down.
+    wait_for_server_down 5 ${bin}
+    assert_eq 1 ${_WAIT_FOR_SERVER_DOWN} \
+        "Expected wait_for_server_down return %d, returned %d"
+
+    # All ok. Shut down Kea and exit.
+    test_finish 0
+}
+
 server_pid_file_test "${CONFIG}" DHCP4_ALREADY_RUNNING
 server_pid_file_test "${CONFIG}" DHCP4_ALREADY_RUNNING
 dynamic_reconfiguration_test
 dynamic_reconfiguration_test
 shutdown_test "dhcpv4.sigterm_test" 15
 shutdown_test "dhcpv4.sigterm_test" 15
 shutdown_test "dhcpv4.sigint_test" 2
 shutdown_test "dhcpv4.sigint_test" 2
 version_test "dhcpv4.version"
 version_test "dhcpv4.version"
 logger_vars_test "dhcpv4.variables"
 logger_vars_test "dhcpv4.variables"
+lfc_timer_test

+ 50 - 14
src/bin/dhcp6/ctrl_dhcp6_srv.cc

@@ -118,8 +118,34 @@ ControlledDhcpv6Srv::processConfig(isc::data::ConstElementPtr config) {
         return (no_srv);
         return (no_srv);
     }
     }
 
 
+    // We're going to modify the timers configuration. This is not allowed
+    // when the thread is running.
+    try {
+        TimerMgr::instance()->stopThread();
+    } catch (const std::exception& ex) {
+        std::ostringstream err;
+        err << "Unable to stop worker thread running timers: "
+            << ex.what() << ".";
+        return (isc::config::createAnswer(1, err.str()));
+    }
+
     ConstElementPtr answer = configureDhcp6Server(*srv, config);
     ConstElementPtr answer = configureDhcp6Server(*srv, config);
 
 
+    // Start worker thread if there are any timers installed. Note that
+    // we also start worker thread when the reconfiguration failed, because
+    // in that case we continue using an old configuration and the server
+    // should still run installed timers.
+    if (TimerMgr::instance()->timersCount() > 0) {
+        try {
+            TimerMgr::instance()->startThread();
+        } catch (const std::exception& ex) {
+            std::ostringstream err;
+            err << "Unable to start worker thread running timers: "
+                << ex.what() << ".";
+            return (isc::config::createAnswer(1, err.str()));
+        }
+    }
+
     // Check that configuration was successful. If not, do not reopen sockets
     // Check that configuration was successful. If not, do not reopen sockets
     // and don't bother with DDNS stuff.
     // and don't bother with DDNS stuff.
     try {
     try {
@@ -156,7 +182,7 @@ ControlledDhcpv6Srv::processConfig(isc::data::ConstElementPtr config) {
 }
 }
 
 
 ControlledDhcpv6Srv::ControlledDhcpv6Srv(uint16_t port)
 ControlledDhcpv6Srv::ControlledDhcpv6Srv(uint16_t port)
-    : Dhcpv6Srv(port) {
+    : Dhcpv6Srv(port), io_service_(), timer_mgr_(TimerMgr::instance()) {
     if (server_) {
     if (server_) {
         isc_throw(InvalidOperation,
         isc_throw(InvalidOperation,
                   "There is another Dhcpv6Srv instance already.");
                   "There is another Dhcpv6Srv instance already.");
@@ -196,19 +222,29 @@ void ControlledDhcpv6Srv::shutdown() {
 }
 }
 
 
 ControlledDhcpv6Srv::~ControlledDhcpv6Srv() {
 ControlledDhcpv6Srv::~ControlledDhcpv6Srv() {
-    cleanup();
-
-   // Close the command socket (if it exists).
-    CommandMgr::instance().closeCommandSocket();
-
-    // Deregister any registered commands
-    CommandMgr::instance().deregisterCommand("shutdown");
-    CommandMgr::instance().deregisterCommand("statistic-get");
-    CommandMgr::instance().deregisterCommand("statistic-reset");
-    CommandMgr::instance().deregisterCommand("statistic-remove");
-    CommandMgr::instance().deregisterCommand("statistic-get-all");
-    CommandMgr::instance().deregisterCommand("statistic-reset-all");
-    CommandMgr::instance().deregisterCommand("statistic-remove-all");
+    try {
+        cleanup();
+
+        // Stop worker thread running timers, if it is running.
+        timer_mgr_->stopThread();
+
+        // Close the command socket (if it exists).
+        CommandMgr::instance().closeCommandSocket();
+
+        // Deregister any registered commands
+        CommandMgr::instance().deregisterCommand("shutdown");
+        CommandMgr::instance().deregisterCommand("statistic-get");
+        CommandMgr::instance().deregisterCommand("statistic-reset");
+        CommandMgr::instance().deregisterCommand("statistic-remove");
+        CommandMgr::instance().deregisterCommand("statistic-get-all");
+        CommandMgr::instance().deregisterCommand("statistic-reset-all");
+        CommandMgr::instance().deregisterCommand("statistic-remove-all");
+
+    } catch (...) {
+        // Don't want to throw exceptions from the destructor. The server
+        // is shutting down anyway.
+        ;
+    }
 
 
     server_ = NULL; // forget this instance. There should be no callback anymore
     server_ = NULL; // forget this instance. There should be no callback anymore
                     // at this stage anyway.
                     // at this stage anyway.

+ 18 - 9
src/bin/dhcp6/ctrl_dhcp6_srv.h

@@ -18,6 +18,7 @@
 #include <asiolink/asiolink.h>
 #include <asiolink/asiolink.h>
 #include <cc/data.h>
 #include <cc/data.h>
 #include <cc/command_interpreter.h>
 #include <cc/command_interpreter.h>
+#include <dhcpsrv/timer_mgr.h>
 #include <dhcp6/dhcp6_srv.h>
 #include <dhcp6/dhcp6_srv.h>
 
 
 namespace isc {
 namespace isc {
@@ -101,12 +102,7 @@ public:
         return (server_);
         return (server_);
     }
     }
 
 
-protected:
-    /// @brief Static pointer to the sole instance of the DHCP server.
-    ///
-    /// This is required for config and command handlers to gain access to
-    /// the server. Some of them need to be static methods.
-    static ControlledDhcpv6Srv* server_;
+private:
 
 
     /// @brief Callback that will be called from iface_mgr when data
     /// @brief Callback that will be called from iface_mgr when data
     /// is received over control socket.
     /// is received over control socket.
@@ -116,9 +112,6 @@ protected:
     /// (that was sent from some yet unspecified sender).
     /// (that was sent from some yet unspecified sender).
     static void sessionReader(void);
     static void sessionReader(void);
 
 
-    /// @brief IOService object, used for all ASIO operations.
-    isc::asiolink::IOService io_service_;
-
     /// @brief handler for processing 'shutdown' command
     /// @brief handler for processing 'shutdown' command
     ///
     ///
     /// This handler processes shutdown command, which initializes shutdown
     /// This handler processes shutdown command, which initializes shutdown
@@ -156,6 +149,22 @@ protected:
     isc::data::ConstElementPtr
     isc::data::ConstElementPtr
     commandConfigReloadHandler(const std::string& command,
     commandConfigReloadHandler(const std::string& command,
                                isc::data::ConstElementPtr args);
                                isc::data::ConstElementPtr args);
+
+    /// @brief Static pointer to the sole instance of the DHCP server.
+    ///
+    /// This is required for config and command handlers to gain access to
+    /// the server. Some of them need to be static methods.
+    static ControlledDhcpv6Srv* server_;
+
+    /// @brief IOService object, used for all ASIO operations.
+    isc::asiolink::IOService io_service_;
+
+    /// @brief Instance of the @c TimerMgr.
+    ///
+    /// Shared pointer to the instance of timer @c TimerMgr is held here to
+    /// make sure that the @c TimerMgr outlives instance of this class.
+    TimerMgrPtr timer_mgr_;
+
 };
 };
 
 
 }; // namespace isc::dhcp
 }; // namespace isc::dhcp

+ 0 - 7
src/bin/dhcp6/dhcp6_messages.mes

@@ -376,13 +376,6 @@ The first argument holds the client and the transaction identification
 information. The second argument holds the IAID. The third argument
 information. The second argument holds the IAID. The third argument
 holds the detailed lease information.
 holds the detailed lease information.
 
 
-% DHCP6_LEASE_DATABASE_TIMERS_EXEC_FAIL failed to execute timer-based functions for lease database: %1
-A warning message executed when a server process is unable to execute
-the periodic actions for the lease database. An example of the periodic
-action is a Lease File Cleanup. One of the reasons for the failure is
-a misconfiguration of the lease database, whereby the lease database
-hasn't been selected.
-
 % DHCP6_LEASE_NA_WITHOUT_DUID %1: address lease for address %2 does not have a DUID
 % DHCP6_LEASE_NA_WITHOUT_DUID %1: address lease for address %2 does not have a DUID
 This error message indicates a database consistency problem. The lease
 This error message indicates a database consistency problem. The lease
 database has an entry indicating that the given address is in use,
 database has an entry indicating that the given address is in use,

+ 1 - 19
src/bin/dhcp6/dhcp6_srv.cc

@@ -317,16 +317,7 @@ bool Dhcpv6Srv::run() {
         Pkt6Ptr rsp;
         Pkt6Ptr rsp;
 
 
         try {
         try {
-            // The lease database backend may install some timers for which
-            // the handlers need to be executed periodically. Retrieve the
-            // maximum interval at which the handlers must be executed from
-            // the lease manager.
-            uint32_t timeout = LeaseMgrFactory::instance().getIOServiceExecInterval();
-            // If the returned value is zero it means that there are no
-            // timers installed, so use a default value.
-            if (timeout == 0) {
-                timeout = 1000;
-            }
+            uint32_t timeout = 1000;
             LOG_DEBUG(packet6_logger, DBG_DHCP6_DETAIL, DHCP6_BUFFER_WAIT).arg(timeout);
             LOG_DEBUG(packet6_logger, DBG_DHCP6_DETAIL, DHCP6_BUFFER_WAIT).arg(timeout);
             query = receivePacket(timeout);
             query = receivePacket(timeout);
 
 
@@ -385,15 +376,6 @@ bool Dhcpv6Srv::run() {
                 .arg(e.what());
                 .arg(e.what());
         }
         }
 
 
-        // Execute ready timers for the lease database, e.g. Lease File Cleanup.
-        try {
-            LeaseMgrFactory::instance().getIOService()->poll();
-
-        } catch (const std::exception& ex) {
-            LOG_WARN(dhcp6_logger, DHCP6_LEASE_DATABASE_TIMERS_EXEC_FAIL)
-                .arg(ex.what());
-        }
-
         // Timeout may be reached or signal received, which breaks select()
         // Timeout may be reached or signal received, which breaks select()
         // with no packet received
         // with no packet received
         if (!query) {
         if (!query) {

+ 106 - 1
src/bin/dhcp6/tests/dhcp6_process_tests.sh.in

@@ -16,6 +16,8 @@
 CFG_FILE=@abs_top_builddir@/src/bin/dhcp6/tests/test_config.json
 CFG_FILE=@abs_top_builddir@/src/bin/dhcp6/tests/test_config.json
 # Path to the Kea log file.
 # Path to the Kea log file.
 LOG_FILE=@abs_top_builddir@/src/bin/dhcp6/tests/test.log
 LOG_FILE=@abs_top_builddir@/src/bin/dhcp6/tests/test.log
+# Path to the Kea lease file.
+LEASE_FILE=@abs_top_builddir@/src/bin/dhcp6/tests/test_leases.csv
 # Expected version
 # Expected version
 EXPECTED_VERSION="@PACKAGE_VERSION@"
 EXPECTED_VERSION="@PACKAGE_VERSION@"
 # Kea configuration to be stored in the configuration file.
 # Kea configuration to be stored in the configuration file.
@@ -31,7 +33,9 @@ CONFIG="{
         \"lease-database\":
         \"lease-database\":
         {
         {
             \"type\": \"memfile\",
             \"type\": \"memfile\",
-            \"persist\": false
+            \"name\": \"$LEASE_FILE\",
+            \"persist\": false,
+            \"lfc-interval\": 0
         },
         },
         \"subnet6\": [
         \"subnet6\": [
         {
         {
@@ -277,9 +281,110 @@ returned %d."
     test_finish 0
     test_finish 0
 }
 }
 
 
+# This test verifies that DHCPv6 can be configured to run lease file cleanup
+# periodially.
+lfc_timer_test() {
+    # Log the start of the test and print test name.
+    test_start "dhcpv6_srv.lfc_timer_test"
+    # Remove dangling Kea instances and remove log files.
+    cleanup
+    # Create a configuration with the LFC enabled, by replacing the section
+    # with the lfc-interval and persist parameters.
+    LFC_CONFIG=$(printf "${CONFIG}" | sed -e 's/\"lfc-interval\": 0/\"lfc-interval\": 1/g' \
+                        | sed -e 's/\"persist\": false/\"persist\": true/g')
+    # Create new configuration file.
+    create_config "${LFC_CONFIG}"
+    # Instruct Kea to log to the specific file.
+    set_logger
+    # Start Kea.
+    start_kea ${bin_path}/${bin}
+    # Wait up to 20s for Kea to start.
+    wait_for_kea 20
+    if [ ${_WAIT_FOR_KEA} -eq 0 ]; then
+        printf "ERROR: timeout waiting for Kea to start.\n"
+        clean_exit 1
+    fi
+
+    # Check if it is still running. It could have terminated (e.g. as a result
+    # of configuration failure).
+    get_pids ${bin}
+    if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+        printf "ERROR: expected one Kea process to be started. Found %d processes\
+ started.\n" ${_GET_PIDS_NUM}
+        clean_exit 1
+    fi
+
+    # Check if Kea emits the log message indicating that LFC is started.
+    wait_for_message 10 "DHCPSRV_MEMFILE_LFC_EXECUTE" 1
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: Server did not execute LFC.\n"
+        clean_exit 1
+    fi
+
+    # Give it a short time to run.
+    sleep 1
+
+    # Modify the interval.
+    LFC_CONFIG=$(printf "${CONFIG}" | sed -e 's/\"lfc-interval\": 1/\"lfc-interval\": 2/g')
+    # Create new configuration file.
+    create_config "${LFC_CONFIG}"
+
+    # Reconfigure the server with SIGHUP.
+    send_signal 1 ${bin}
+
+    # There should be two occurrences of the DHCP4_CONFIG_COMPLETE messages.
+    # Wait for it up to 10s.
+    wait_for_message 10 "DHCP6_CONFIG_COMPLETE" 2
+
+    # After receiving SIGHUP the server should get reconfigured and the
+    # reconfiguration should be noted in the log file. We should now
+    # have two configurations logged in the log file.
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: server hasn't been reconfigured.\n"
+        clean_exit 1
+    else
+        printf "Server successfully reconfigured.\n"
+    fi
+
+    # Make sure the server is still operational.
+    get_pids ${bin}
+    if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+        printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
+        clean_exit 1
+    fi
+
+    # Wait for the LFC to run the second time.
+    wait_for_message 10 "DHCPSRV_MEMFILE_LFC_EXECUTE" 2
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: Server did not execute LFC.\n"
+        clean_exit 1
+    fi
+
+    # Send signal to Kea SIGTERM
+    send_signal 15 ${bin}
+
+    # Wait up to 10s for the server's graceful shutdown. The graceful shut down
+    # should be recorded in the log file with the appropriate message.
+    wait_for_message 10 "DHCP6_SHUTDOWN" 1
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: Server did not record shutdown in the log.\n"
+        clean_exit 1
+    fi
+
+    # Make sure the server is down.
+    wait_for_server_down 5 ${bin}
+    assert_eq 1 ${_WAIT_FOR_SERVER_DOWN} \
+        "Expected wait_for_server_down return %d, returned %d"
+
+    # All ok. Shut down Kea and exit.
+    test_finish 0
+}
+
 server_pid_file_test "${CONFIG}" DHCP6_ALREADY_RUNNING
 server_pid_file_test "${CONFIG}" DHCP6_ALREADY_RUNNING
 dynamic_reconfiguration_test
 dynamic_reconfiguration_test
 shutdown_test "dhcpv6.sigterm_test" 15
 shutdown_test "dhcpv6.sigterm_test" 15
 shutdown_test "dhcpv6.sigint_test" 2
 shutdown_test "dhcpv6.sigint_test" 2
 version_test "dhcpv6.version"
 version_test "dhcpv6.version"
 logger_vars_test "dhcpv6.variables"
 logger_vars_test "dhcpv6.variables"
+lfc_timer_test
+

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

@@ -346,6 +346,12 @@ failure persist however, the size of the lease file will increase without bound.
 An informational message issued when the Memfile lease database backend
 An informational message issued when the Memfile lease database backend
 starts the periodic Lease File Cleanup.
 starts the periodic Lease File Cleanup.
 
 
+% DHCPSRV_MEMFILE_LFC_UNREGISTER_TIMER_FAILED failed to unregister timer 'memfile-lfc': %1
+This error message is logged when Memfile backend fails to unregister
+timer used for lease file cleanup scheduling. This is highly unlikely
+and indicates programming error. The message include the reason for this
+error.
+
 % DHCPSRV_MEMFILE_NO_STORAGE running in non-persistent mode, leases will be lost after restart
 % DHCPSRV_MEMFILE_NO_STORAGE running in non-persistent mode, leases will be lost after restart
 A warning message issued when writes of leases to disk have been disabled
 A warning message issued when writes of leases to disk have been disabled
 in the configuration. This mode is useful for some kinds of performance
 in the configuration. This mode is useful for some kinds of performance

+ 1 - 40
src/lib/dhcpsrv/lease_mgr.h

@@ -118,16 +118,6 @@ public:
 /// be used directly, but rather specialized derived class should be used
 /// be used directly, but rather specialized derived class should be used
 /// instead.
 /// instead.
 ///
 ///
-/// This class creates an instance of the @c asiolink::IOService in the
-/// constructor. This object is required to execute the
-/// @c asiolink::IntervalTimer for the operations triggered periodically
-/// by the lease database backends which implement @c LeaseMgr interface.
-/// In order to execute the timers installed by the particular backend,
-/// the owner of the backend (e.g. DHCP server) should retrieve the pointer
-/// to the @c asiolink::IOService object by calling @c LeaseMgr::getIOService
-/// and call the appropriate functions, e.g. @c poll_one or @c run_one in a
-/// main loop.
-///
 /// This class throws no exceptions.  However, methods in concrete
 /// This class throws no exceptions.  However, methods in concrete
 /// implementations of this class may throw exceptions: see the documentation
 /// implementations of this class may throw exceptions: see the documentation
 /// of those classes for details.
 /// of those classes for details.
@@ -145,7 +135,7 @@ public:
     /// @param parameters A data structure relating keywords and values
     /// @param parameters A data structure relating keywords and values
     ///        concerned with the database.
     ///        concerned with the database.
     LeaseMgr(const ParameterMap& parameters)
     LeaseMgr(const ParameterMap& parameters)
-        : parameters_(parameters), io_service_(new asiolink::IOService())
+        : parameters_(parameters)
     {}
     {}
 
 
     /// @brief Destructor
     /// @brief Destructor
@@ -439,31 +429,6 @@ public:
     /// @brief returns value of the parameter
     /// @brief returns value of the parameter
     virtual std::string getParameter(const std::string& name) const;
     virtual std::string getParameter(const std::string& name) const;
 
 
-    /// @brief Returns the interval at which the @c IOService events should
-    /// be released.
-    ///
-    /// The implementations of this class may install the timers which
-    /// periodically trigger event handlers defined for them. Depending
-    /// on the intervals specified for these timers the @c IOService::poll,
-    /// @c IOService::run etc. have to be executed to allow the timers
-    /// for checking whether they have already expired and the handler
-    /// must be executed. Running the @c IOService with a lower interval
-    /// would cause the desynchronization of timers with the clock.
-    ///
-    /// @return A maximum interval in seconds at which the @c IOService
-    /// should be executed. A value of 0 means that no timers are installed
-    /// and that there is no requirement for the @c IOService to be
-    /// executed at any specific interval.
-    virtual uint32_t getIOServiceExecInterval() const {
-        return (0);
-    }
-
-    /// @brief Returns a reference to the @c IOService object used
-    /// by the Lease Manager.
-    const asiolink::IOServicePtr& getIOService() const {
-        return (io_service_);
-    }
-
 private:
 private:
     /// @brief list of parameters passed in dbconfig
     /// @brief list of parameters passed in dbconfig
     ///
     ///
@@ -471,10 +436,6 @@ private:
     /// password and other parameters required for DB access. It is not
     /// password and other parameters required for DB access. It is not
     /// intended to keep any DHCP-related parameters.
     /// intended to keep any DHCP-related parameters.
     ParameterMap parameters_;
     ParameterMap parameters_;
-
-    /// @brief Pointer to the IO service object used by the derived classes
-    /// to trigger interval timers.
-    asiolink::IOServicePtr io_service_;
 };
 };
 
 
 }; // end of isc::dhcp namespace
 }; // end of isc::dhcp namespace

+ 52 - 36
src/lib/dhcpsrv/memfile_lease_mgr.cc

@@ -17,6 +17,7 @@
 #include <dhcpsrv/dhcpsrv_log.h>
 #include <dhcpsrv/dhcpsrv_log.h>
 #include <dhcpsrv/lease_file_loader.h>
 #include <dhcpsrv/lease_file_loader.h>
 #include <dhcpsrv/memfile_lease_mgr.h>
 #include <dhcpsrv/memfile_lease_mgr.h>
+#include <dhcpsrv/timer_mgr.h>
 #include <exceptions/exceptions.h>
 #include <exceptions/exceptions.h>
 #include <util/pid_file.h>
 #include <util/pid_file.h>
 #include <util/process_spawn.h>
 #include <util/process_spawn.h>
@@ -73,9 +74,12 @@ public:
     /// @c Memfile_LeaseMgr class.
     /// @c Memfile_LeaseMgr class.
     ///
     ///
     /// @param callback A pointer to the callback function.
     /// @param callback A pointer to the callback function.
-    /// @param io_service An io service used to create the interval timer.
-    LFCSetup(asiolink::IntervalTimer::Callback callback,
-             asiolink::IOService& io_service);
+    LFCSetup(asiolink::IntervalTimer::Callback callback);
+
+    /// @brief Destructor.
+    ///
+    /// Unregisters LFC timer.
+    ~LFCSetup();
 
 
     /// @brief Sets the new configuration for the %Lease File Cleanup.
     /// @brief Sets the new configuration for the %Lease File Cleanup.
     ///
     ///
@@ -92,11 +96,6 @@ public:
     /// @brief Spawns a new process.
     /// @brief Spawns a new process.
     void execute();
     void execute();
 
 
-    /// @brief Returns interval at which the cleanup is performed.
-    ///
-    /// @return Interval in milliseconds.
-    long getInterval() const;
-
     /// @brief Checks if the lease file cleanup is in progress.
     /// @brief Checks if the lease file cleanup is in progress.
     ///
     ///
     /// @return true if the lease file cleanup is being executed.
     /// @return true if the lease file cleanup is being executed.
@@ -107,9 +106,6 @@ public:
 
 
 private:
 private:
 
 
-    /// @brief Interval timer for LFC.
-    asiolink::IntervalTimer timer_;
-
     /// @brief A pointer to the @c ProcessSpawn object used to execute
     /// @brief A pointer to the @c ProcessSpawn object used to execute
     /// the LFC.
     /// the LFC.
     boost::scoped_ptr<util::ProcessSpawn> process_;
     boost::scoped_ptr<util::ProcessSpawn> process_;
@@ -119,11 +115,40 @@ private:
 
 
     /// @brief A PID of the last executed LFC process.
     /// @brief A PID of the last executed LFC process.
     pid_t pid_;
     pid_t pid_;
+
+    /// @brief Pointer to the timer manager.
+    ///
+    /// We have to hold this pointer here to make sure that the timer
+    /// manager is not destroyed before the lease manager.
+    TimerMgrPtr timer_mgr_;
 };
 };
 
 
-LFCSetup::LFCSetup(asiolink::IntervalTimer::Callback callback,
-                   asiolink::IOService& io_service)
-    : timer_(io_service), process_(), callback_(callback), pid_(0) {
+LFCSetup::LFCSetup(asiolink::IntervalTimer::Callback callback)
+    : process_(), callback_(callback), pid_(0),
+      timer_mgr_(TimerMgr::instance()) {
+}
+
+LFCSetup::~LFCSetup() {
+    try {
+        // If we're here it means that either the process is terminating
+        // or we're reconfiguring the server. In the latter case the
+        // thread has been stopped probably, but we need to handle the
+        // former case so we call stopThread explicitly here.
+        timer_mgr_->stopThread();
+        // This may throw exception if the timer hasn't been registered
+        // but if the LFC Setup instance exists it means that the timer
+        // must have been registered or such registration have been
+        // attempted. The registration may fail if the duplicate timer
+        // exists or if the TimerMgr's worker thread is running but if
+        // this happens it is a programming error. In any case, we
+        // don't want exceptions being thrown from the destructor so
+        // we just log an error here.
+        timer_mgr_->unregisterTimer("memfile-lfc");
+
+    } catch (const std::exception& ex) {
+        LOG_ERROR(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_UNREGISTER_TIMER_FAILED)
+            .arg(ex.what());
+    }
 }
 }
 
 
 void
 void
@@ -143,13 +168,6 @@ LFCSetup::setup(const uint32_t lfc_interval,
             executable = c_executable;
             executable = c_executable;
         }
         }
 
 
-        // Set the timer to call callback function periodically.
-        LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_SETUP).arg(lfc_interval);
-        // Multiple the lfc_interval value by 1000 as this value specifies
-        // a timeout in seconds, whereas the setup() method expects the
-        // timeout in milliseconds.
-        timer_.setup(callback_, lfc_interval * 1000);
-
         // Start preparing the command line for kea-lfc.
         // Start preparing the command line for kea-lfc.
 
 
         // Gather the base file name.
         // Gather the base file name.
@@ -187,12 +205,17 @@ LFCSetup::setup(const uint32_t lfc_interval,
 
 
         // Create the process (do not start it yet).
         // Create the process (do not start it yet).
         process_.reset(new util::ProcessSpawn(executable, args));
         process_.reset(new util::ProcessSpawn(executable, args));
-    }
-}
 
 
-long
-LFCSetup::getInterval() const {
-    return (timer_.getInterval());
+        // Set the timer to call callback function periodically.
+        LOG_INFO(dhcpsrv_logger, DHCPSRV_MEMFILE_LFC_SETUP).arg(lfc_interval);
+
+        // Multiple the lfc_interval value by 1000 as this value specifies
+        // a timeout in seconds, whereas the setup() method expects the
+        // timeout in milliseconds.
+        timer_mgr_->registerTimer("memfile-lfc", callback_, lfc_interval * 1000,
+                                  asiolink::IntervalTimer::REPEATING);
+        timer_mgr_->setup("memfile-lfc");
+    }
 }
 }
 
 
 void
 void
@@ -227,9 +250,7 @@ const int Memfile_LeaseMgr::MAJOR_VERSION;
 const int Memfile_LeaseMgr::MINOR_VERSION;
 const int Memfile_LeaseMgr::MINOR_VERSION;
 
 
 Memfile_LeaseMgr::Memfile_LeaseMgr(const ParameterMap& parameters)
 Memfile_LeaseMgr::Memfile_LeaseMgr(const ParameterMap& parameters)
-    : LeaseMgr(parameters),
-      lfc_setup_(new LFCSetup(boost::bind(&Memfile_LeaseMgr::lfcCallback, this),
-                              *getIOService()))
+    : LeaseMgr(parameters), lfc_setup_()
     {
     {
     // Check the universe and use v4 file or v6 file.
     // Check the universe and use v4 file or v6 file.
     std::string universe = getParameter("universe");
     std::string universe = getParameter("universe");
@@ -255,7 +276,7 @@ Memfile_LeaseMgr::Memfile_LeaseMgr(const ParameterMap& parameters)
         LOG_WARN(dhcpsrv_logger, DHCPSRV_MEMFILE_NO_STORAGE);
         LOG_WARN(dhcpsrv_logger, DHCPSRV_MEMFILE_NO_STORAGE);
 
 
     } else  {
     } else  {
-        lfcSetup();
+       lfcSetup();
     }
     }
 }
 }
 
 
@@ -785,12 +806,6 @@ Memfile_LeaseMgr::appendSuffix(const std::string& file_name,
     return (name);
     return (name);
 }
 }
 
 
-
-uint32_t
-Memfile_LeaseMgr::getIOServiceExecInterval() const {
-    return (static_cast<uint32_t>(lfc_setup_->getInterval() / 1000));
-}
-
 std::string
 std::string
 Memfile_LeaseMgr::getDefaultLeaseFilePath(Universe u) const {
 Memfile_LeaseMgr::getDefaultLeaseFilePath(Universe u) const {
     std::ostringstream s;
     std::ostringstream s;
@@ -943,6 +958,7 @@ Memfile_LeaseMgr::lfcSetup() {
     }
     }
 
 
     if (lfc_interval > 0) {
     if (lfc_interval > 0) {
+        lfc_setup_.reset(new LFCSetup(boost::bind(&Memfile_LeaseMgr::lfcCallback, this)));
         lfc_setup_->setup(lfc_interval, lease_file4_, lease_file6_);
         lfc_setup_->setup(lfc_interval, lease_file4_, lease_file6_);
     }
     }
 }
 }

+ 1 - 19
src/lib/dhcpsrv/memfile_lease_mgr.h

@@ -49,12 +49,7 @@ class LFCSetup;
 /// The backend installs an @c asiolink::IntervalTimer to periodically execute
 /// The backend installs an @c asiolink::IntervalTimer to periodically execute
 /// the @c Memfile_LeaseMgr::lfcCallback. This callback function controls
 /// the @c Memfile_LeaseMgr::lfcCallback. This callback function controls
 /// the startup of the background process which removes redundant information
 /// the startup of the background process which removes redundant information
-/// from the lease file(s). Note that the @c asiolink::IntervalTimer uses
-/// @c asiolink::IOService to execute the callback. The @c LeaseMgr class
-/// creates this object, which can be obtained by the caller using the
-/// @c LeaseMgr::getIOService. The caller should later call an appropriate
-/// method, @c boost::asio::io_service::poll_one to execute the callback when
-/// the timer is ready.
+/// from the lease file(s).
 ///
 ///
 /// When the backend is starting up, it reads leases from the lease file (one
 /// When the backend is starting up, it reads leases from the lease file (one
 /// by one) and adds them to the in-memory container as follows:
 /// by one) and adds them to the in-memory container as follows:
@@ -458,19 +453,6 @@ public:
     ///       about the state of the backend.
     ///       about the state of the backend.
     //@{
     //@{
 
 
-    /// @brief Returns the interval at which the @c IOService events should
-    /// be released.
-    ///
-    /// The Memfile backend may install a timer to execute the %Lease File
-    /// Cleanup periodically. If this timer is installed, the method returns
-    /// the LFC interval in milliseconds.
-    ///
-    /// @return A maximum interval (in seconds) at which the @c IOService
-    /// should be executed. A value of 0 means that no timers are installed
-    /// and that there is no requirement for the @c IOService to be
-    /// executed at any specific interval.
-    virtual uint32_t getIOServiceExecInterval() const;
-
     /// @brief Returns default path to the lease file.
     /// @brief Returns default path to the lease file.
     ///
     ///
     /// @param u Universe (V4 or V6).
     /// @param u Universe (V4 or V6).

+ 29 - 76
src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc

@@ -14,16 +14,20 @@
 
 
 #include <config.h>
 #include <config.h>
 
 
+#include <boost/asio.hpp>
 #include <asiolink/io_address.h>
 #include <asiolink/io_address.h>
 #include <dhcp/duid.h>
 #include <dhcp/duid.h>
+#include <dhcp/iface_mgr.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/memfile_lease_mgr.h>
 #include <dhcpsrv/memfile_lease_mgr.h>
+#include <dhcpsrv/timer_mgr.h>
 #include <dhcpsrv/tests/lease_file_io.h>
 #include <dhcpsrv/tests/lease_file_io.h>
 #include <dhcpsrv/tests/test_utils.h>
 #include <dhcpsrv/tests/test_utils.h>
 #include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
 #include <dhcpsrv/tests/generic_lease_mgr_unittest.h>
 #include <util/pid_file.h>
 #include <util/pid_file.h>
+#include <util/stopwatch.h>
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 
 
 #include <boost/bind.hpp>
 #include <boost/bind.hpp>
@@ -106,8 +110,7 @@ public:
     MemfileLeaseMgrTest() :
     MemfileLeaseMgrTest() :
         io4_(getLeaseFilePath("leasefile4_0.csv")),
         io4_(getLeaseFilePath("leasefile4_0.csv")),
         io6_(getLeaseFilePath("leasefile6_0.csv")),
         io6_(getLeaseFilePath("leasefile6_0.csv")),
-        io_service_(),
-        fail_on_callback_(false) {
+        timer_mgr_(TimerMgr::instance()) {
 
 
         std::ostringstream s;
         std::ostringstream s;
         s << KEA_LFC_BUILD_DIR << "/kea-lfc";
         s << KEA_LFC_BUILD_DIR << "/kea-lfc";
@@ -134,21 +137,10 @@ public:
     ///
     ///
     /// destroys lease manager backend.
     /// destroys lease manager backend.
     virtual ~MemfileLeaseMgrTest() {
     virtual ~MemfileLeaseMgrTest() {
-        // Explicitly destroy the timer and the IO service. Note that they
-        // must be destroyed in this order because test_timer_ depends on
-        // the io_service_ as it is passed its reference during the
-        // construction. It usually doesn't matter unless the timer is
-        // running (hasn't been cancelled). Destroying an underlying
-        // IO service before cancelling the timer relying on it may lead
-        // to a crash. This has been proven on CentOS 6, running boost
-        // version 1.41. Note that destroying the timer also cancels it.
-        // In theory, we could avoid this explicit release of these objects
-        // and rely on the order in which they are declared in the class.
-        // But, this seems to be better as it makes it more visible
-        // what we are doing here.
-        test_timer_.reset();
-        io_service_.reset();
-
+        // Stop TimerMgr worker thread if it is running.
+        timer_mgr_->stopThread();
+        // Make sure there are no timers registered.
+        timer_mgr_->unregisterTimers();
         LeaseMgrFactory::destroy();
         LeaseMgrFactory::destroy();
         // Remove lease files and products of Lease File Cleanup.
         // Remove lease files and products of Lease File Cleanup.
         removeFiles(getLeaseFilePath("leasefile4_0.csv"));
         removeFiles(getLeaseFilePath("leasefile4_0.csv"));
@@ -219,18 +211,15 @@ public:
         lmptr_ = &(LeaseMgrFactory::instance());
         lmptr_ = &(LeaseMgrFactory::instance());
     }
     }
 
 
+    /// @brief Runs @c IfaceMgr::receive6 in a look for a specified time.
+    ///
+    /// @param ms Duration in milliseconds.
     void setTestTime(const uint32_t ms) {
     void setTestTime(const uint32_t ms) {
-        IntervalTimer::Callback cb =
-            boost::bind(&MemfileLeaseMgrTest::testTimerCallback, this);
-        test_timer_.reset(new IntervalTimer(*io_service_));
-        test_timer_->setup(cb, ms, IntervalTimer::ONE_SHOT);
-    }
-
-    /// @brief Test timer callback function.
-    void testTimerCallback() {
-        io_service_->stop();
-        if (fail_on_callback_) {
-            FAIL() << "Test timeout reached";
+        // Measure test time and exit if timeout hit.
+        Stopwatch stopwatch;
+        while (stopwatch.getTotalMilliseconds() < ms) {
+            // Block for one 1 millisecond.
+            IfaceMgr::instance().receive6(0, 1000);
         }
         }
     }
     }
 
 
@@ -258,15 +247,8 @@ public:
     /// @brief Object providing access to v6 lease IO.
     /// @brief Object providing access to v6 lease IO.
     LeaseFileIO io6_;
     LeaseFileIO io6_;
 
 
-    /// @brief IO service object used for the timer tests.
-    asiolink::IOServicePtr io_service_;
-
-    /// @brief Test timer for the test.
-    boost::shared_ptr<IntervalTimer> test_timer_;
-
-    /// @brief Indicates if the @c testTimerCallback should cause test failure.
-    bool fail_on_callback_;
-
+    /// @brief Pointer to the instance of the @c TimerMgr.
+    TimerMgrPtr timer_mgr_;
 };
 };
 
 
 // This test checks if the LeaseMgr can be instantiated and that it
 // This test checks if the LeaseMgr can be instantiated and that it
@@ -368,16 +350,14 @@ TEST_F(MemfileLeaseMgrTest, lfcTimer) {
     boost::scoped_ptr<LFCMemfileLeaseMgr>
     boost::scoped_ptr<LFCMemfileLeaseMgr>
         lease_mgr(new LFCMemfileLeaseMgr(pmap));
         lease_mgr(new LFCMemfileLeaseMgr(pmap));
 
 
-    // Check that the interval is correct.
-    EXPECT_EQ(1, lease_mgr->getIOServiceExecInterval());
-
-    io_service_ = lease_mgr->getIOService();
+    // Start worker thread to execute LFC periodically.
+    ASSERT_NO_THROW(timer_mgr_->startThread());
 
 
     // Run the test for at most 2.9 seconds.
     // Run the test for at most 2.9 seconds.
     setTestTime(2900);
     setTestTime(2900);
 
 
-    // Run the IO service to execute timers.
-    io_service_->run();
+    // Stop worker thread.
+    ASSERT_NO_THROW(timer_mgr_->stopThread());
 
 
     // Within 2.9 we should record two LFC executions.
     // Within 2.9 we should record two LFC executions.
     EXPECT_EQ(2, lease_mgr->getLFCCount());
     EXPECT_EQ(2, lease_mgr->getLFCCount());
@@ -397,15 +377,16 @@ TEST_F(MemfileLeaseMgrTest, lfcTimerDisabled) {
     boost::scoped_ptr<LFCMemfileLeaseMgr>
     boost::scoped_ptr<LFCMemfileLeaseMgr>
         lease_mgr(new LFCMemfileLeaseMgr(pmap));
         lease_mgr(new LFCMemfileLeaseMgr(pmap));
 
 
-    EXPECT_EQ(0, lease_mgr->getIOServiceExecInterval());
-
-    io_service_ = lease_mgr->getIOService();
+    // Start worker thread to execute LFC periodically.
+    ASSERT_NO_THROW(timer_mgr_->startThread());
 
 
     // Run the test for at most 1.9 seconds.
     // Run the test for at most 1.9 seconds.
     setTestTime(1900);
     setTestTime(1900);
 
 
-    // Run the IO service to execute timers.
-    io_service_->run();
+    // Stop worker thread to make sure it is not running when lease
+    // manager is destroyed. The lease manager will be unable to
+    // unregster timer when the thread is active.
+    ASSERT_NO_THROW(timer_mgr_->stopThread());
 
 
     // There should be no LFC execution recorded.
     // There should be no LFC execution recorded.
     EXPECT_EQ(0, lease_mgr->getLFCCount());
     EXPECT_EQ(0, lease_mgr->getLFCCount());
@@ -739,34 +720,6 @@ TEST_F(MemfileLeaseMgrTest, leaseFileCopy) {
     ASSERT_FALSE(input_file.exists());
     ASSERT_FALSE(input_file.exists());
 }
 }
 
 
-
-// Test that the backend returns a correct value of the interval
-// at which the IOService must be executed to run the handlers
-// for the installed timers.
-TEST_F(MemfileLeaseMgrTest, getIOServiceExecInterval) {
-    LeaseMgr::ParameterMap pmap;
-    pmap["type"] = "memfile";
-    pmap["universe"] = "4";
-    pmap["name"] = getLeaseFilePath("leasefile4_0.csv");
-
-    // The lfc-interval is not set, so the returned value should be 0.
-    boost::scoped_ptr<LFCMemfileLeaseMgr> lease_mgr(new LFCMemfileLeaseMgr(pmap));
-    EXPECT_EQ(0, lease_mgr->getIOServiceExecInterval());
-
-    // lfc-interval = 10
-    pmap["lfc-interval"] = "10";
-    lease_mgr.reset();
-    lease_mgr.reset(new LFCMemfileLeaseMgr(pmap));
-    EXPECT_EQ(10, lease_mgr->getIOServiceExecInterval());
-
-    // lfc-interval = 20
-    pmap["lfc-interval"] = "20";
-    lease_mgr.reset();
-    lease_mgr.reset(new LFCMemfileLeaseMgr(pmap));
-    EXPECT_EQ(20, lease_mgr->getIOServiceExecInterval());
-
-}
-
 // Checks that adding/getting/deleting a Lease6 object works.
 // Checks that adding/getting/deleting a Lease6 object works.
 TEST_F(MemfileLeaseMgrTest, addGetDelete6) {
 TEST_F(MemfileLeaseMgrTest, addGetDelete6) {
     startBackend(V6);
     startBackend(V6);

+ 10 - 6
src/lib/dhcpsrv/tests/timer_mgr_unittest.cc

@@ -208,6 +208,7 @@ TEST_F(TimerMgrTest, registerTimer) {
 TEST_F(TimerMgrTest, unregisterTimer) {
 TEST_F(TimerMgrTest, unregisterTimer) {
     // Register a timer and start it.
     // Register a timer and start it.
     ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 1));
     ASSERT_NO_FATAL_FAILURE(registerTimer("timer1", 1));
+    ASSERT_EQ(1, timer_mgr_->timersCount());
     ASSERT_NO_THROW(timer_mgr_->setup("timer1"));
     ASSERT_NO_THROW(timer_mgr_->setup("timer1"));
     ASSERT_NO_THROW(timer_mgr_->startThread());
     ASSERT_NO_THROW(timer_mgr_->startThread());
 
 
@@ -223,10 +224,13 @@ TEST_F(TimerMgrTest, unregisterTimer) {
 
 
     // Check that an attempt to unregister a non-existing timer would
     // Check that an attempt to unregister a non-existing timer would
     // result in exeception.
     // result in exeception.
-    EXPECT_THROW(timer_mgr_->unregisterTimer("timer2"), BadValue);
+    ASSERT_THROW(timer_mgr_->unregisterTimer("timer2"), BadValue);
+    // Number of timers shouldn't have changed.
+    ASSERT_EQ(1, timer_mgr_->timersCount());
 
 
     // Now unregister the correct one.
     // Now unregister the correct one.
     ASSERT_NO_THROW(timer_mgr_->unregisterTimer("timer1"));
     ASSERT_NO_THROW(timer_mgr_->unregisterTimer("timer1"));
+    ASSERT_EQ(0, timer_mgr_->timersCount());
 
 
     // Start the thread again and wait another 100ms.
     // Start the thread again and wait another 100ms.
     ASSERT_NO_THROW(timer_mgr_->startThread());
     ASSERT_NO_THROW(timer_mgr_->startThread());
@@ -239,11 +243,6 @@ TEST_F(TimerMgrTest, unregisterTimer) {
 }
 }
 
 
 // This test verifies taht it is possible to unregister all timers.
 // 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, unregisterTimers) {
 TEST_F(TimerMgrTest, unregisterTimers) {
     // Register 10 timers.
     // Register 10 timers.
     for (int i = 1; i <= 20; ++i) {
     for (int i = 1; i <= 20; ++i) {
@@ -252,6 +251,8 @@ TEST_F(TimerMgrTest, unregisterTimers) {
         ASSERT_NO_FATAL_FAILURE(registerTimer(s.str(), 1))
         ASSERT_NO_FATAL_FAILURE(registerTimer(s.str(), 1))
             << "fatal failure occurred while registering "
             << "fatal failure occurred while registering "
             << s.str();
             << s.str();
+        ASSERT_EQ(i, timer_mgr_->timersCount())
+            << "invalid number of registered timers returned";
         ASSERT_NO_THROW(timer_mgr_->setup(s.str()))
         ASSERT_NO_THROW(timer_mgr_->setup(s.str()))
             << "exception thrown while calling setup() for the "
             << "exception thrown while calling setup() for the "
             << s.str();
             << s.str();
@@ -278,6 +279,9 @@ TEST_F(TimerMgrTest, unregisterTimers) {
     // Let's unregister all timers.
     // Let's unregister all timers.
     ASSERT_NO_THROW(timer_mgr_->unregisterTimers());
     ASSERT_NO_THROW(timer_mgr_->unregisterTimers());
 
 
+    // Make sure there are no timers registered.
+    ASSERT_EQ(0, timer_mgr_->timersCount());
+
     // Start worker thread again and wait for 500ms.
     // Start worker thread again and wait for 500ms.
     ASSERT_NO_THROW(timer_mgr_->startThread());
     ASSERT_NO_THROW(timer_mgr_->startThread());
     doWait(500);
     doWait(500);

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

@@ -168,6 +168,9 @@ public:
     /// process.
     /// process.
     void unregisterTimers();
     void unregisterTimers();
 
 
+    /// @brief Returns the number of registered timers.
+    size_t timersCount() const;
+
     /// @brief Schedules the execution of the interval timer.
     /// @brief Schedules the execution of the interval timer.
     ///
     ///
     /// This method schedules the timer, i.e. the callback will be executed
     /// This method schedules the timer, i.e. the callback will be executed
@@ -406,6 +409,11 @@ TimerMgrImpl::unregisterTimers() {
     }
     }
 }
 }
 
 
+size_t
+TimerMgrImpl::timersCount() const {
+    return (registered_timers_.size());
+}
+
 void
 void
 TimerMgrImpl::setup(const std::string& timer_name) {
 TimerMgrImpl::setup(const std::string& timer_name) {
 
 
@@ -652,6 +660,11 @@ TimerMgr::unregisterTimers() {
     impl_->unregisterTimers();
     impl_->unregisterTimers();
 }
 }
 
 
+size_t
+TimerMgr::timersCount() const {
+    return (impl_->timersCount());
+}
+
 void
 void
 TimerMgr::setup(const std::string& timer_name) {
 TimerMgr::setup(const std::string& timer_name) {
 
 

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

@@ -176,6 +176,9 @@ public:
     /// @brief Unregisters all timers.
     /// @brief Unregisters all timers.
     void unregisterTimers();
     void unregisterTimers();
 
 
+    /// @brief Returns the number of registered timers.
+    size_t timersCount() const;
+
     /// @brief Schedules the execution of the interval timer.
     /// @brief Schedules the execution of the interval timer.
     ///
     ///
     /// This method schedules the timer, i.e. the callback will be executed
     /// This method schedules the timer, i.e. the callback will be executed

+ 6 - 0
src/lib/testutils/dhcp_test_lib.sh.in

@@ -247,6 +247,12 @@ cleanup() {
 
 
     # Remove temporary files.
     # Remove temporary files.
     rm -rf ${LOG_FILE}
     rm -rf ${LOG_FILE}
+    # Use asterisk to remove all files starting with the given name,
+    # in case the LFC has been run. LFC creates files with postfixes
+    # appended to the lease file name.
+    if [ ! -z "${LEASE_FILE}" ]; then
+        rm -rf ${LEASE_FILE}*
+    fi
     rm -rf ${CFG_FILE}
     rm -rf ${CFG_FILE}
     rm -rf ${KEACTRL_CFG_FILE}
     rm -rf ${KEACTRL_CFG_FILE}
 }
 }

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

@@ -10,6 +10,7 @@ lib_LTLIBRARIES = libkea-util.la
 libkea_util_la_SOURCES  = boost_time_utils.h boost_time_utils.cc
 libkea_util_la_SOURCES  = boost_time_utils.h boost_time_utils.cc
 libkea_util_la_SOURCES += csv_file.h csv_file.cc
 libkea_util_la_SOURCES += csv_file.h csv_file.cc
 libkea_util_la_SOURCES += filename.h filename.cc
 libkea_util_la_SOURCES += filename.h filename.cc
+libkea_util_la_SOURCES += fork_detector.h
 libkea_util_la_SOURCES += strutil.h strutil.cc
 libkea_util_la_SOURCES += strutil.h strutil.cc
 libkea_util_la_SOURCES += buffer.h io_utilities.h
 libkea_util_la_SOURCES += buffer.h io_utilities.h
 libkea_util_la_SOURCES += time_utilities.h time_utilities.cc
 libkea_util_la_SOURCES += time_utilities.h time_utilities.cc

+ 62 - 0
src/lib/util/fork_detector.h

@@ -0,0 +1,62 @@
+// 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 FORK_DETECTOR_H
+#define FORK_DETECTOR_H
+
+#include <sys/types.h>
+#include <unistd.h>
+
+namespace isc {
+namespace util {
+
+/// @brief Class which detects being child process.
+///
+/// This class detects if it is in the child process, by checking if the
+/// process PID matches the PID of the process which created instance of
+/// the class.
+///
+/// Detecting if we're in the child process is important when the application
+/// spawns new process using fork/exec. If exec step fails for any reason
+/// the child process exits. However, to exit gracefully the process may
+/// need to know if it is a child or parent and take a different path
+/// during destruction.
+class ForkDetector {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Stores the PID of the process creating this instance.
+    ForkDetector()
+        : creator_pid_(getpid()) {
+    }
+
+    /// @brief Check if the process is a parent process;
+    ///
+    /// @return true if the process is a parent process.
+    bool isParent() const {
+        return (getpid() == creator_pid_);
+    }
+
+private:
+
+    /// @brief PID of the process which created instance of this class.
+    pid_t creator_pid_;
+
+};
+
+} // namespace isc::util
+} // namespace isc
+
+#endif // FORK_DETECTOR_H

+ 192 - 34
src/lib/util/threads/tests/thread_unittest.cc

@@ -14,14 +14,18 @@
 
 
 #include <config.h>
 #include <config.h>
 
 
+#include <util/process_spawn.h>
+#include <util/threads/sync.h>
 #include <util/threads/thread.h>
 #include <util/threads/thread.h>
 #include <util/unittests/check_valgrind.h>
 #include <util/unittests/check_valgrind.h>
 
 
 #include <boost/bind.hpp>
 #include <boost/bind.hpp>
+#include <boost/scoped_ptr.hpp>
 
 
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 
 
 #include <signal.h>
 #include <signal.h>
+#include <unistd.h>
 
 
 // This file tests the Thread class. It's hard to test an actual thread is
 // This file tests the Thread class. It's hard to test an actual thread is
 // started, but we at least check the function is run and exceptions are
 // started, but we at least check the function is run and exceptions are
@@ -34,38 +38,166 @@
 // started in parallel (the other tests wait for the previous one to terminate
 // started in parallel (the other tests wait for the previous one to terminate
 // before starting new one).
 // before starting new one).
 
 
+using namespace isc::util;
 using namespace isc::util::thread;
 using namespace isc::util::thread;
 
 
 namespace {
 namespace {
 const size_t iterations = 200;
 const size_t iterations = 200;
 const size_t detached_iterations = 25;
 const size_t detached_iterations = 25;
 
 
-void
-doSomething(int*) { }
+/// @brief Implements a thread which can be stopped.
+///
+/// This class implements a worker thread which can be stopped by calling
+/// StoppableThread::stop. The call to this function blocks until the thread
+/// terminates. This class is useful for testing scenarios when the thread
+/// needs to be run for a specific amount of time.
+class StoppableThread {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// It doesn't run the thread yet. It merely initializes required
+    /// class members.
+    StoppableThread()
+        : stopping_(false), mutex_(), thread_() {
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Detaches the thread.
+    ~StoppableThread() {
+    }
+
+    /// @brief Starts the execution of the thread.
+    ///
+    /// This method will not start the thread if the thread has been stopped.
+    /// In order to start the thread again, @c StoppableThread::reset must be
+    /// called first.
+    void run() {
+        if (!amStopping()) {
+            thread_.reset(new Thread(boost::bind(&StoppableThread::loop, this)));
+        }
+    }
+
+    /// @brief Stops the thread as soon as possible.
+    void stop() {
+        if (!amStopping()) {
+            setStopping(true);
+            if (thread_) {
+                thread_->wait();
+                thread_.reset();
+            }
+        }
+    }
+
+    /// @brief Resets the thread state so as it can be ran again.
+    void reset() {
+        setStopping(false);
+    }
+
+private:
+
+    /// @brief Checks if the thread is being stopped.
+    ///
+    /// @return true if the thread is being stopped.
+    bool amStopping() {
+        Mutex::Locker lock(mutex_);
+        return (stopping_);
+    }
+
+    /// @brief Sets the stopping state.
+    ///
+    /// @param stopping New value for @c stopping_ state.
+    void setStopping(const bool stopping) {
+        Mutex::Locker lock(mutex_);
+        stopping_ = stopping;
+    }
+
+    /// @brief Worker thread loop.
+    ///
+    /// It runs until the @c StoppableThread::stop is called.
+    void loop() {
+        for (;;) {
+            if (amStopping()) {
+                break;
+            }
+            usleep(100);
+        }
+    }
+
+    /// @brief Flag indicating if the thread is being stopped.
+    bool stopping_;
+    /// @brief Mutex used for protecting @c stopping_ flag.
+    Mutex mutex_;
+    /// @brief Pointer to the thread instance.
+    boost::scoped_ptr<Thread> thread_;
+};
+
+/// @brief Static instance of the stoppable thread.
+boost::scoped_ptr<StoppableThread> thread;
+
+/// @brief Test fixture class for testing @c Thread.
+class ThreadTest : public ::testing::Test {
+public:
+
+    /// @brief Destructor.
+    ///
+    /// Stops the thread and resets the static pointer to
+    /// @c StoppableThread.
+    virtual ~ThreadTest() {
+        if (thread) {
+            thread->stop();
+        }
+        thread.reset();
+    }
+
+    /// @brief No-op method.
+    static void doSomething(int*) { }
+
+    /// @brief Marks specified boolean value as true to indicate that the
+    /// function has been run.
+    static void markRun(bool* mark) {
+        EXPECT_FALSE(*mark);
+        *mark = true;
+    }
+
+    /// @brief Throws 42.
+    static void throwSomething() {
+        throw 42; // Throw something really unusual, to see everything is caught.
+    }
+
+    /// @brief Throws standard exception.
+    static void throwException() {
+        throw std::exception();
+    }
+
+    /// @brief Returns signal mask set for a thread.
+    ///
+    /// @parm mask Pointer to signal mask set for the calling thread.
+    static void getSignalMask(sigset_t* mask) {
+        pthread_sigmask(SIG_SETMASK, 0, mask);
+    }
+};
+
+
 
 
 // We just test that we can forget about the thread and nothing
 // We just test that we can forget about the thread and nothing
 // bad will happen on our side.
 // bad will happen on our side.
-TEST(ThreadTest, detached) {
+TEST_F(ThreadTest, detached) {
     if (!isc::util::unittests::runningOnValgrind()) {
     if (!isc::util::unittests::runningOnValgrind()) {
         int x;
         int x;
         for (size_t i = 0; i < detached_iterations; ++i) {
         for (size_t i = 0; i < detached_iterations; ++i) {
-            Thread thread(boost::bind(&doSomething, &x));
+            Thread thread(boost::bind(&ThreadTest::doSomething, &x));
         }
         }
     }
     }
 }
 }
 
 
-void
-markRun(bool* mark) {
-    EXPECT_FALSE(*mark);
-    *mark = true;
-}
-
 // Wait for a thread to end first. The variable must be set at the time.
 // Wait for a thread to end first. The variable must be set at the time.
-TEST(ThreadTest, wait) {
+TEST_F(ThreadTest, wait) {
     if (!isc::util::unittests::runningOnValgrind()) {
     if (!isc::util::unittests::runningOnValgrind()) {
         for (size_t i = 0; i < iterations; ++i) {
         for (size_t i = 0; i < iterations; ++i) {
             bool mark = false;
             bool mark = false;
-            Thread thread(boost::bind(markRun, &mark));
+            Thread thread(boost::bind(&ThreadTest::markRun, &mark));
             thread.wait();
             thread.wait();
             ASSERT_TRUE(mark) << "Not finished yet in " << i << "th iteration";
             ASSERT_TRUE(mark) << "Not finished yet in " << i << "th iteration";
             // Can't wait second time
             // Can't wait second time
@@ -74,30 +206,20 @@ TEST(ThreadTest, wait) {
     }
     }
 }
 }
 
 
-void
-throwSomething() {
-    throw 42; // Throw something really unusual, to see everything is caught.
-}
-
-void
-throwException() {
-    throw std::exception();
-}
-
 // Exception in the thread we forget about should not do anything to us
 // Exception in the thread we forget about should not do anything to us
-TEST(ThreadTest, detachedException) {
+TEST_F(ThreadTest, detachedException) {
     if (!isc::util::unittests::runningOnValgrind()) {
     if (!isc::util::unittests::runningOnValgrind()) {
         for (size_t i = 0; i < detached_iterations; ++i) {
         for (size_t i = 0; i < detached_iterations; ++i) {
-            Thread thread(throwSomething);
+            Thread thread(&ThreadTest::throwSomething);
         }
         }
         for (size_t i = 0; i < detached_iterations; ++i) {
         for (size_t i = 0; i < detached_iterations; ++i) {
-            Thread thread(throwException);
+            Thread thread(&ThreadTest::throwException);
         }
         }
     }
     }
 }
 }
 
 
 // An uncaught exception in the thread should propagate through wait
 // An uncaught exception in the thread should propagate through wait
-TEST(ThreadTest, exception) {
+TEST_F(ThreadTest, exception) {
     if (!isc::util::unittests::runningOnValgrind()) {
     if (!isc::util::unittests::runningOnValgrind()) {
         for (size_t i = 0; i < iterations; ++i) {
         for (size_t i = 0; i < iterations; ++i) {
             Thread thread(throwSomething);
             Thread thread(throwSomething);
@@ -108,21 +230,57 @@ TEST(ThreadTest, exception) {
     }
     }
 }
 }
 
 
-// Returns signal mask set for a thread.
-void
-getSignalMask(sigset_t* mask) {
-    pthread_sigmask(SIG_SETMASK, 0, mask);
-}
-
 // Verify that all signals are blocked.
 // Verify that all signals are blocked.
-TEST(ThreadTest, sigmask) {
+TEST_F(ThreadTest, sigmask) {
     sigset_t mask;
     sigset_t mask;
     sigemptyset(&mask);
     sigemptyset(&mask);
-    Thread thread(boost::bind(getSignalMask, &mask));
+    Thread thread(boost::bind(&ThreadTest::getSignalMask, &mask));
     ASSERT_NO_THROW(thread.wait());
     ASSERT_NO_THROW(thread.wait());
     EXPECT_EQ(1, sigismember(&mask, SIGHUP));
     EXPECT_EQ(1, sigismember(&mask, SIGHUP));
     EXPECT_EQ(1, sigismember(&mask, SIGINT));
     EXPECT_EQ(1, sigismember(&mask, SIGINT));
     EXPECT_EQ(1, sigismember(&mask, SIGTERM));
     EXPECT_EQ(1, sigismember(&mask, SIGTERM));
 }
 }
 
 
+
+/// The @c ProcessSpawn class spawns new processes using the fork/exec
+/// scheme. If the call to exec fails, child process exits with the
+/// EXIT_FAILURE status code. The call to exit triggers destruction of
+/// all static objects, i.e. destructors of statically declared
+/// @c Thread objects are called in the child process. The call to
+/// fork doesn't clone threads existing in the main process. So, the
+/// @c Thread objects in the child process have invalid state, because
+/// they point to non-existing threads. As a result any attempts to
+/// detach or join the thread may result in errors or asserts.
+///
+/// This test verifies that the @c Thread class protects against such
+/// errors. It is supposed to detect that the @c Thread object is in
+/// the child process and not assert when the destruction fails.
+TEST_F(ThreadTest, spawnProcessWithThread) {
+    // Initialize and run the stoppable thread. Note that the 'thread'
+    // is a static variable, which will be 'cloned' into the child
+    // process. Its destructor will be called when the child process
+    // terminates with EXIT_FAILURE status. So in fact the destructor
+    // of the @c StoppableThread will be called twice: in the main
+    // process and in the child process.
+    thread.reset(new StoppableThread());
+    thread->run();
+
+    // Spawn the new process, using some non-existing executable. The
+    // current process will fork but the execvp should fail.
+    ProcessSpawn process_spawn("kea-dhcp4-a86570943h");
+    pid_t pid = process_spawn.spawn();
+    // Wait for the process to terminate.
+    while (process_spawn.isRunning(pid)) {
+        usleep(100);
+    }
+    // When the child process terminates it will call destructors of
+    // static objects. This means that it will call the destructor of
+    // the 'thread' object too. The 'thread' object in the child
+    // process points to non-existing thread, but we expect the
+    // graceful destruction, i.e. the destructor should not assert.
+    // If the destructor asserts the exit code returned here will
+    // be 0 - same as if we aborted.
+    EXPECT_EQ(EXIT_FAILURE, process_spawn.getExitStatus(pid));
+}
+
 }
 }

+ 24 - 6
src/lib/util/threads/thread.cc

@@ -12,6 +12,7 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 // PERFORMANCE OF THIS SOFTWARE.
 
 
+#include <util/fork_detector.h>
 #include <util/threads/thread.h>
 #include <util/threads/thread.h>
 #include <util/threads/sync.h>
 #include <util/threads/sync.h>
 
 
@@ -74,7 +75,8 @@ public:
         // and the creating thread needs to release it.
         // and the creating thread needs to release it.
         waiting_(2),
         waiting_(2),
         main_(main),
         main_(main),
-        exception_(false)
+        exception_(false),
+        fork_detector_()
     {}
     {}
     // Another of the waiting events is done. If there are no more, delete
     // Another of the waiting events is done. If there are no more, delete
     // impl.
     // impl.
@@ -125,6 +127,8 @@ public:
     Mutex mutex_;
     Mutex mutex_;
     // Which thread are we talking about anyway?
     // Which thread are we talking about anyway?
     pthread_t tid_;
     pthread_t tid_;
+    // Class to detect if we're in the child or parent process.
+    ForkDetector fork_detector_;
 };
 };
 
 
 Thread::Thread(const boost::function<void ()>& main) :
 Thread::Thread(const boost::function<void ()>& main) :
@@ -148,8 +152,15 @@ Thread::Thread(const boost::function<void ()>& main) :
 
 
 Thread::~Thread() {
 Thread::~Thread() {
     if (impl_ != NULL) {
     if (impl_ != NULL) {
-        // In case we didn't call wait yet
-        const int result = pthread_detach(impl_->tid_);
+
+        int result = pthread_detach(impl_->tid_);
+        // If the error indicates that thread doesn't exist but we're
+        // in child process (after fork) it is expected. We should
+        // not cause an assert.
+        if (result == ESRCH && !impl_->fork_detector_.isParent()) {
+            result = 0;
+        }
+
         Impl::done(impl_);
         Impl::done(impl_);
         impl_ = NULL;
         impl_ = NULL;
         // If the detach ever fails, something is screwed rather badly.
         // If the detach ever fails, something is screwed rather badly.
@@ -164,13 +175,20 @@ Thread::wait() {
                   "Wait called and no thread to wait for");
                   "Wait called and no thread to wait for");
     }
     }
 
 
+    // Was there an exception in the thread?
+    scoped_ptr<UncaughtException> ex;
+
     const int result = pthread_join(impl_->tid_, NULL);
     const int result = pthread_join(impl_->tid_, NULL);
     if (result != 0) {
     if (result != 0) {
-        isc_throw(isc::InvalidOperation, std::strerror(result));
+        // We will not throw exception if the error indicates that the
+        // thread doesn't exist and we are in the child process (forked).
+        // For the child process it is expected that the thread is not
+        // re-created when we fork.
+        if (result != ESRCH || impl_->fork_detector_.isParent()) {
+            isc_throw(isc::InvalidOperation, std::strerror(result));
+        }
     }
     }
 
 
-    // Was there an exception in the thread?
-    scoped_ptr<UncaughtException> ex;
     // Something here could in theory throw. But we already terminated the thread, so
     // Something here could in theory throw. But we already terminated the thread, so
     // we need to make sure we are in consistent state even in such situation (like
     // we need to make sure we are in consistent state even in such situation (like
     // releasing the mutex and impl_).
     // releasing the mutex and impl_).