Commit 231f440a authored by Thomas Markwalder's avatar Thomas Markwalder
Browse files

[3407] Added IOSignal class to send signals to IOService

Created d2::IOSignal and d2::IOSignalQueue for propagating
caught OS signals to IOService instances.
Added TimedSignal test class.

New files:
    src/bin/d2/io_service_signal.cc
    src/bin/d2/io_service_signal.h
    src/bin/d2/tests/io_service_signal_unittests.cc
parent 9d712897
......@@ -63,6 +63,7 @@ b10_dhcp_ddns_SOURCES += d2_update_message.cc d2_update_message.h
b10_dhcp_ddns_SOURCES += d2_update_mgr.cc d2_update_mgr.h
b10_dhcp_ddns_SOURCES += d2_zone.cc d2_zone.h
b10_dhcp_ddns_SOURCES += dns_client.cc dns_client.h
b10_dhcp_ddns_SOURCES += io_service_signal.cc io_service_signal.h
b10_dhcp_ddns_SOURCES += labeled_value.cc labeled_value.h
b10_dhcp_ddns_SOURCES += nc_add.cc nc_add.h
b10_dhcp_ddns_SOURCES += nc_remove.cc nc_remove.h
......
......@@ -475,3 +475,8 @@ 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.
// 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 <asiolink/interval_timer.h>
#include <d2/d2_log.h>
#include <d2/io_service_signal.h>
namespace isc {
namespace d2 {
IOSignal::IOSignal (asiolink::IOService& io_service, int signum,
IOSignalHandler handler)
: sequence_id_(nextSequenceId()), signum_(signum),
timer_(new asiolink::IntervalTimer(io_service)) {
// Valid handler is essential.
if (!handler) {
isc_throw(IOSignalError,
"IOSignal - handler cannot be null");
}
// Set up the timer as a one-shot which expires in 1 ms (intervals of 0
// are invalid). This means that once control returns to IOService::run
// the timer will have expired and its handler will be invoked.
timer_->setup(TimerCallback(sequence_id_, handler), 1,
asiolink::IntervalTimer::ONE_SHOT);
}
IOSignal::~IOSignal() {
if (timer_) {
// In the unlikely event that the timer hasn't expired cancel it.
timer_->cancel();
}
}
IOSignal::
TimerCallback::TimerCallback(IOSignalId sequence_id, IOSignalHandler handler)
: sequence_id_(sequence_id), handler_(handler) {
if (!handler) {
isc_throw(IOSignalError,
"IOSignal::TimerCallback - handler cannot be null");
}
}
void
IOSignal::TimerCallback::operator()() {
try {
handler_(sequence_id_);
} catch (const std::exception& ex) {
// We log it and swallow it so we don't undermine IOService::run.
LOG_ERROR(dctl_logger, DHCP_DDNS_SIGNAL_ERROR)
.arg(sequence_id_).arg(ex.what());
}
return;
}
IOSignalQueue::IOSignalQueue(IOServicePtr& io_service)
: io_service_(io_service), signals_() {
if (!io_service_) {
isc_throw(IOSignalError, "IOSignalQueue - io_serivce cannot be NULL");
}
}
IOSignalQueue::~IOSignalQueue() {
clear();
}
IOSignalId
IOSignalQueue::pushSignal(int signum, IOSignalHandler handler) {
// Create the new signal.
IOSignalPtr signal(new IOSignal(*io_service_, signum, handler));
// Make sure the sequence_id isn't already in the queue.
IOSignalId sequence_id = signal->getSequenceId();
IOSignalMap::iterator it = signals_.find(sequence_id);
if (it != signals_.end()) {
// This really shouldn't happen unless we are in the weeds.
isc_throw (IOSignalError, "pushSignal - "
"signal already exists for sequence_id: " << sequence_id);
}
// Add the signal to the queue.
signals_[sequence_id] = signal;
return (sequence_id);
}
IOSignalPtr
IOSignalQueue::popSignal(IOSignalId sequence_id) {
// Look for the signal in the queue.
IOSignalMap::iterator it = signals_.find(sequence_id);
if (it == signals_.end()) {
// This really shouldn't happen unless we are in the weeds.
isc_throw (IOSignalError, "popSignal - "
"signal not found for sequence_id: " << sequence_id);
}
// Save the siganl so we can return it.
IOSignalPtr signal = ((*it).second);
// Delete it fromt the queue.
signals_.erase(it);
// Return the signal.
return (signal);
}
void
IOSignalQueue::clear() {
signals_.clear();
}
}; // end of isc::d2 namespace
}; // end of isc namespace
// 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 IO_SERVICE_SIGNAL_H
#define IO_SERVICE_SIGNAL_H
#include <d2/d2_asio.h>
#include <exceptions/exceptions.h>
//#include <util/signal_set.h>
#include <map>
namespace isc {
namespace d2 {
/// @brief Exception thrown if IOSignal encounters an error.
class IOSignalError : public isc::Exception {
public:
IOSignalError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) { };
};
/// @brief Defines a unique identifier type for IOSignal.
typedef uint64_t IOSignalId;
/// @brief Defines a handler function for an IOSignal.
/// IOSignalHandlers should contain the application level logic that would
/// ordinarily be an OS signal handler.
typedef boost::function<void(IOSignalId sequence_id)> IOSignalHandler;
/// @brief Implements an asynchronous "signal" for IOService driven processing
///
/// This class allows a OS signal such as SIGHUP to propagated to an IOService
/// as a ready event with a callback. While boost::asio provides a signal class,
/// it requires linking in additional boost libraries that as of yet we do not
/// need. Therefore, this class was implemented to allow IOService-based
/// processes to handle signals as IOService events.
///
/// The mechanics of IOSignal are straight forward. Upon construction it is
/// given the target IOService, the value of the signal to send (i.e. SIGINT,
/// SIGHUP...), and an IOSignalHandler. The IOSignalHandler should contain
/// the logic the caller would normally execute in its OS signal handler. Each
/// IOSignal instance has a unique identifier called its sequence_id.
///
/// Internally, IOSignal creates a 1 ms, one-shot timer, on the given
/// IOService. When the timer expires its event handler invokes the caller's
/// IOSignalHandler passing it the sequence_id of the IOSignal.
///
/// Sending IOSignals is done through an IOSignalQueue. This class is used to
/// create the signals, house them until they are delivered, and dequeue them
/// so they can be been handled. To generate an IOSignal when an OS signal
/// arrives, the process's OS signal handler simply calls @ref
/// isc::d2::IOSignalQueue::pushSignal() with the appropriate values.
///
/// @Note that an IOSignalQueue requires a non-null IOServicePtr to construct.
/// This ensures that the IOService cannot be destroyed before any pending
/// signals can be canceled. It also means that a queue can only be used to
/// send signals to that IOService. If you need to send signals to more than
/// one service, each service must have its own queue.
///
/// To dequeue the IOSignal inside the caller's IOSignalHandler, one simply
/// invokes @ref isc::d2::IOSignalQueue:popSignal() passing it the sequence_id
/// parameter passed to the handler. This method returns a pointer to
/// instigating IOSignal from which the value of OS signal (i.e. SIGINT,
/// SIGUSR1...) can be obtained. Note that calling popSignal() removes the
/// IOSignalPtr from the queue, which should reduce its reference count to
/// zero upon exiting the handler (unless a delibrate copy of it is made).
///
/// A typical IOSignalHandler might be structured as follows:
/// @code
///
/// void processSignal(IOSignalId sequence_id) {
/// // Pop the signal instance off the queue.
/// IOSignalPtr signal = io_signal_queue_->popSignal(sequence_id);
///
/// int os_signal_value = signal->getSignum();
/// :
/// // logic based on the signal value
/// :
/// }
///
/// @endcode
///
/// IOSignal handler invocation code will catch, log ,and then swallow any
/// exceptions thrown by a IOSignalHandler invocation. This is done to protect
/// the integrity IOService context.
///
class IOSignal {
public:
/// @brief Constructor
///
/// @param io_service IOService to which to send the signal
/// @param signum value of the signal to send
/// @param handler the handler to run when IOService "receives" the
/// signal
///
/// @throw IOSignalError if handler is null
IOSignal(asiolink::IOService& io_service, int signum,
IOSignalHandler handler);
~IOSignal();
/// @brief Static method for generating IOSignal sequence_ids.
///
/// Generates and returns the next IOSignalId. This method is intentionally
/// static in the event a process is using generating signals to more than
/// IOService. It assures that each IOSignal is unique with the process
/// space.
///
/// @return The next sequential value as an IOSignalId.
static IOSignalId nextSequenceId() {
static IOSignalId next_id_ = 0;
return (++next_id_);
}
/// @brief Gets the IOSignal's sequence_id
///
/// @return The sequence_id of the signal.
IOSignalId getSequenceId() const {
return (sequence_id_);
}
/// @brief Gets the OS signal value this IOSignal represents.
///
/// @return The OS signal value (i.e. SIGINT, SIGUSR1...)
int getSignum() const {
return (signum_);
}
/// @brief Defines the callback used by IOSignal's internal timer.
///
/// This class stores the sequence_id of the IOSignal being sent and the
/// IOSignalHandler to invoke when delivering the signal. The () operator
/// is called by IOService when the timer expires. This method invokes
/// the IOSignalHandler passing it the sequence_id.
class TimerCallback : public std::unary_function<void, void> {
public:
/// @brief Constructor
///
/// @param sequence_id sequence_id of the IOSignal to handle
/// @param handler pointer to the function to handle the IOSignal
///
/// @throw IOSignalError if handler is null.
TimerCallback(IOSignalId sequence_id_, IOSignalHandler handler_);
/// @brief () Operator which serves as the timer's callback
///
/// It is invoked when the timer expires and calls the handler
/// passing in the signal.
void operator()();
private:
/// @brief Id of the IOSignal to which the callback pertains.
IOSignalId sequence_id_;
/// @brief Pointer to the function to handle the signal
IOSignalHandler handler_;
};
private:
/// @brief Value which uniquely identifies each IOSignal instance.
IOSignalId sequence_id_;
/// @brief Numeric value of the signal to send (e.g. SIGINT, SIGUSR1...)
int signum_;
/// @brief Timer instance created to propagate the signal.
asiolink::IntervalTimerPtr timer_;
};
/// @brief Defines a pointer to an IOSignal
typedef boost::shared_ptr<IOSignal> IOSignalPtr;
/// @brief Defines a map of IOSignalPtr keyed by id
typedef std::map<IOSignalId, IOSignalPtr> IOSignalMap;
/// @brief Creates and manages IOSignals
///
/// This class is used to create IOSignals, house them until they are delivered,
/// and dequeue them so they can be been handled. IOSignals are designed to
/// used once and then destroyed. They need to be created from within OS
/// signal handlers and persist until they have been delivered and processed.
///
/// This class is designed specifically to make managing them painless.
/// It maintains an internal map of IOSignals keyed by sequence_id. When a
/// signal is created via the pushSignal() method it is added to the map. When
/// a signal is retrevied via the popSignal() method it is removed from the map.
class IOSignalQueue {
public:
/// @brief Constructor
///
/// @param io_service the IOService to which to send signals.
/// @throw IOSignalError if io_service is NULL.
IOSignalQueue (IOServicePtr& io_service);
/// Destructor.
~IOSignalQueue();
/// @brief Creates an IOSignal
///
/// Given a signal number and a handler, it will instantiate an IOSignal
/// and add it to the instance map. (Remember that IOSignals are really
/// just timers programmed during construction, so once instantiated
/// there's nothing more required to "send" the signal other than return
/// control to IOService::run()).
///
/// @param signum OS signal value of the signal to propagate
/// @param handler IOSignalHandler to invoke when the signal is delivererd.
///
/// @return The sequence_id of the newly created signal.
///
/// @throw IOSignalError if the sequence_id already exists in the map. This
/// is virtually impossible unless things have gone very wrong.
IOSignalId pushSignal(int signum, IOSignalHandler handler);
/// @brief Removes an IOSignal from the map and returns it.
///
/// Given a sequence_id this method will extract the IOSignal from the
/// internal map and return. At that point, the caller will hold the
/// only copy of the IOSignal.
///
/// @param sequence_id sequence_id of the IOSignal to retrieve.
///
/// @return A smart pointer to the IOSignal.
///
/// @throw IOSignalError if there is no matching IOSignal in the map for
/// the given sequence_id. Other than by doubling popping, this should be
/// very unlikley.
IOSignalPtr popSignal(IOSignalId sequence_id);
/// @brief Erases the contents of the queue.
///
/// Any instances still in the map will be destroyed. This will cause their
/// timers to be cancelled without any callbacks invoked. (Not sure when
/// this might be desirable).
void clear();
private:
/// @brief Pointer to the IOService which will receive the signals.
IOServicePtr io_service_;
/// @brief A map of the IOSignals pushed through this queue.
IOSignalMap signals_;
};
/// @brief Defines a pointer to an IOSignalQueue.
typedef boost::shared_ptr<IOSignalQueue> IOSignalQueuePtr;
}; // end of isc::d2 namespace
}; // end of isc namespace
#endif // IO_SERVICE_SIGNAL_H
......@@ -64,6 +64,7 @@ d2_unittests_SOURCES += ../d2_update_message.cc ../d2_update_message.h
d2_unittests_SOURCES += ../d2_update_mgr.cc ../d2_update_mgr.h
d2_unittests_SOURCES += ../d2_zone.cc ../d2_zone.h
d2_unittests_SOURCES += ../dns_client.cc ../dns_client.h
d2_unittests_SOURCES += ../io_service_signal.cc ../io_service_signal.h
d2_unittests_SOURCES += ../labeled_value.cc ../labeled_value.h
d2_unittests_SOURCES += ../nc_add.cc ../nc_add.h
d2_unittests_SOURCES += ../nc_remove.cc ../nc_remove.h
......@@ -79,6 +80,7 @@ d2_unittests_SOURCES += d2_update_message_unittests.cc
d2_unittests_SOURCES += d2_update_mgr_unittests.cc
d2_unittests_SOURCES += d2_zone_unittests.cc
d2_unittests_SOURCES += dns_client_unittests.cc
d2_unittests_SOURCES += io_service_signal_unittests.cc
d2_unittests_SOURCES += labeled_value_unittests.cc
d2_unittests_SOURCES += nc_add_unittests.cc
d2_unittests_SOURCES += nc_remove_unittests.cc
......
......@@ -683,6 +683,68 @@ public:
isc::data::ConstElementPtr answer_;
};
/// @brief Implements a time-delayed signal
///
/// Given an IOService, a signal number, and a time period, this class will
/// send (raise) the signal to the current process.
class TimedSignal {
public:
/// @brief Constructor
///
/// @param io_service IOService to run the timer
/// @param signum OS signal value (e.g. SIGHUP, SIGUSR1 ...)
/// @param milliseconds time in milliseconds to wait until the signal is
/// raised.
/// @param mode selects between a one-shot signal or a signal which repeats
/// at "milliseconds" interval.
TimedSignal(asiolink::IOService& io_service, int signum, int milliseconds,
const asiolink::IntervalTimer::Mode& mode =
asiolink::IntervalTimer::ONE_SHOT)
: timer_(new asiolink::IntervalTimer(io_service)) {
timer_->setup(SendSignalCallback(signum), milliseconds, mode);
}
/// @brief Cancels the given timer.
void cancel() {
if (timer_) {
timer_->cancel();
}
}
/// @brief Destructor.
~TimedSignal() {
cancel();
}
/// @brief Callback for the TimeSignal's internal timer.
class SendSignalCallback: public std::unary_function<void, void> {
public:
/// @brief Constructor
///
/// @param signum OS signal value of the signal to send
SendSignalCallback(int signum) : signum_(signum) {
}
/// @brief Callback method invoked when the timer expires
///
/// Calls raise with the given signal which should generate that
/// signal to the given process.
void operator()() {
ASSERT_EQ(0, raise(signum_));
return;
}
private:
/// @brief Stores the OS signal value to send.
int signum_;
};
private:
/// @brief Timer which controls when the signal is sent.
asiolink::IntervalTimerPtr timer_;
};
/// @brief Defines a small but valid DHCP-DDNS compliant configuration for
/// testing configuration parsing fundamentals.
extern const char* valid_d2_config;
......
// 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 <d_test_stubs.h>
#include <d2/io_service_signal.h>
#include <gtest/gtest.h>
#include <queue>
namespace isc {
namespace d2 {
/// @brief Test fixture for testing the use of IOSignals.
///
/// This fixture is exercises IOSignaling as it is intended to be used in
/// an application in conjuction with util::SignalSet.
class IOSignalTest : public ::testing::Test {
public:
/// @brief IOService instance to process IO.
IOServicePtr io_service_;
/// @brief Failsafe timer to ensure test(s) do not hang.
isc::asiolink::IntervalTimer test_timer_;
/// @brief Maximum time should be allowed to run.
int test_time_ms_;
/// @brief SignalSet object so we can catch real signals.
util::SignalSetPtr signal_set_;
/// @brief IOSignalQueue so we can generate IOSignals.
IOSignalQueuePtr io_signal_queue_;
/// @brief Vector to record the signal values received.
std::vector<int> processed_signals_;
/// @brief The number of signals that must be received to stop the test.
int stop_at_count_;
/// @brief Boolean which causes IOSignalHandler to throw if true.
bool handler_throw_error_;
/// @brief Constructor
IOSignalTest() :
io_service_(new asiolink::IOService()), test_timer_(*io_service_),
test_time_ms_(0), signal_set_(),
io_signal_queue_(new IOSignalQueue(io_service_)),
processed_signals_(), stop_at_count_(0), handler_throw_error_(false) {
}
/// @brief Destructor
~IOSignalTest() {
if (signal_set_) {
signal_set_->clear();
}
// clear the on-receipt handler
util::SignalSet::clearOnReceiptHandler();
}
/// @brief On-receipt signal handler used by unit tests.
///
/// This function is registered with SignalSet as the "on-receipt" handler.
/// When an OS signal is caught it schedules an IOSignal.
///
/// @param signum Signal being handled.
bool onReceiptHandler(int signum) {
// Queue up a signal binging processSignal instance method as the
// IOSignalHandler.
io_signal_queue_->pushSignal(signum,
boost::bind(&IOSignalTest::processSignal,
this, _1));
// Return true so SignalSet knows the signal has been consumed.
return (true);
}
/// @brief Method used as the IOSignalHandler.
///
/// Records the value of the given signal and checks if the desired
/// number of signals have been received. If so, the IOService is
/// stopped which will cause IOService::run() to exit, returning control
/// to the test.
///
/// @param sequence_id id of the IOSignal received
void processSignal(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.
IOSignalPtr signal = io_signal_queue_->popSignal(sequence_id);
// Remember the signal we got.
processed_signals_.push_back(signal->getSignum());
// If the flag is on, force a throw to test error handling.
if (handler_throw_error_) {
handler_throw_error_ = false;
isc_throw(BadValue, "processSignal throwing simulated error");
}
// If we've hit the number we want stop the IOService. This will cause
// run to exit.
if (processed_signals_.size() >= stop_at_count_) {
io_service_->stop();
}
}
/// @brief Sets the failsafe timer for the test to the given time.
///
/// @param test_time_ms maximum time in milliseconds the test should
/// be allowed to run.
void setTestTime(int test_time_ms) {
// Fail safe shutdown
test_time_ms_ = test_time_ms;
test_timer_.setup(boost::bind(&IOSignalTest::testTimerHandler,
this),
test_time_ms_, asiolink::IntervalTimer::ONE_SHOT);
}
/// @brief Failsafe timer expiration handler.
void testTimerHandler() {
io_service_->stop();
FAIL() << "Test Time: " << test_time_ms_ << " expired";