Browse Source

[master] Merge branch 'trac3405'

Marcin Siodelski 11 years ago
parent
commit
dd0270bd91

+ 1 - 0
doc/devel/mainpage.dox

@@ -56,6 +56,7 @@
  *   - @subpage dhcpv4DDNSIntegration
  *   - @subpage dhcpv4Classifier
  *   - @subpage dhcpv4ConfigBackend
+ *   - @subpage dhcpv4SignalBasedReconfiguration
  *   - @subpage dhcpv4Other
  * - @subpage dhcp6
  *   - @subpage dhcpv6Session

+ 3 - 1
src/bin/dhcp4/bundy_controller.cc

@@ -134,7 +134,9 @@ bundyConfigHandler(ConstElementPtr new_config) {
 
 
 
-void ControlledDhcpv4Srv::init(const std::string& /*config_file*/) {
+void ControlledDhcpv4Srv::init(const std::string& config_file) {
+    // Call base class's init.
+    Daemon::init(config_file);
 
     string specfile;
     if (getenv("B10_FROM_BUILD")) {

+ 31 - 0
src/bin/dhcp4/dhcp4.dox

@@ -231,6 +231,37 @@ for ISC-DHCP. Is at least possible that similar will happen for Kea. Finally, if
 extend the isc::dhcp::CfgMgr with configuration export, this approach could be used as
 a migration tool.
 
+@section dhcpv4SignalBasedReconfiguration Reconfiguring DHCPv4 server with SIGHUP signal
+
+Online reconfiguration (reconfiguration without a need to restart the server) is an
+important feature which is supported by all modern DHCP servers. When using the JSON
+configuration backend, a configuration file name is specified with a command line
+option of the DHCP server binary. The configuration file is used to configure the
+server at startup. If the initial configuration fails, the server will fail to start.
+If the server starts and configures successfully it will use the initial configuration
+until it is reconfigured.
+
+The reconfiguration request can be triggered externally (from other process) by editing
+a configuration file and sending a SIGHUP signal to DHCP server process. After receiving
+the SIGHUP signal, the server will re-read the configuration file specified at startup.
+If the reconfiguration fails, the server will continue to run and use the last good
+configuration.
+
+The signal handler for SIGHUP (also for SIGTERM and SIGINT) are installed in the
+kea_controller.cc using the @c isc::util::SignalSet class. The
+@c isc::dhcp::Dhcp6Srv calls @c isc::dhcp::Daemon::handleSignal on each pass
+through the main loop. This method fetches the last received signal and calls
+a handler function defined in the kea_controller.cc. The handler function
+calls a static function @c configure defined in the kea_controller.cc.
+
+In order for the signal handler to know the location of the configuration file
+(specified at process startup), the location of this file needs to be stored
+in a static variable so as it may be directly accessed by the signal handler.
+This static variable is stored in the @c dhcp::Daemon class and all Kea processes
+can use it (all processes derive from this class). The configuration file
+location is initialized when the @c Daemon::init method is called. Therefore,
+derived classes should call it in their implementations of the @c init method.
+
 @section dhcpv4Other Other DHCPv4 topics
 
  For hooks API support in DHCPv4, see @ref dhcpv4Hooks.

+ 8 - 5
src/bin/dhcp4/dhcp4_messages.mes

@@ -77,11 +77,6 @@ change is committed by the administrator.
 A debug message indicating that the DHCPv4 server has received an
 updated configuration from the BIND 10 configuration system.
 
-% DHCP4_DB_BACKEND_STARTED lease database started (type: %1, name: %2)
-This informational message is printed every time DHCPv4 server is started
-and gives both the type and name of the database being used to store
-lease and other information.
-
 % DHCP4_DDNS_REQUEST_SEND_FAILED failed sending a request to b10-dhcp-ddns, error: %1,  ncr: %2
 This error message indicates that DHCP4 server attempted to send a DDNS
 update reqeust to the DHCP-DDNS server.  This is most likely a configuration or
@@ -97,6 +92,14 @@ This error message is logged when the attempt to compute DHCID for a specified
 lease has failed. The lease details and reason for failure is logged in the
 message.
 
+% DHCP4_DYNAMIC_RECONFIGURATION initate server reconfiguration using file: %1, after receiving SIGHUP signal
+This is the info message logged when the DHCPv4 server starts reconfiguration
+as a result of receiving SIGHUP signal.
+
+% DHCP4_DYNAMIC_RECONFIGURATION_FAIL dynamic server reconfiguration failed with file: %1
+This is an error message logged when the dynamic reconfiguration of the
+DHCP server failed.
+
 % DHCP4_EMPTY_HOSTNAME received empty hostname from the client, skipping processing of this option
 This debug message is issued when the server received an empty Hostname option
 from a client. Server does not process empty Hostname options and therefore

+ 16 - 10
src/bin/dhcp4/dhcp4_srv.cc

@@ -82,11 +82,11 @@ namespace dhcp {
 
 const std::string Dhcpv4Srv::VENDOR_CLASS_PREFIX("VENDOR_CLASS_");
 
-Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast,
+Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const bool use_bcast,
                      const bool direct_response_desired)
-: shutdown_(true), alloc_engine_(), port_(port),
-    use_bcast_(use_bcast), hook_index_pkt4_receive_(-1),
-    hook_index_subnet4_select_(-1), hook_index_pkt4_send_(-1) {
+    : shutdown_(true), alloc_engine_(), port_(port),
+      use_bcast_(use_bcast), hook_index_pkt4_receive_(-1),
+      hook_index_subnet4_select_(-1), hook_index_pkt4_send_(-1) {
 
     LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_OPEN_SOCKET).arg(port);
     try {
@@ -112,12 +112,6 @@ Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast,
             IfaceMgr::instance().openSockets4(port_, use_bcast_, error_handler);
         }
 
-        // Instantiate LeaseMgr
-        LeaseMgrFactory::create(dbconfig);
-        LOG_INFO(dhcp4_logger, DHCP4_DB_BACKEND_STARTED)
-            .arg(LeaseMgrFactory::instance().getType())
-            .arg(LeaseMgrFactory::instance().getName());
-
         // Instantiate allocation engine
         alloc_engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100,
                                             false /* false = IPv4 */));
@@ -175,6 +169,18 @@ Dhcpv4Srv::run() {
             LOG_ERROR(dhcp4_logger, DHCP4_PACKET_RECEIVE_FAIL).arg(e.what());
         }
 
+        // Handle next signal received by the process. It must be called after
+        // an attempt to receive a packet to properly handle server shut down.
+        // The SIGTERM or SIGINT will be received prior to, or during execution
+        // of select() (select is invoked by recivePacket()). When that happens,
+        // select will be interrupted. The signal handler will be invoked
+        // immediately after select(). The handler will set the shutdown flag
+        // and cause the process to terminate before the next select() function
+        // is called. If the function was called before receivePacket the
+        // process could wait up to the duration of timeout of select() to
+        // terminate.
+        handleSignal();
+
         // Timeout may be reached or signal received, which breaks select()
         // with no reception ocurred
         if (!query) {

+ 0 - 3
src/bin/dhcp4/dhcp4_srv.h

@@ -83,13 +83,10 @@ public:
     /// root privileges.
     ///
     /// @param port specifies port number to listen on
-    /// @param dbconfig Lease manager configuration string.  The default
-    ///        of the "memfile" manager is used for testing.
     /// @param use_bcast configure sockets to support broadcast messages.
     /// @param direct_response_desired specifies if it is desired to
     /// use direct V4 traffic.
     Dhcpv4Srv(uint16_t port = DHCP4_SERVER_PORT,
-              const char* dbconfig = "type=memfile universe=4",
               const bool use_bcast = true,
               const bool direct_response_desired = true);
 

+ 80 - 20
src/bin/dhcp4/kea_controller.cc

@@ -25,11 +25,18 @@ using namespace isc::asiolink;
 using namespace isc::dhcp;
 using namespace std;
 
-namespace isc {
-namespace dhcp {
-
-void
-ControlledDhcpv4Srv::init(const std::string& file_name) {
+namespace {
+
+/// @brief Configure DHCPv4 server using the configuration file specified.
+///
+/// This function is used to both configure the DHCP server on its startup
+/// and dynamically reconfigure the server when SIGHUP signal is received.
+///
+/// It fetches DHCPv6 server's configuration from the 'Dhcp4' section of
+/// the JSON configuration file.
+///
+/// @param file_name Configuration file location.
+void configure(const std::string& file_name) {
     // This is a configuration backend implementation that reads the
     // configuration from a JSON file.
 
@@ -41,8 +48,8 @@ ControlledDhcpv4Srv::init(const std::string& file_name) {
     try {
         if (file_name.empty()) {
             // Basic sanity check: file name must not be empty.
-            isc_throw(BadValue, "JSON configuration file not specified. Please "
-                      "use -c command line option.");
+            isc_throw(isc::BadValue, "JSON configuration file not specified."
+                      " Please use -c command line option.");
         }
 
         // Read contents of the file and parse it as JSON
@@ -51,8 +58,8 @@ ControlledDhcpv4Srv::init(const std::string& file_name) {
         if (!json) {
             LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL)
                 .arg("Config file " + file_name + " missing or empty.");
-            isc_throw(BadValue, "Unable to process JSON configuration file:"
-                      + file_name);
+            isc_throw(isc::BadValue, "Unable to process JSON configuration"
+                      " file: " << file_name);
         }
 
         // Get Dhcp4 component from the config
@@ -60,29 +67,30 @@ ControlledDhcpv4Srv::init(const std::string& file_name) {
 
         if (!dhcp4) {
             LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL)
-                .arg("Config file " + file_name + " does not include 'Dhcp4' entry.");
-            isc_throw(BadValue, "Unable to process JSON configuration file:"
-                      + file_name);
+                .arg("Config file " + file_name + " does not include 'Dhcp4'"
+                     " entry.");
+            isc_throw(isc::BadValue, "Unable to process JSON configuration"
+                      " file: " << file_name);
         }
 
         // Use parsed JSON structures to configure the server
-        result = processCommand("config-reload", dhcp4);
+        result = ControlledDhcpv4Srv::processCommand("config-reload", dhcp4);
 
     }  catch (const std::exception& ex) {
         LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL).arg(ex.what());
-        isc_throw(BadValue, "Unable to process JSON configuration file:"
-                  + file_name);
+        isc_throw(isc::BadValue, "Unable to process JSON configuration file: "
+                  << file_name);
     }
 
     if (!result) {
         // Undetermined status of the configuration. This should never happen,
-        // but as the configureDhcp4Server returns a pointer, it is theoretically
-        // possible that it will return NULL.
+        // but as the configureDhcp4Server returns a pointer, it is
+        // theoretically possible that it will return NULL.
         LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL)
             .arg("Configuration failed: Undefined result of processCommand("
                  "config-reload, " + file_name + ")");
-        isc_throw(BadValue, "Configuration failed: Undefined result of "
-                  "processCommand('config-reload', " + file_name + ")");
+        isc_throw(isc::BadValue, "Configuration failed: Undefined result of "
+                  "processCommand('config-reload', " << file_name << ")");
     }
 
     // Now check is the returned result is successful (rcode=0) or not
@@ -95,12 +103,64 @@ ControlledDhcpv4Srv::init(const std::string& file_name) {
             reason = string(" (") + comment->stringValue() + string(")");
         }
         LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL).arg(reason);
-        isc_throw(BadValue, "Failed to apply configuration:" << reason);
+        isc_throw(isc::BadValue, "Failed to apply configuration: " << reason);
     }
+}
+
+/// @brief Signals handler for DHCPv4 server.
+///
+/// This signal handler handles the following signals received by the DHCPv4
+/// server process:
+/// - SIGHUP - triggers server's dynamic reconfiguration.
+/// - SIGTERM - triggers server's shut down.
+/// - SIGINT - triggers server's shut down.
+///
+/// @param signo Signal number received.
+void signalHandler(int signo) {
+    // SIGHUP signals a request to reconfigure the server.
+    if (signo == SIGHUP) {
+        // Get configuration file name.
+        std::string file = ControlledDhcpv4Srv::getInstance()->getConfigFile();
+        try {
+            LOG_INFO(dhcp4_logger, DHCP4_DYNAMIC_RECONFIGURATION).arg(file);
+            configure(file);
+        } catch (const std::exception& ex) {
+            // Log the unsuccessful reconfiguration. The reason for failure
+            // should be already logged. Don't rethrow an exception so as
+            // the server keeps working.
+            LOG_ERROR(dhcp4_logger, DHCP4_DYNAMIC_RECONFIGURATION_FAIL)
+                .arg(file);
+        }
+    } else if ((signo == SIGTERM) || (signo == SIGINT)) {
+        isc::data::ElementPtr params(new isc::data::MapElement());
+        ControlledDhcpv4Srv::processCommand("shutdown", params);
+    }
+}
+
+}
+
+namespace isc {
+namespace dhcp {
+
+void
+ControlledDhcpv4Srv::init(const std::string& file_name) {
+    // Call parent class's init to initialize file name.
+    Daemon::init(file_name);
+
+    // Configure the server using JSON file.
+    configure(file_name);
 
     // We don't need to call openActiveSockets() or startD2() as these
     // methods are called in processConfig() which is called by
     // processCommand("reload-config", ...)
+
+    // Set signal handlers. When the SIGHUP is received by the process
+    // the server reconfiguration will be triggered. When SIGTERM or
+    // SIGINT will be received, the server will start shutting down.
+    signal_set_.reset(new isc::util::SignalSet(SIGINT, SIGHUP, SIGTERM));
+    // Set the pointer to the handler function.
+    signal_handler_ = signalHandler;
+
 }
 
 void ControlledDhcpv4Srv::cleanup() {

+ 23 - 2
src/bin/dhcp4/tests/Makefile.am

@@ -1,7 +1,20 @@
 PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
 
 PYTESTS = dhcp4_test.py
-EXTRA_DIST = $(PYTESTS)
+SHTESTS =
+# The test of dynamic reconfiguration based on signals will work only
+# if we are using file based configuration approach.
+if CONFIG_BACKEND_JSON
+SHTESTS += dhcp4_reconfigure_test.sh
+SHTESTS += dhcp4_sigterm_test.sh
+SHTESTS += dhcp4_sigint_test.sh
+endif
+
+EXTRA_DIST  = $(PYTESTS)
+EXTRA_DIST += dhcp4_reconfigure_test.sh
+EXTRA_DIST += dhcp4_sigterm_test.sh
+EXTRA_DIST += dhcp4_sigint_test.sh
+EXTRA_DIST += dhcp4_shutdown_test.sh
 
 # Explicitly specify paths to dynamic libraries required by loadable python
 # modules. That is required on Mac OS systems. Otherwise we will get exception
@@ -21,6 +34,13 @@ check-local:
 		$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
 	done
 
+	for shtest in $(SHTESTS) ; do \
+	echo Running test: $$shtest ; \
+	export B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir); \
+	$(abs_srcdir)/$$shtest || exit ; \
+	done
+
+
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
 AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
 AM_CPPFLAGS += -I$(top_srcdir)/src/bin
@@ -33,7 +53,7 @@ AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
 
 CLEANFILES = $(builddir)/interfaces.txt $(builddir)/logger_lockfile
 CLEANFILES += $(builddir)/load_marker.txt $(builddir)/unload_marker.txt
-CLEANFILES += *.json
+CLEANFILES += *.json *.log
 
 AM_CXXFLAGS = $(B10_CXXFLAGS)
 if USE_CLANGPP
@@ -121,6 +141,7 @@ dhcp4_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/util/io/libkea-util-io.la
 endif
 
 noinst_EXTRA_DIST = configs-list.txt

+ 1 - 1
src/bin/dhcp4/tests/d2_unittest.h

@@ -36,7 +36,7 @@ public:
 
     /// @brief Constructor
     D2Dhcpv4Srv()
-        : Dhcpv4Srv(0, "type=memfile", false, false), error_count_(0) {
+        : Dhcpv4Srv(0, false, false), error_count_(0) {
     }
 
     /// @brief virtual Destructor.

+ 163 - 0
src/bin/dhcp4/tests/dhcp4_reconfigure_test.sh

@@ -0,0 +1,163 @@
+# Copyright (C) 2014 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.
+
+# Test name
+TEST_NAME="DHCPv4.dynamicReconfiguration"
+# Path to the temporary configuration file.
+CFG_FILE="test_config.json"
+# Path to the Kea log file.
+LOG_FILE="test.log"
+# Kea configuration to be stored in the configuration file.
+CONFIG="{
+    \"Dhcp4\":
+    {
+        \"interfaces\": [ ],
+        \"valid-lifetime\": 4000,
+        \"renew-timer\": 1000,
+        \"rebind-timer\": 2000,
+        \"lease-database\":
+        {
+            \"type\": \"memfile\",
+            \"persist\": false
+        },
+        \"subnet4\": [
+        {
+            \"subnet\": \"10.0.0.0/8\",
+            \"pool\": [ \"10.0.0.10-10.0.0.100\" ]
+        } ]
+    }
+}"
+# Invalid configuration (negative valid-lifetime) to check that Kea
+# gracefully handles reconfiguration errors.
+CONFIG_INVALID="{
+    \"Dhcp4\":
+    {
+        \"interfaces\": [ ],
+        \"valid-lifetime\": -3,
+        \"renew-timer\": 1000,
+        \"rebind-timer\": 2000,
+        \"lease-database\":
+        {
+            \"type\": \"memfile\",
+            \"persist\": false
+        },
+        \"subnet4\": [
+        {
+            \"subnet\": \"10.0.0.0/8\",
+            \"pool\": [ \"10.0.0.10-10.0.0.100\" ]
+        } ]
+    }
+}"
+
+# Set the location of the executable.
+BIN="b10-dhcp4"
+BIN_PATH=".."
+
+# Import common test library.
+. $(dirname $0)/../../../lib/testutils/dhcp_test_lib.sh
+
+# Log the start of the test and print test name.
+test_start
+# Remove dangling Kea instances and remove log files.
+cleanup
+# Create new configuration file.
+create_config "${CONFIG}"
+# Instruct Kea to log to the specific file.
+set_logger
+# Start Kea.
+start_kea
+# 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
+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 in the log file, how many times server has been configured. It should
+# be just once on startup.
+get_reconfigs
+if [ ${_GET_RECONFIGS} -ne 1 ]; then
+    printf "ERROR: server hasn't been configured.\n"
+    clean_exit 1
+else
+    printf "Server successfully configured.\n"
+fi
+
+# Now use invalid configuration.
+create_config "${CONFIG_INVALID}"
+
+# Try to reconfigure by sending SIGHUP
+send_signal 1
+
+# The configuration should fail and the error message should be there.
+wait_for_message 10 "DHCP4_CONFIG_LOAD_FAIL" 1
+
+# After receiving SIGHUP the server should try to reconfigure itself.
+# The configuration provided is invalid so it should result in
+# reconfiguration failure but the server should still be running.
+get_reconfigs
+if [ ${_GET_RECONFIGS} -ne 1 ]; then
+    printf "ERROR: server has been reconfigured despite bogus configuration.\n"
+    clean_exit 1
+elif [ ${_GET_RECONFIG_ERRORS} -ne 1 ]; then
+    printf "ERROR: server did not report reconfiguration error despite attempt\
+ to configure it with invalid configuration.\n"
+    clean_exit 1
+fi
+
+# Make sure the server is still operational.
+get_pids
+if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+    printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
+    clean_exit 1
+fi
+
+# Restore the good configuration.
+create_config "${CONFIG}"
+
+# Reconfigure the server with SIGHUP.
+send_signal 1
+
+# 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
+if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+    printf "ERROR: Kea process was killed when attempting reconfiguration.\n"
+    clean_exit 1
+fi
+
+# All ok. Shut down Kea and exit.
+clean_exit 0

+ 111 - 0
src/bin/dhcp4/tests/dhcp4_shutdown_test.sh

@@ -0,0 +1,111 @@
+# Copyright (C) 2014 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.
+
+if [ $# -ne 2 ]; then
+    printf "USAGE: dhcp4_shutdown_test.sh <test_name> <signal_num>\n"
+    exit 1
+fi
+
+# Test name
+TEST_NAME=$1
+# Signal number to be used for this test.
+SIG_NUM=$2
+# Path to the temporary configuration file.
+CFG_FILE="test_config.json"
+# Path to the Kea log file.
+LOG_FILE="test.log"
+# Kea configuration to be stored in the configuration file.
+CONFIG="{
+    \"Dhcp4\":
+    {
+        \"interfaces\": [ ],
+        \"valid-lifetime\": 4000,
+        \"renew-timer\": 1000,
+        \"rebind-timer\": 2000,
+        \"lease-database\":
+        {
+            \"type\": \"memfile\",
+            \"persist\": false
+        },
+        \"subnet4\": [
+        {
+            \"subnet\": \"10.0.0.0/8\",
+            \"pool\": [ \"10.0.0.10-10.0.0.100\" ]
+        } ]
+    }
+}"
+
+# Set the location of the executable.
+BIN="b10-dhcp4"
+BIN_PATH=".."
+
+# Import common test library.
+. $(dirname $0)/../../../lib/testutils/dhcp_test_lib.sh
+
+# Log the start of the test and print test name.
+test_start
+# Remove dangling Kea instances and remove log files.
+cleanup
+# Create new configuration file.
+create_config "${CONFIG}"
+# Instruct Kea to log to the specific file.
+set_logger
+# Start Kea.
+start_kea
+# 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
+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 in the log file, how many times server has been configured. It should
+# be just once on startup.
+get_reconfigs
+if [ ${_GET_RECONFIGS} -ne 1 ]; then
+    printf "ERROR: server hasn't been configured.\n"
+    clean_exit 1
+else
+    printf "Server successfully configured.\n"
+fi
+
+# Send signal to Kea (SIGTERM, SIGINT etc.)
+send_signal ${SIG_NUM}
+
+# 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
+
+# Server should have shut down.
+get_pids
+if [ ${_GET_PIDS_NUM} -ne 0 ]; then
+    printf "ERROR: Kea did not shut down after receiving signal.\n"\
+ ${_GET_PIDS_NUM}
+    clean_exit 1
+fi
+
+clean_exit 0

+ 16 - 0
src/bin/dhcp4/tests/dhcp4_sigint_test.sh

@@ -0,0 +1,16 @@
+# Copyright (C) 2014 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.
+
+# Run a test that sends SIGINT to Kea and checks if it shuts down gracefully.
+$(dirname $0)/dhcp4_shutdown_test.sh "DHCPv4.sigint" 2

+ 16 - 0
src/bin/dhcp4/tests/dhcp4_sigterm_test.sh

@@ -0,0 +1,16 @@
+# Copyright (C) 2014 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.
+
+# Run a test that sends SIGTERM to Kea and checks if it shuts down gracefully.
+$(dirname $0)/dhcp4_shutdown_test.sh "DHCPv4.sigterm" 15

+ 2 - 2
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -401,8 +401,8 @@ TEST_F(Dhcpv4SrvTest, basic) {
 
     // Check that the base class can be instantiated
     boost::scoped_ptr<Dhcpv4Srv> srv;
-    ASSERT_NO_THROW(srv.reset(new Dhcpv4Srv(DHCP4_SERVER_PORT + 10000, "type=memfile",
-                                            false, false)));
+    ASSERT_NO_THROW(srv.reset(new Dhcpv4Srv(DHCP4_SERVER_PORT + 10000, false,
+                                            false)));
     srv.reset();
     // We have to close open sockets because further in this test we will
     // call the Dhcpv4Srv constructor again. This constructor will try to

+ 5 - 2
src/bin/dhcp4/tests/dhcp4_test_utils.h

@@ -27,6 +27,7 @@
 #include <dhcp/pkt_filter_inet.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcp4/dhcp4_srv.h>
 #include <asiolink/io_address.h>
 #include <config/ccsession.h>
@@ -117,8 +118,10 @@ public:
     /// @param port port number to listen on; the default value 0 indicates
     /// that sockets should not be opened.
     NakedDhcpv4Srv(uint16_t port = 0)
-        : Dhcpv4Srv(port, "type=memfile universe=4 persist=false",
-                    false, false) {
+        : Dhcpv4Srv(port, false, false) {
+        // Create a default lease database backend.
+        std::string dbconfig = "type=memfile universe=4 persist=false";
+        isc::dhcp::LeaseMgrFactory::create(dbconfig);
         // Create fixed server id.
         server_id_.reset(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER,
                                             asiolink::IOAddress("192.0.3.1")));

+ 14 - 15
src/bin/dhcp6/dhcp6.dox

@@ -279,21 +279,20 @@ the SIGHUP signal, the server will re-read the configuration file specified at s
 If the reconfiguration fails, the server will continue to run and use the last good
 configuration.
 
-The SIGHUP signal handler is defined in the kea_controller.cc. The handler calls the
-same function to reconfigure the server which is called to configure it at startup.
-The signal handler catches exceptions emitted during reconfiguration so as the
-uncaught exceptions don't cause the process to exit.
-
-Signal handlers are static and therefore they must call static functions. The
-@c ControlledDhcpv6Srv::processCommand which performs the actual server
-reconfiguration is static, so it can be called from the signal handler. In order
-for the signal handler to know the location of the configuration file (specified
-at process startup), the location of this file needs to be stored in a static
-variable so as it may be directly accessed by the signal handler. This static
-variable is stored in the @c dhcp::Daemon class and all Kea processes can use
-it (all processes derive from this class). The configuration file location is
-initialized when the @c Daemon::init method is called. Therefore, derived
-classes should call it in their implementations of the @c init method.
+The signal handler for SIGHUP (also for SIGTERM and SIGINT) are installed in the
+kea_controller.cc using the @c isc::util::SignalSet class. The
+@c isc::dhcp::Dhcp6Srv calls @c isc::dhcp::Daemon::handleSignal on each pass
+through the main loop. This method fetches the last received signal and calls
+a handler function defined in the kea_controller.cc. The handler function
+calls a static function @c configure defined in the kea_controller.cc.
+
+In order for the signal handler to know the location of the configuration file
+(specified at process startup), the location of this file needs to be stored
+in a static variable so as it may be directly accessed by the signal handler.
+This static variable is stored in the @c dhcp::Daemon class and all Kea processes
+can use it (all processes derive from this class). The configuration file
+location is initialized when the @c Daemon::init method is called. Therefore,
+derived classes should call it in their implementations of the @c init method.
 
  @section dhcpv6Other Other DHCPv6 topics
 

+ 12 - 0
src/bin/dhcp6/dhcp6_srv.cc

@@ -255,6 +255,18 @@ bool Dhcpv6Srv::run() {
             LOG_ERROR(dhcp6_logger, DHCP6_PACKET_RECEIVE_FAIL).arg(e.what());
         }
 
+        // Handle next signal received by the process. It must be called after
+        // an attempt to receive a packet to properly handle server shut down.
+        // The SIGTERM or SIGINT will be received prior to, or during execution
+        // of select() (select is invoked by recivePacket()). When that happens,
+        // select will be interrupted. The signal handler will be invoked
+        // immediately after select(). The handler will set the shutdown flag
+        // and cause the process to terminate before the next select() function
+        // is called. If the function was called before receivePacket the
+        // process could wait up to the duration of timeout of select() to
+        // terminate.
+        handleSignal();
+
         // Timeout may be reached or signal received, which breaks select()
         // with no packet received
         if (!query) {

+ 3 - 3
src/bin/dhcp6/kea_controller.cc

@@ -160,9 +160,9 @@ ControlledDhcpv6Srv::init(const std::string& file_name) {
     // Set signal handlers. When the SIGHUP is received by the process
     // the server reconfiguration will be triggered. When SIGTERM or
     // SIGINT will be received, the server will start shutting down.
-    signal(SIGHUP, signalHandler);
-    signal(SIGTERM, signalHandler);
-    signal(SIGINT, signalHandler);
+    signal_set_.reset(new isc::util::SignalSet(SIGINT, SIGHUP, SIGTERM));
+    // Set the pointer to the handler function.
+    signal_handler_ = signalHandler;
 }
 
 void ControlledDhcpv6Srv::cleanup() {

+ 7 - 1
src/bin/dhcp6/tests/Makefile.am

@@ -8,7 +8,12 @@ SHTESTS += dhcp6_reconfigure_test.sh
 SHTESTS += dhcp6_sigterm_test.sh
 SHTESTS += dhcp6_sigint_test.sh
 endif
-EXTRA_DIST = $(PYTESTS) $(SHTESTS)
+
+EXTRA_DIST = $(PYTESTS)
+EXTRA_DIST += dhcp6_reconfigure_test.sh
+EXTRA_DIST += dhcp6_sigterm_test.sh
+EXTRA_DIST += dhcp6_sigint_test.sh
+EXTRA_DIST += dhcp6_shutdown_test.sh
 
 # Explicitly specify paths to dynamic libraries required by loadable python
 # modules. That is required on Mac OS systems. Otherwise we will get exception
@@ -135,6 +140,7 @@ dhcp6_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la
+dhcp6_unittests_LDADD += $(top_builddir)/src/lib/util/io/libkea-util-io.la
 endif
 
 noinst_PROGRAMS = $(TESTS)

+ 5 - 4
src/bin/dhcp6/tests/dhcp6_reconfigure_test.sh

@@ -13,7 +13,7 @@
 # PERFORMANCE OF THIS SOFTWARE.
 
 # Test name
-TEST_NAME="DynamicReconfiguration"
+TEST_NAME="DHCPv6.dynamicReconfiguration"
 # Path to the temporary configuration file.
 CFG_FILE="test_config.json"
 # Path to the Kea log file.
@@ -90,7 +90,8 @@ fi
 # of configuration failure).
 get_pids
 if [ ${_GET_PIDS_NUM} -ne 1 ]; then
-    printf "ERROR: expected one Kea process to be started. Found %d processes started.\n" ${_GET_PIDS_NUM}
+    printf "ERROR: expected one Kea process to be started. Found %d processes\
+ started.\n" ${_GET_PIDS_NUM}
     clean_exit 1
 fi
 
@@ -121,8 +122,8 @@ if [ ${_GET_RECONFIGS} -ne 1 ]; then
     printf "ERROR: server has been reconfigured despite bogus configuration.\n"
     clean_exit 1
 elif [ ${_GET_RECONFIG_ERRORS} -ne 1 ]; then
-    printf "ERROR: server did not report reconfiguration error despite attempt" \
-        " to configure it with invalid configuration.\n"
+    printf "ERROR: server did not report reconfiguration error despite attempt\
+ to configure it with invalid configuration.\n"
     clean_exit 1
 fi
 

+ 4 - 2
src/bin/dhcp6/tests/dhcp6_shutdown_test.sh

@@ -75,7 +75,8 @@ fi
 # of configuration failure).
 get_pids
 if [ ${_GET_PIDS_NUM} -ne 1 ]; then
-    printf "ERROR: expected one Kea process to be started. Found %d processes started.\n" ${_GET_PIDS_NUM}
+    printf "ERROR: expected one Kea process to be started. Found %d processes\
+ started.\n" ${_GET_PIDS_NUM}
     clean_exit 1
 fi
 
@@ -103,7 +104,8 @@ fi
 # Server should have shut down.
 get_pids
 if [ ${_GET_PIDS_NUM} -ne 0 ]; then
-    printf "ERROR: Kea did not shut down after receiving signal.\n" ${_GET_PIDS_NUM}
+    printf "ERROR: Kea did not shut down after receiving signal.\n"\
+ ${_GET_PIDS_NUM}
     clean_exit 1
 fi
 

+ 1 - 1
src/bin/dhcp6/tests/dhcp6_sigint_test.sh

@@ -13,4 +13,4 @@
 # PERFORMANCE OF THIS SOFTWARE.
 
 # Run a test that sends SIGINT to Kea and checks if it shuts down gracefully.
-$(dirname $0)/dhcp6_shutdown_test.sh "Sigint" 2
+$(dirname $0)/dhcp6_shutdown_test.sh "DHCPv6.sigint" 2

+ 1 - 1
src/bin/dhcp6/tests/dhcp6_sigterm_test.sh

@@ -13,4 +13,4 @@
 # PERFORMANCE OF THIS SOFTWARE.
 
 # Run a test that sends SIGTERM to Kea and checks if it shuts down gracefully.
-$(dirname $0)/dhcp6_shutdown_test.sh "Sigterm" 15
+$(dirname $0)/dhcp6_shutdown_test.sh "DHCPv6.sigterm" 15

+ 11 - 2
src/lib/dhcpsrv/daemon.cc

@@ -15,6 +15,7 @@
 #include <config.h>
 #include <dhcpsrv/daemon.h>
 #include <exceptions/exceptions.h>
+#include <boost/bind.hpp>
 #include <errno.h>
 
 /// @brief provides default implementation for basic daemon operations
@@ -27,7 +28,11 @@ namespace dhcp {
 // This is an initial config file location.
 std::string Daemon::config_file_ = "";
 
-Daemon::Daemon() {
+Daemon::Daemon()
+    : signal_set_(), signal_handler_() {
+}
+
+Daemon::~Daemon() {
 }
 
 void Daemon::init(const std::string& config_file) {
@@ -42,8 +47,12 @@ void Daemon::shutdown() {
 
 }
 
-Daemon::~Daemon() {
+void Daemon::handleSignal() {
+    if (signal_set_ && signal_handler_) {
+        signal_set_->handleNext(boost::bind(signal_handler_, _1));
+    }
 }
 
+
 };
 };

+ 43 - 8
src/lib/dhcpsrv/daemon.h

@@ -13,8 +13,9 @@
 // PERFORMANCE OF THIS SOFTWARE.
 
 #include <config.h>
-#include <string>
+#include <util/signal_set.h>
 #include <boost/noncopyable.hpp>
+#include <string>
 
 
 namespace isc {
@@ -44,6 +45,11 @@ namespace dhcp {
 /// By default, the configuration file location is empty and its actual value
 /// is assigned to the static object in @c Daemon::init function.
 ///
+/// Classes derived from @c Daemon may install custom signal handlers using
+/// @c isc::util::SignalSet class. This base class provides a declaration
+/// of the @c SignalSet object that should be initialized in the derived
+/// classes to install the custom exception handlers.
+///
 /// @note Only one instance of this class is instantiated as it encompasses
 ///       the whole operation of the server.  Nothing, however, enforces the
 ///       singleton status of the object.
@@ -52,9 +58,16 @@ class Daemon : public boost::noncopyable {
 public:
     /// @brief Default constructor
     ///
-    /// Currently it does nothing.
+    /// Initializes the object installing custom signal handlers for the
+    /// process to NULL.
     Daemon();
 
+    /// @brief Desctructor
+    ///
+    /// Having virtual destructor ensures that all derived classes will have
+    /// virtual destructor as well.
+    virtual ~Daemon();
+
     /// @brief Initializes the server.
     ///
     /// Depending on the configuration backend, it establishes msgq session,
@@ -87,12 +100,6 @@ public:
     /// @brief Initiates shutdown procedure for the whole DHCPv6 server.
     virtual void shutdown();
 
-    /// @brief Desctructor
-    ///
-    /// Having virtual destructor ensures that all derived classes will have
-    /// virtual destructor as well.
-    virtual ~Daemon();
-
     /// @brief Returns config file name.
     static std::string getConfigFile() {
         return (config_file_);
@@ -107,6 +114,34 @@ public:
     /// @param verbose verbose mode (true usually enables DEBUG messages)
     static void loggerInit(const char* log_name, bool verbose);
 
+protected:
+
+    /// @brief Invokes handler for the next received signal.
+    ///
+    /// This function provides a default implementation for the function
+    /// handling next signal received by the process. It checks if a pointer
+    /// to @c isc::util::SignalSet object and the signal handler function
+    /// have been set. If they have been set, the signal handler is invoked for
+    /// the the next signal registered in the @c SignalSet object.
+    ///
+    /// This function should be received in the main loop of the process.
+    virtual void handleSignal();
+
+    /// @brief A pointer to the object installing custom signal handlers.
+    ///
+    /// This pointer needs to be initialized to point to the @c SignalSet
+    /// object in the derived classes which need to handle signals received
+    /// by the process.
+    isc::util::SignalSetPtr signal_set_;
+
+    /// @brief Pointer to the common signal handler invoked by the handleSignal
+    /// function.
+    ///
+    /// This pointer needs to be initialized to point to the signal handler
+    /// function for signals being handled by the process. If signal handler
+    /// it not initialized, the signals will not be handled.
+    isc::util::SignalHandler signal_handler_;
+
 private:
 
     /// @brief Config file name or empty if config file not used.

+ 4 - 3
src/lib/testutils/dhcp_test_lib.sh

@@ -63,10 +63,10 @@ _GET_RECONFIG_ERRORS=  # Return value: number of configuration errors.
 get_reconfigs() {
     # Grep log file for DHCP6_CONFIG_COMPLETE occurences. There should
     # be one occurence per (re)configuration.
-    _GET_RECONFIGS=`grep -o DHCP6_CONFIG_COMPLETE ${LOG_FILE} | wc -w`
+    _GET_RECONFIGS=`grep -o CONFIG_COMPLETE ${LOG_FILE} | wc -w`
     # Grep log file for DHCP6_CONFIG_LOAD_FAIL to check for configuration
     # failures.
-    _GET_RECONFIG_ERRORS=`grep -o DHCP6_CONFIG_LOAD_FAIL ${LOG_FILE} | wc -w`
+    _GET_RECONFIG_ERRORS=`grep -o CONFIG_LOAD_FAIL ${LOG_FILE} | wc -w`
     # Remove whitespaces
     ${_GET_RECONFIGS##*[! ]}
     ${_GET_RECONFIG_ERRORS##*[! ]}
@@ -182,7 +182,8 @@ send_signal() {
     # Get Kea pid.
     get_pids
     if [ ${_GET_PIDS_NUM} -ne 1 ]; then
-        printf "ERROR: expected one Kea process to be started. Found %d processes started.\n" ${_GET_PIDS_NUM}
+        printf "ERROR: expected one Kea process to be started.\
+ Found %d processes started.\n" ${_GET_PIDS_NUM}
         clean_exit 1
     fi
     printf "Sending signal ${sig} to Kea process (pid=%s).\n" ${_GET_PIDS}

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

@@ -31,6 +31,7 @@ if USE_SHARED_MEMORY
 libkea_util_la_SOURCES += memory_segment_mapped.h memory_segment_mapped.cc
 endif
 libkea_util_la_SOURCES += range_utilities.h
+libkea_util_la_SOURCES += signal_set.cc signal_set.h
 libkea_util_la_SOURCES += hash/sha1.h hash/sha1.cc
 libkea_util_la_SOURCES += encode/base16_from_binary.h
 libkea_util_la_SOURCES += encode/base32hex.h encode/base64.h

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

@@ -1,3 +1,4 @@
+SUBDIRS = .
 AM_CXXFLAGS = $(B10_CXXFLAGS)
 
 AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib

+ 229 - 0
src/lib/util/signal_set.cc

@@ -0,0 +1,229 @@
+// Copyright (C) 2014 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 <util/signal_set.h>
+
+#include <cerrno>
+#include <list>
+
+using namespace isc;
+using namespace isc::util;
+
+namespace {
+
+/// @brief Returns a pointer to the global set of registered signals.
+///
+/// Multiple instances of @c SignalSet may use this pointer to access
+/// and update the set.
+///
+/// @return Pointer to the global set of registered signals. This pointer
+/// is always initialized and points to a valid object.
+std::set<int>* getRegisteredSignals() {
+    static std::set<int> registered_signals;
+    return (&registered_signals);
+}
+
+/// @brief Returns a pointer to static collection of signals received.
+///
+/// Multiple instances of @c SignalSet may use this pointer to access
+/// and update the queue of signals received.
+///
+/// @return Static collection of signals received. This pointer is always
+/// initialized and points to a valid object.
+std::list<int>* getSignalStates() {
+    static std::list<int> states;
+    return (&states);
+}
+
+/// @brief Internal signal handler for @c isc::util::io::SignalSet class.
+///
+/// This signal handler adds a signal number for which it is being
+/// invoked to the queue of received signals. It prevents adding duplicated
+/// signals. All duplicated signals are dropped. This prevents hammering
+/// a process to invoke handlers (e.g. DHCP server reconfiguration), when
+/// many the same signals are received one after another.
+///
+/// @param sig Signal number.
+void internalHandler(int sig) {
+    std::list<int>* states = getSignalStates();
+    for (std::list<int>::const_iterator it = states->begin();
+         it != states->end(); ++it) {
+        if (sig == *it) {
+            return;
+        }
+    }
+    states->push_back(sig);
+}
+
+}
+
+namespace isc {
+namespace util {
+
+SignalSet::SignalSet(const int sig0) {
+    add(sig0);
+}
+
+SignalSet::SignalSet(const int sig0, const int sig1) {
+    add(sig0);
+    add(sig1);
+}
+
+SignalSet::SignalSet(const int sig0, const int sig1, const int sig2) {
+    add(sig0);
+    add(sig1);
+    add(sig2);
+}
+
+SignalSet::~SignalSet() {
+    // Set default signal handlers.
+    try {
+        clear();
+    } catch (...) {
+        // Not a good thing to throw from a destructor. in fact this should
+        // not throw an exception because we just unregister the signals
+        // that we have previously registered. So the signal codes are fine.
+    }
+}
+
+void
+SignalSet::add(const int sig) {
+    insert(sig);
+    struct sigaction sa;
+    memset(&sa, 0, sizeof(sa));
+    sa.sa_handler = internalHandler;
+    sigfillset(&sa.sa_mask);
+    if (sigaction(sig, &sa, 0) < 0) {
+        erase(sig);
+        isc_throw(SignalSetError, "failed to register a signal handler for"
+                  " signal " << sig << ": " << strerror(errno));
+    }
+}
+
+void
+SignalSet::clear() {
+    // Iterate over a copy of the registered signal set because the
+    // remove function is erasing the elements and we don't want to
+    // erase the elements we are iterating over. This would cause
+    // a segfault.
+    std::set<int> all_signals = local_signals_;
+    for (std::set<int>::const_iterator it = all_signals.begin();
+         it != all_signals.end(); ++it) {
+        remove(*it);
+    }
+}
+
+int
+SignalSet::getNext() const {
+    std::list<int>* states = getSignalStates();
+    for (std::list<int>::iterator it = states->begin();
+         it != states->end(); ++it) {
+        if (local_signals_.find(*it) != local_signals_.end()) {
+            return (*it);
+        }
+    }
+    return (-1);
+}
+
+void
+SignalSet::erase(const int sig) {
+    if (local_signals_.find(sig) == local_signals_.end()) {
+        isc_throw(SignalSetError, "failed to unregister signal " << sig
+                  << " from a signal set: signal is not owned by the"
+                  " signal set");
+    }
+    // Remove globally registered signal.
+    getRegisteredSignals()->erase(sig);
+    // Remove unhandled signals from the queue.
+    for (std::list<int>::iterator it = getSignalStates()->begin();
+         it != getSignalStates()->end(); ++it) {
+        if (*it == sig) {
+            it = getSignalStates()->erase(it);
+        }
+    }
+    // Remove locally registered signal.
+    local_signals_.erase(sig);
+}
+
+void
+SignalSet::handleNext(SignalHandler signal_handler) {
+    block();
+    int signum = getNext();
+    if (signum >= 0) {
+        popNext();
+        try {
+            signal_handler(signum);
+        } catch (...) {
+            unblock();
+            throw;
+        }
+    }
+    unblock();
+}
+
+void
+SignalSet::insert(const int sig) {
+    std::set<int>* global_signals = getRegisteredSignals();
+    if ((global_signals->find(sig) != global_signals->end()) ||
+        (local_signals_.find(sig) != local_signals_.end())) {
+        isc_throw(SignalSetError, "attempt to register a duplicate signal "
+                  << sig);
+    }
+    global_signals->insert(sig);
+    local_signals_.insert(sig);
+}
+
+void
+SignalSet::maskSignals(const int mask) const {
+    sigset_t new_set;
+    for (std::set<int>::const_iterator it = getRegisteredSignals()->begin();
+         it != getRegisteredSignals()->end(); ++it) {
+        sigaddset(&new_set, *it);
+    }
+    sigprocmask(mask, &new_set, 0);
+}
+
+void
+SignalSet::popNext() {
+    std::list<int>* states = getSignalStates();
+    for (std::list<int>::iterator it = states->begin();
+         it != states->end(); ++it) {
+        if (local_signals_.find(*it) != local_signals_.end()) {
+            states->erase(it);
+            return;
+        }
+    }
+}
+
+void
+SignalSet::remove(const int sig) {
+    // Unregister only if we own this signal.
+    if (local_signals_.find(sig) != local_signals_.end()) {
+        struct sigaction sa;
+        memset(&sa, 0, sizeof(sa));
+        sa.sa_handler = SIG_DFL;
+        sigfillset(&sa.sa_mask);
+        if (sigaction(sig, &sa, 0) < 0) {
+            isc_throw(SignalSetError, "unable to restore original signal"
+                      " handler for signal: " << sig);
+        }
+        erase(sig);
+    } else {
+        isc_throw(SignalSetError, "failed to unregister signal " << sig
+                  << ": this signal is not owned by the signal set");
+    }
+}
+
+} // end of isc::util
+} // end of isc

+ 187 - 0
src/lib/util/signal_set.h

@@ -0,0 +1,187 @@
+// Copyright (C) 2014 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 SIGNAL_SET_H
+#define SIGNAL_SET_H
+
+#include <exceptions/exceptions.h>
+#include <boost/function.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+#include <set>
+#include <signal.h>
+
+namespace isc {
+namespace util {
+
+/// @brief Exception thrown when the @c isc::util::io::SignalSet class
+/// experiences an error.
+class SignalSetError : public Exception {
+public:
+    SignalSetError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Forward declaration to the @c isc::util::io::SignalSet.
+class SignalSet;
+/// @brief Pointer to the @c isc::util::io::SignalSet.
+typedef boost::shared_ptr<SignalSet> SignalSetPtr;
+/// @brief Pointer to the signal handling function.
+typedef boost::function<void(int signum)> SignalHandler;
+
+/// @brief Represents a collection of signals handled in a customized way.
+///
+/// Kea processes must handle selected signals in a specialized way. For
+/// example: SIGINT and SIGTERM must perform a graceful shut down of the
+/// server. The SIGHUP signal is used to trigger server's reconfiguration.
+///
+/// This class allows specifying signals which should be handled in a
+/// specialized way as well as specifying a signal handler function.
+/// When a signal is received the signal handler function is called and
+/// the code of the received signal is recorded. This function doesn't
+/// do anything beyond recording the signal number to minimize the time
+/// spent on handling the signal and process interruption. The process
+/// can later check the signals received and call the handlers on its
+/// descretion by calling a @c isc::util::io::SignalSet::handleNext function.
+///
+/// @note This class is not thread safe. It uses static variables and
+/// functions to track a global state of signal registration and received
+/// signals' queue.
+class SignalSet : public boost::noncopyable {
+public:
+
+    /// @brief Constructor installing one signal.
+    ///
+    /// @param sig0 First signal.
+    /// @throw SignalSetError If attempting to add duplicated signal or
+    /// the signal is invalid.
+    SignalSet(const int sig0);
+
+    /// @brief Constructor installing two signals.
+    ///
+    /// @param sig0 First signal.
+    /// @param sig1 Second signal.
+    /// @throw SignalSetError If attempting to add duplicated signal or
+    /// the signal is invalid.
+    SignalSet(const int sig0, const int sig1);
+
+    /// @brief Constructor installing three signals.
+    ///
+    /// @param sig0 First signal.
+    /// @param sig1 Second signal.
+    /// @param sig2 Third signal.
+    /// @throw SignalSetError If attempting to add duplicated signal or
+    /// the signal is invalid.
+    SignalSet(const int sig0, const int sig1, const int sig2);
+
+    /// @brief Destructor.
+    ///
+    /// Removes installed handlers.
+    ~SignalSet();
+
+    /// @brief Installs the handler for the specified signal.
+    ///
+    /// This function adds a signal to the set. When the signal is received
+    /// by the process, it will be recorded and a signal can be later handled
+    /// by the process.
+    ///
+    /// @param sig Signal code.
+    /// @throw SignalSetError if signal being added duplicates an existing
+    /// signal.
+    void add(const int sig);
+
+    /// @brief Uninstalls all signals.
+    ///
+    /// This function calls @c isc::util::io::SignalSet::remove for each
+    /// installed signal.
+    void clear();
+
+    /// @brief Returns a code of the next received signal.
+    ///
+    /// @return A code of the next received signal or -1 if there are no
+    /// more signals received.
+    int getNext() const;
+
+    /// @brief Calls a handler for the next received signal.
+    ///
+    /// This function handles the next received signal and removes it from the
+    /// queue of received signals. While the function is executed, all custom
+    /// signal handlers are blocked to prevent race condition.
+    ///
+    /// @param signal_handler A pointer to the signal handler function to
+    /// be used to handle the signal.
+    void handleNext(SignalHandler signal_handler);
+
+    /// @brief Uninstalls signal handler for a specified signal.
+    ///
+    /// @param sig A code of the signal to be removed.
+    void remove(const int sig);
+
+private:
+
+    /// @brief Blocks signals in the set.
+    ///
+    /// This function blocks the signals in a set to prevent race condition
+    /// between the signal handler and the new signal coming in.
+    void block() const {
+        maskSignals(SIG_BLOCK);
+    }
+
+    /// @brief Removes the signal from the set.
+    ///
+    /// This function removes only a signal which is owned by this signal set.
+    ///
+    /// @param sig Signal to be removed.
+    /// @throw SignalSetError if the signal being removed is not owned by this
+    /// signal set.
+    void erase(const int sig);
+
+    /// @brief Insert a signal to the set.
+    ///
+    /// @param sig Signal to be inserted.
+    /// @throw SignalSetError if a signal being inserted has already been
+    /// registered in this or other signal set.
+    void insert(const int sig);
+
+    /// @brief Applies a mask to all signals in the set.
+    ///
+    /// This function is used by @c SignalSet::block and @c SignalSet::unblock
+    /// to apply the SIG_BLOCK and SIG_UNBLOCK mask to signals.
+    ///
+    /// @param mask A mask to be applied to all signals.
+    void maskSignals(const int mask) const;
+
+    /// @brief Pops a next signal number from the static collection of signals.
+    ///
+    /// The static collection of signals is updated by the internal signal
+    /// handler being invoked when one of the installed signals is received by
+    /// the process. This function removes the first element of the collection.
+    void popNext();
+
+    /// @brief Unblocks signals in the set.
+    ///
+    /// This function unblocks the signals in a set.
+    void unblock() const {
+        maskSignals(SIG_UNBLOCK);
+    }
+
+    /// @brief Stores the set of signals registered in this signal set.
+    std::set<int> local_signals_;
+};
+
+}
+}
+
+#endif // SIGNAL_SET_H
+

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

@@ -47,6 +47,7 @@ run_unittests_SOURCES += socketsession_unittest.cc
 run_unittests_SOURCES += strutil_unittest.cc
 run_unittests_SOURCES += time_utilities_unittest.cc
 run_unittests_SOURCES += range_utilities_unittest.cc
+run_unittests_SOURCES += signal_set_unittest.cc
 
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)

+ 188 - 0
src/lib/util/tests/signal_set_unittest.cc

@@ -0,0 +1,188 @@
+// Copyright (C) 2014 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 <util/signal_set.h>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+#include <signal.h>
+
+namespace {
+
+using namespace isc;
+using namespace isc::util;
+
+/// @brief Test fixture class for @c isc::util::SignalSet class.
+///
+/// This class contains a handler function which records the signal
+/// being handled. It allows for checking whether the signal set
+/// has invoked the handler for the expected signal.
+class SignalSetTest : public ::testing::Test {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Resets the signal sets and variables being modified by the
+    /// signal handler function.
+    SignalSetTest()
+        : signal_set_(),
+          secondary_signal_set_() {
+        handler_calls_ = 0;
+        signum_ = -1;
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Uninstalls the signals from the signal sets.
+    ~SignalSetTest() {
+        if (signal_set_) {
+            signal_set_->clear();
+        }
+        if (secondary_signal_set_) {
+            secondary_signal_set_->clear();
+        }
+    }
+
+    /// @brief Signal handler used by unit tests.
+    ///
+    /// @param signum Signal being handled.
+    static void testHandler(int signum) {
+        signum_ = signum;
+        ++handler_calls_;
+    }
+
+    /// @brief Number of handler calls so far.
+    static int handler_calls_;
+    /// @brief The last signal handled.
+    static int signum_;
+    /// @brief Test signal set object.
+    SignalSetPtr signal_set_;
+    /// @brief Second signal set object.
+    SignalSetPtr secondary_signal_set_;
+};
+
+int SignalSetTest::handler_calls_ = 0;
+int SignalSetTest::signum_ = -1;
+
+/// Check that the signals are recorded by the signal handlers.
+TEST_F(SignalSetTest, twoSignals) {
+    // Register handlers for two signals.
+    ASSERT_NO_THROW(signal_set_.reset(new SignalSet(SIGHUP, SIGINT)));
+    // Send SIGHUP signal to the process.
+    ASSERT_EQ(0, raise(SIGHUP));
+    // The SIGHUP should be the next one in the queue to be handled.
+    EXPECT_EQ(SIGHUP, signal_set_->getNext());
+    // But, no handlers should have been called yet.
+    EXPECT_EQ(0, handler_calls_);
+    // Send a different signal.
+    ASSERT_EQ(0, raise(SIGINT));
+    // The SIGHUP hasn't been handled yet so it should still be the first
+    // one in the queue.
+    EXPECT_EQ(SIGHUP, signal_set_->getNext());
+    // No handlers have been called yet.
+    EXPECT_EQ(0, handler_calls_);
+    // Raise another SIGHUP before the first one has been handled. The
+    // second one should be dropped.
+    ASSERT_EQ(0, raise(SIGHUP));
+    // Execute the first handler (for SIGHUP).
+    signal_set_->handleNext(boost::bind(&SignalSetTest::testHandler, _1));
+    // The handler should have been called once and the signal
+    // handled should be SIGHUP.
+    EXPECT_EQ(1, handler_calls_);
+    EXPECT_EQ(SIGHUP, signum_);
+    // Next signal to be handled should be SIGINT.
+    EXPECT_EQ(SIGINT, signal_set_->getNext());
+    signal_set_->handleNext(boost::bind(&SignalSetTest::testHandler, _1));
+    EXPECT_EQ(2, handler_calls_);
+    EXPECT_EQ(SIGINT, signum_);
+    // There should be no more waiting handlers.
+    EXPECT_EQ(-1, signal_set_->getNext());
+    // Make sure that signals can be unregistered.
+    EXPECT_NO_THROW(signal_set_->remove(SIGHUP));
+    EXPECT_NO_THROW(signal_set_->remove(SIGINT));
+}
+
+/// Check that the signal set can only handle signals owned by it.
+TEST_F(SignalSetTest, twoSignalSets) {
+    // Register handler for SIGHUP in the first signal set.
+    signal_set_.reset(new SignalSet(SIGHUP));
+    // Register handler for SIGINT in the second signal set.
+    secondary_signal_set_.reset(new SignalSet(SIGINT));
+    // Send SIGHUP.
+    ASSERT_EQ(0, raise(SIGHUP));
+    // Send SIGINT.
+    ASSERT_EQ(0, raise(SIGINT));
+    // Although the SIGHUP is the first signal received by the process
+    // it is not owned by the secondary signal set. The first signal
+    // to be handled by the secondary signal set is SIGINT.
+    EXPECT_EQ(SIGINT, secondary_signal_set_->getNext());
+    // The signal set owns SIGHUP so it should be the next to handle.
+    EXPECT_EQ(SIGHUP, signal_set_->getNext());
+    // Handle next signal owned by the secondary signal set.
+    secondary_signal_set_->handleNext(boost::bind(&SignalSetTest::testHandler,
+                                                  _1));
+    EXPECT_EQ(1, handler_calls_);
+    EXPECT_EQ(SIGINT, signum_);
+    // No more signals to be handled for this signal set.
+    EXPECT_EQ(-1, secondary_signal_set_->getNext());
+    // Handle next signal owned by the signal set.
+    signal_set_->handleNext(boost::bind(&SignalSetTest::testHandler, _1));
+    EXPECT_EQ(2, handler_calls_);
+    EXPECT_EQ(SIGHUP, signum_);
+    // No more signals to be handled by this signal set.
+    EXPECT_EQ(-1, signal_set_->getNext());
+}
+
+// Check that each signal set removes only the signals that it has been used
+// to register.
+TEST_F(SignalSetTest, remove) {
+    // Register handlers for SIGHUP using one signal set.
+    ASSERT_NO_THROW(signal_set_.reset(new SignalSet(SIGHUP)));
+    // Define another signal set and register a different signal.
+    ASSERT_NO_THROW(secondary_signal_set_.reset(new SignalSet(SIGINT)));
+    // The SIGHUP has been already registsred with the other signal
+    // set, so it should not be possible to register it again.
+    ASSERT_THROW(secondary_signal_set_->add(SIGHUP), SignalSetError);
+    // It shouldn't be possible to remove the signal registered in a different
+    // signal set.
+    ASSERT_THROW(secondary_signal_set_->remove(SIGHUP), SignalSetError);
+    // Remove all signals from the first signal set. The signals registered
+    // with the other signal signal set should be preserved.
+    ASSERT_NO_THROW(signal_set_->clear());
+    // Check indirectly that the SIGINT is still registered. An attempt to
+    // register registered signal should result in failure.
+    EXPECT_THROW(secondary_signal_set_->add(SIGINT), SignalSetError);
+    // But, we should be able to regsiter SIGHUP.
+    EXPECT_NO_THROW(secondary_signal_set_->add(SIGHUP));
+}
+
+/// Check that it is not possible to duplicate signals.
+TEST_F(SignalSetTest, duplicates) {
+    ASSERT_NO_THROW(signal_set_.reset(new SignalSet(SIGHUP)));
+    // It shouldn't be possible to register the same signal.
+    EXPECT_THROW(signal_set_->add(SIGHUP), SignalSetError);
+    // But ok to register a different one.
+    EXPECT_NO_THROW(signal_set_->add(SIGTERM));
+    // Now, let's define other signal set.
+    SignalSetPtr other;
+    // SIGINT hasn't been registered in the first signal set
+    // so it should be fine to register.
+    ASSERT_NO_THROW(other.reset(new SignalSet(SIGINT)));
+    // SIGHUP has been already registered in the first signal set so
+    // an attempt to register it again should result in a failure.
+    EXPECT_THROW(other->add(SIGHUP), SignalSetError);
+}
+
+
+} // end of anonymous namespace