Commit 73f57b83 authored by Thomas Markwalder's avatar Thomas Markwalder
Browse files

[3407] Integrated IOSignaling into CPL

DController was extended to instantiate an IOSignalQueue and register for
signals with a SignalSet.  The default implementation for signal processing
supports SIGHUP as config file reload, and SIGINT/SIGTERM for graceful
shutdown.  D2Controller inherits this support without change.

A good deal of work went into the unit test classes as well, particularly
DControllerTest.
parent 8c102369
......@@ -32,15 +32,15 @@ new configuration. It is output during server startup, and when an updated
configuration is committed by the administrator. Additional information
may be provided.
% DCTL_CONFIG_FILE_LOAD_FAIL %1 configuration could not be loaded from file: %2
This fatal error message indicates that the application attempted to load its
initial configuration from file and has failed. The service will exit.
% DCTL_CONFIG_LOAD_FAIL %1 configuration failed to load: %2
This critical error message indicates that the initial application
configuration has failed. The service will start, but will not
process requests until the configuration has been corrected.
% DCTL_CONFIG_FILE_LOAD_FAIL %1 configuration could not be loaded from file: %2
This fatal error message indicates that the application attempted to load its
initial configuration from file and has failed. The service will exit.
% DCTL_CONFIG_START parsing new configuration: %1
A debug message indicating that the application process has received an
updated configuration and has passed it to its configuration manager
......@@ -129,6 +129,16 @@ mapping additions which were received and accepted by an appropriate DNS server.
This is a debug message that indicates that the application has DHCP_DDNS
requests in the queue but is working as many concurrent requests as allowed.
% DHCP_DDNS_CFG_FILE_RELOAD_ERROR configuration reload failed: %1, reverting to current configuration.
This is an error message indicating that the application attempted to reload
its configuration from file and encountered an error. This is likely due to
invalid content in the configuration file. The application should continue
to operate under its current configuration.
% DHCP_DDNS_CFG_FILE_RELOAD_SIGNAL_RECVD OS signal %1 received, reloading configurationfrom file: %2
This is an informational message indicating the application has received a signal
instructing it to reload its configuration from file.
% DHCP_DDNS_CLEARED_FOR_SHUTDOWN application has met shutdown criteria for shutdown type: %1
This is an informational message issued when the application has been instructed
to shutdown and has met the required criteria to exit.
......@@ -452,6 +462,15 @@ in event loop.
This is informational message issued when the application has been instructed
to shut down by the controller.
% DHCP_DDNS_SHUTDOWN_SIGNAL_RECVD OS signal %1 received, starting shutdown
This is an informational message indicating the application has received a signal
instructing it to shutdown.
% DHCP_DDNS_SIGNAL_ERROR signal handler for signal %1, threw an unexpected exception: %2
This is an error message indicating that the application encountered an unexpected error after receiving a signal. This is a programmatic error and should be
reported. While The application will likely continue to operating, it may be
unable to respond correctly to signals.
% DHCP_DDNS_STARTING_TRANSACTION Transaction Key: %1
This is a debug message issued when DHCP-DDNS has begun a transaction for
a given request.
......@@ -468,6 +487,11 @@ message but the attempt to send it suffered a unexpected error. This is most
likely a programmatic error, rather than a communications issue. Some or all
of the DNS updates requested as part of this request did not succeed.
% DHCP_DDNS_UNSUPPORTED_SIGNAL ignoring reception of unsupported signal: %1
This is a debug message indicating that the application received an unsupported
signal. This a programmatic error indicating the application has registered to
receive the signal, but for which no processing logic has been added.
% DHCP_DDNS_UPDATE_REQUEST_SENT %1 for transaction key: %2 to server: %3
This is a debug message issued when DHCP_DDNS sends a DNS request to a DNS
server.
......@@ -475,8 +499,3 @@ server.
% DHCP_DDNS_UPDATE_RESPONSE_RECEIVED for transaction key: %1 to server: %2 status: %3
This is a debug message issued when DHCP_DDNS receives sends a DNS update
response from a DNS server.
% DHCP_DDNS_SIGNAL_ERROR The signal handler for signal %1, threw an unexpected exception: %2
This is an error message indicating that the application encountered an unexpected error after receiving a signal. This is a programmatic error and should be
reported. While The application will likely continue to operating, it may be
unable to respond correctly to signals.
......@@ -30,7 +30,8 @@ DControllerBasePtr DControllerBase::controller_;
DControllerBase::DControllerBase(const char* app_name, const char* bin_name)
: app_name_(app_name), bin_name_(bin_name),
verbose_(false), spec_file_name_(""),
io_service_(new isc::asiolink::IOService()){
io_service_(new isc::asiolink::IOService()),
signal_set_(), io_signal_queue_() {
}
void
......@@ -75,6 +76,9 @@ DControllerBase::launch(int argc, char* argv[], const bool test_mode) {
"Application Process initialization failed: " << ex.what());
}
// Now that we have a proces, we can set up signal handling.
initSignalHandling();
LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DCTL_STANDALONE).arg(app_name_);
// Step 3 is to load configuration from file.
......@@ -287,6 +291,79 @@ DControllerBase::shutdownProcess(isc::data::ConstElementPtr args) {
return (isc::config::createAnswer(0, "Process has not been initialzed."));
}
void
DControllerBase::initSignalHandling() {
/// @todo block everything we don't handle
// Create our signal queue.
io_signal_queue_.reset(new IOSignalQueue(io_service_));
// Install the on-receipt handler
util::SignalSet::setOnReceiptHandler(boost::bind(&DControllerBase::
osSignalHandler,
this, _1));
// Register for the signals we wish to handle.
signal_set_.reset(new util::SignalSet(SIGHUP,SIGINT,SIGTERM));
}
bool
DControllerBase::osSignalHandler(int signum) {
// Create a IOSignal to propagate the signal to IOService.
io_signal_queue_->pushSignal(signum, boost::bind(&DControllerBase::
ioSignalHandler,
this, _1));
return (true);
}
void
DControllerBase::ioSignalHandler(IOSignalId sequence_id) {
// Pop the signal instance off the queue. This should make us
// the only one holding it, so when we leave it should be freed.
// (note that popSignal will throw if signal is not found, which
// in turn will caught, logged, and swallowed by IOSignal callback
// invocation code.)
IOSignalPtr io_signal = io_signal_queue_->popSignal(sequence_id);
// Now call virtual signal processing method.
processSignal(io_signal->getSignum());
}
void
DControllerBase::processSignal(int signum) {
switch (signum) {
case SIGHUP:
{
LOG_INFO(dctl_logger, DHCP_DDNS_CFG_FILE_RELOAD_SIGNAL_RECVD)
.arg(signum).arg(getConfigFile());
int rcode;
isc::data::ConstElementPtr comment = isc::config::
parseAnswer(rcode,
configFromFile());
if (rcode != 0) {
LOG_ERROR(dctl_logger, DHCP_DDNS_CFG_FILE_RELOAD_ERROR)
.arg(comment->stringValue());
}
break;
}
case SIGINT:
case SIGTERM:
{
LOG_INFO(dctl_logger, DHCP_DDNS_SHUTDOWN_SIGNAL_RECVD)
.arg(signum);
isc::data::ElementPtr arg_set;
executeCommand(SHUT_DOWN_COMMAND, arg_set);
break;
}
default:
LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT,
DHCP_DDNS_UNSUPPORTED_SIGNAL).arg(signum);
break;
}
}
void
DControllerBase::usage(const std::string & text)
{
......
......@@ -19,9 +19,11 @@
#include <d2/d2_asio.h>
#include <d2/d2_log.h>
#include <d2/d_process.h>
#include <d2/io_service_signal.h>
#include <dhcpsrv/daemon.h>
#include <exceptions/exceptions.h>
#include <log/logger_support.h>
#include <util/signal_set.h>
#include <boost/shared_ptr.hpp>
#include <boost/noncopyable.hpp>
......@@ -78,9 +80,12 @@ typedef boost::shared_ptr<DControllerBase> DControllerBasePtr;
/// creation.
/// It provides the callback handlers for command and configuration events
/// which could be triggered by an external source. Such sources are intended
/// to be registed with and monitored by the controller's IOService such that
/// to be registered with and monitored by the controller's IOService such that
/// the appropriate handler can be invoked.
///
/// DControllerBase provides dynamic configuration file reloading upon receipt
/// of SIGHUP, and graceful shutdown upon receipt of either SIGINT or SIGTERM.
///
/// NOTE: Derivations must supply their own static singleton instance method(s)
/// for creating and fetching the instance. The base class declares the instance
/// member in order for it to be available for static callback functions.
......@@ -101,9 +106,10 @@ public:
///
/// 1. parse command line arguments
/// 2. instantiate and initialize the application process
/// 3. load the configuration file
/// 4. start and wait on the application process event loop
/// 5. exit to the caller
/// 3. initialize signal handling
/// 4. load the configuration file
/// 5. start and wait on the application process event loop
/// 6. exit to the caller
///
/// It is intended to be called from main() and be given the command line
/// arguments.
......@@ -155,7 +161,7 @@ public:
/// configuration data for this controller's application
///
/// module-config: a set of zero or more JSON elements which comprise
/// the application'ss configuration values
/// the application's configuration values
/// @endcode
///
/// The method extracts the set of configuration elements for the
......@@ -279,6 +285,24 @@ protected:
return ("");
}
/// @brief Application-level signal processing method.
///
/// This method is the last step in processing a OS signal occurrence. It
/// is invoked when an IOSignal's internal timer callback is executed by
/// IOService. It currently supports the following signals as follows:
/// -# SIGHUP - instigates reloading the configuration file
/// -# SIGINT - instigates a graceful shutdown
/// -# SIGTERM - instigates a graceful shutdown
/// If if received any other signal, it will issue a debug statement and
/// discard it.
/// Derivations wishing to support additional signals could override this
/// method with one that: processes the signal if it is one of additional
/// signals, otherwise invoke this method (DControllerBase::processSignal())
/// with signal value.
/// @todo Provide a convenient way for derivations to register additional
/// signals.
virtual void processSignal(int signum);
/// @brief Supplies whether or not verbose logging is enabled.
///
/// @return returns true if verbose logging is enabled.
......@@ -372,7 +396,7 @@ protected:
/// to begin its shutdown process.
///
/// Note, it is assumed that the process of shutting down is neither
/// instanteneous nor synchronous. This method does not "block" waiting
/// instantaneous nor synchronous. This method does not "block" waiting
/// until the process has halted. Rather it is used to convey the
/// need to shutdown. A successful return indicates that the shutdown
/// has successfully commenced, but does not indicate that the process
......@@ -383,9 +407,41 @@ protected:
/// non-zero means failure), and a string explanation of the outcome.
isc::data::ConstElementPtr shutdownProcess(isc::data::ConstElementPtr args);
/// @brief Initializes signal handling
///
/// This method configures the controller to catch and handle signals.
/// It instantiates an IOSignalQueue, registers @c osSignalHandler() as
/// the SignalSet "on-receipt" handler, and lastly instantiates a SignalSet
/// which listens for SIGHUP, SIGINT, and SIGTERM.
void initSignalHandling();
/// @brief Handler for processing OS-level signals
///
/// This method is installed as the SignalSet "on-receipt" handler. Upon
/// invocation, it uses the controller's IOSignalQueue to schedule an
/// IOSignal with for the given signal value.
///
/// @param signum OS signal value (e.g. SIGINT, SIGUSR1 ...) to received
///
/// @return SignalSet "on-receipt" handlers are required to return a
/// boolean indicating if the OS signal has been processed (true) or if it
/// should be saved for deferred processing (false). Currently this
/// method processes all received signals, so it always returns true.
bool osSignalHandler(int signum);
/// @brief Handler for processing IOSignals
///
/// This method is supplied as the callback when IOSignals are scheduled.
/// It fetches the IOSignal for the given sequence_id and then invokes
/// the virtual method, @c processSignal() passing it the signal value
/// obtained from the IOSignal. This allows derivations to supply a
/// custom signal processing method, while ensuring IOSignalQueue
/// integrity.
void ioSignalHandler(IOSignalId sequence_id);
/// @brief Fetches the current process
///
/// @return the a pointer to the current process instance.
/// @return a pointer to the current process instance.
DProcessBasePtr getProcess() {
return (process_);
}
......@@ -403,7 +459,7 @@ private:
std::string app_name_;
/// @brief Name of the service executable.
/// By convention this matches the executable nam. It is also used to
/// By convention this matches the executable name. It is also used to
/// establish the logger name.
std::string bin_name_;
......@@ -422,6 +478,12 @@ private:
/// @brief Shared pointer to an IOService object, used for ASIO operations.
IOServicePtr io_service_;
/// @brief Set of registered signals to handle.
util::SignalSetPtr signal_set_;
/// @brief Queue for propagating caught signals to the IOService.
IOSignalQueuePtr io_signal_queue_;
/// @brief Singleton instance value.
static DControllerBasePtr controller_;
......
......@@ -17,7 +17,6 @@
#include <d2/d2_asio.h>
#include <exceptions/exceptions.h>
//#include <util/signal_set.h>
#include <map>
......
......@@ -15,6 +15,7 @@
#include <config/ccsession.h>
#include <d_test_stubs.h>
#include <d2/d2_controller.h>
#include <d2/d2_process.h>
#include <d2/spec_config.h>
#include <boost/pointer_cast.hpp>
......@@ -51,6 +52,40 @@ public:
/// @brief Destructor
~D2ControllerTest() {
}
/// @brief Fetches the D2Controller's D2Process
///
/// @return A pointer to the process which may be null if it has not yet
/// been instantiated.
D2ProcessPtr getD2Process() {
return (boost::dynamic_pointer_cast<D2Process>(getProcess()));
}
/// @brief Fetches the D2Process's D2Configuration manager
///
/// @return A pointer to the manager which may be null if it has not yet
/// been instantiated.
D2CfgMgrPtr getD2CfgMgr() {
D2CfgMgrPtr p;
if (getD2Process()) {
p = getD2Process()->getD2CfgMgr();
}
return (p);
}
/// @brief Fetches the D2Configuration manager's D2CfgContext
///
/// @return A pointer to the context which may be null if it has not yet
/// been instantiated.
D2CfgContextPtr getD2CfgContext() {
D2CfgContextPtr p;
if (getD2CfgMgr()) {
p = getD2CfgMgr()->getD2CfgContext();
}
return (p);
}
};
/// @brief Basic Controller instantiation testing.
......@@ -120,34 +155,13 @@ TEST_F(D2ControllerTest, initProcessTesting) {
/// This creates an interval timer to generate a normal shutdown and then
/// launches with a valid, stand-alone command line and no simulated errors.
TEST_F(D2ControllerTest, launchNormalShutdown) {
// command line to run standalone
char* argv[] = { const_cast<char*>("progName"),
const_cast<char*>("-c"),
const_cast<char*>(DControllerTest::CFG_TEST_FILE),
const_cast<char*>("-v") };
int argc = 4;
// Create a valid D2 configuration file.
writeFile(valid_d2_config);
// Write valid_d2_config and then run launch() for 1000 ms.
time_duration elapsed_time;
runWithConfig(valid_d2_config, 1000, elapsed_time);
// Use an asiolink IntervalTimer and callback to generate the
// shutdown invocation. (Note IntervalTimer setup is in milliseconds).
isc::asiolink::IntervalTimer timer(*getIOService());
timer.setup(genShutdownCallback, 2 * 1000);
// Record start time, and invoke launch().
ptime start = microsec_clock::universal_time();
EXPECT_NO_THROW(launch(argc, argv));
// Record stop time.
ptime stop = microsec_clock::universal_time();
// Verify that duration of the run invocation is the same as the
// timer duration. This demonstrates that the shutdown was driven
// by an io_service event and callback.
time_duration elapsed = stop - start;
EXPECT_TRUE(elapsed.total_milliseconds() >= 1900 &&
elapsed.total_milliseconds() <= 2200);
// Give a generous margin to accomodate slower test environs.
EXPECT_TRUE(elapsed_time.total_milliseconds() >= 800 &&
elapsed_time.total_milliseconds() <= 1300);
}
/// @brief Configuration update event testing.
......@@ -211,7 +225,107 @@ TEST_F(D2ControllerTest, executeCommandTests) {
answer = executeCommand(SHUT_DOWN_COMMAND, arg_set);
isc::config::parseAnswer(rcode, answer);
EXPECT_EQ(COMMAND_SUCCESS, rcode);
}
// Tests that the original configuration is retained after a SIGHUP triggered
// reconfiguration fails due to invalid config content.
TEST_F(D2ControllerTest, invalidConfigReload) {
// Schedule to replace the configuration file after launch. This way the
// file is updated after we have done the initial configuration.
scheduleTimedWrite("{ \"string_test\": BOGUS JSON }", 100);
// Setup to raise SIGHUP in 200 ms.
TimedSignal sighup(*getIOService(), SIGHUP, 200);
// Write valid_d2_config and then run launch() for a maximum of 500 ms.
time_duration elapsed_time;
runWithConfig(valid_d2_config, 500, elapsed_time);
// Context is still available post launch.
// Check to see that our configuration matches the original per
// valid_d2_config (see d_test_stubs.cc)
D2CfgMgrPtr d2_cfg_mgr = getD2CfgMgr();
D2ParamsPtr d2_params = d2_cfg_mgr->getD2Params();
ASSERT_TRUE(d2_params);
EXPECT_EQ("127.0.0.1", d2_params->getIpAddress().toText());
EXPECT_EQ(5031, d2_params->getPort());
EXPECT_TRUE(d2_cfg_mgr->forwardUpdatesEnabled());
EXPECT_TRUE(d2_cfg_mgr->reverseUpdatesEnabled());
/// @todo add a way to trap log file and search it
}
// Tests that the original configuration is replaced after a SIGHUP triggered
// reconfiguration succeeds.
TEST_F(D2ControllerTest, validConfigReload) {
// Define a replacement config.
const char* second_cfg =
"{"
" \"ip_address\": \"192.168.77.1\" , "
" \"port\": 777 , "
"\"tsig_keys\": [], "
"\"forward_ddns\" : {}, "
"\"reverse_ddns\" : {} "
"}";
// Schedule to replace the configuration file after launch. This way the
// file is updated after we have done the initial configuration.
scheduleTimedWrite(second_cfg, 100);
// Setup to raise SIGHUP in 200 ms.
TimedSignal sighup(*getIOService(), SIGHUP, 200);
// Write valid_d2_config and then run launch() for a maximum of 500ms.
time_duration elapsed_time;
runWithConfig(valid_d2_config, 500, elapsed_time);
// Context is still available post launch.
// Check to see that our configuration matches the replacement config.
D2CfgMgrPtr d2_cfg_mgr = getD2CfgMgr();
D2ParamsPtr d2_params = d2_cfg_mgr->getD2Params();
ASSERT_TRUE(d2_params);
EXPECT_EQ("192.168.77.1", d2_params->getIpAddress().toText());
EXPECT_EQ(777, d2_params->getPort());
EXPECT_FALSE(d2_cfg_mgr->forwardUpdatesEnabled());
EXPECT_FALSE(d2_cfg_mgr->reverseUpdatesEnabled());
/// @todo add a way to trap log file and search it
}
// Tests that the SIGINT triggers a normal shutdown.
TEST_F(D2ControllerTest, sigintShutdown) {
// Setup to raise SIGHUP in 1 ms.
TimedSignal sighup(*getIOService(), SIGINT, 1);
// Write valid_d2_config and then run launch() for a maximum of 1000 ms.
time_duration elapsed_time;
runWithConfig(valid_d2_config, 1000, elapsed_time);
// Signaled shutdown should make our elapsed time much smaller than
// the maximum run time. Give generous margin to accomodate slow
// test environs.
EXPECT_TRUE(elapsed_time.total_milliseconds() < 300);
/// @todo add a way to trap log file and search it
}
// Tests that the SIGTERM triggers a normal shutdown.
TEST_F(D2ControllerTest, sigtermShutdown) {
// Setup to raise SIGHUP in 1 ms.
TimedSignal sighup(*getIOService(), SIGTERM, 1);
// Write valid_d2_config and then run launch() for a maximum of 1 s.
time_duration elapsed_time;
runWithConfig(valid_d2_config, 1000, elapsed_time);
// Signaled shutdown should make our elapsed time much smaller than
// the maximum run time. Give generous margin to accomodate slow
// test environs.
EXPECT_TRUE(elapsed_time.total_milliseconds() < 300);
/// @todo add a way to trap log file and search it
}
}; // end of isc::d2 namespace
......
......@@ -32,15 +32,19 @@ namespace d2 {
/// has been constructed to exercise DControllerBase.
class DStubControllerTest : public DControllerTest {
public:
/// @brief Constructor.
/// Note the constructor passes in the static DStubController instance
/// method.
DStubControllerTest() : DControllerTest (DStubController::instance) {
controller_ = boost::dynamic_pointer_cast<DStubController>
(DControllerTest::
getController());
}
virtual ~DStubControllerTest() {
}
DStubControllerPtr controller_;
};
/// @brief Basic Controller instantiation testing.
......@@ -183,35 +187,15 @@ TEST_F(DStubControllerTest, launchProcessInitError) {
/// launches with a valid, command line, with a valid configuration file
/// and no simulated errors.
TEST_F(DStubControllerTest, launchNormalShutdown) {
// command line to run standalone
char* argv[] = { const_cast<char*>("progName"),
const_cast<char*>("-c"),
const_cast<char*>(DControllerTest::CFG_TEST_FILE),
const_cast<char*>("-v") };
int argc = 4;
// Create a non-empty, config file. writeFile will wrap the contents
// with the module name for us.
writeFile("{}");
// Use an asiolink IntervalTimer and callback to generate the
// shutdown invocation. (Note IntervalTimer setup is in milliseconds).
isc::asiolink::IntervalTimer timer(*getIOService());
timer.setup(genShutdownCallback, 2 * 1000);
// Record start time, and invoke launch().
ptime start = microsec_clock::universal_time();
EXPECT_NO_THROW(launch(argc, argv));
// Record stop time.
ptime stop = microsec_clock::universal_time();
// Write the valid, empty, config and then run launch() for 1000 ms
time_duration elapsed_time;
ASSERT_NO_THROW(runWithConfig("{}", 2000, elapsed_time));
// Verify that duration of the run invocation is the same as the
// timer duration. This demonstrates that the shutdown was driven
// by an io_service event and callback.
time_duration elapsed = stop - start;
EXPECT_TRUE(elapsed.total_milliseconds() >= 1900 &&
elapsed.total_milliseconds() <= 2200);
EXPECT_TRUE(elapsed_time.total_milliseconds() >= 1900 &&
elapsed_time.total_milliseconds() <= 2300);
}
/// @brief Tests launch with an nonexistant configuration file.
......@@ -255,35 +239,20 @@ TEST_F(DStubControllerTest, missingConfigFileArgument) {
/// the process event loop. It launches wih a valid, stand-alone command line
/// and no simulated errors. Launch should throw ProcessRunError.
TEST_F(DStubControllerTest, launchRuntimeError) {
// command line to run standalone
char* argv[] = { const_cast<char*>("progName"),
const_cast<char*>("-c"),
const_cast<char*>(DControllerTest::CFG_TEST_FILE),
const_cast<char*>("-v") };
int argc = 4;
// Create a non-empty, config file. writeFile will wrap the contents
// with the module name for us.
writeFile("{}");
// Use an asiolink IntervalTimer and callback to generate the
// shutdown invocation. (Note IntervalTimer setup is in milliseconds).
isc::asiolink::IntervalTimer timer(*getIOService());
timer.setup(genFatalErrorCallback, 2 * 1000);
timer.setup(genFatalErrorCallback, 2000);
// Record start time, and invoke launch().
ptime start = microsec_clock::universal_time();
EXPECT_THROW(launch(argc, argv), ProcessRunError);
// Record stop time.
ptime stop = microsec_clock::universal_time();
// Write the valid, empty, config and then run launch() for 1000 ms
time_duration elapsed_time;
EXPECT_THROW(runWithConfig("{}", 2000, elapsed_time), ProcessRunError);
// Verify that duration of the run invocation is the same as the
// timer duration. This demonstrates that the shutdown was driven
// by an io_service event and callback.
time_duration elapsed = stop - start;
EXPECT_TRUE(elapsed.total_milliseconds() >= 1900 &&
elapsed.total_milliseconds() <= 2200);
EXPECT_TRUE(elapsed_time.total_milliseconds() >= 1900 &&
elapsed_time.total_milliseconds() <= 2300);
}
/// @brief Configuration update event testing.
......@@ -380,5 +349,120 @@ TEST_F(DStubControllerTest, executeCommandTests) {
EXPECT_EQ(COMMAND_ERROR, rcode);