]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1736] Initial commit with HA+MT integration
authorThomas Markwalder <tmark@isc.org>
Wed, 14 Apr 2021 15:06:14 +0000 (11:06 -0400)
committerThomas Markwalder <tmark@isc.org>
Fri, 23 Apr 2021 12:54:31 +0000 (08:54 -0400)
HA+MT fully integrated and functional.

src/hooks/dhcp/high_availability/ha_service.*
    HAService - changed client_ from instance to pointer
              - added listener_

    HAService::startClientAndListener()  - new method which
    instantiates client_ and listener_ instances based on config,
    and starts them

    HAService::stopClientAndListener()  - new method that stops
    client_ and listener_ instances.

    HAService::HAService() - invokes startClientAndListener()

    HAService::~HAService()  - invokes stopClientAndListener()

src/hooks/dhcp/high_availability/tests/Makefile.am
    Added ha_mt_unittest.c

src/hooks/dhcp/high_availability/tests/ha_mt_unittest.cc
    New file with HA+MT related tests

src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc
    Modified tests to ensure use of ST HAService

src/lib/config/cmd_http_listener.h
    Added CmdHttpListener commentary

src/hooks/dhcp/high_availability/ha_service.cc
src/hooks/dhcp/high_availability/ha_service.h
src/hooks/dhcp/high_availability/tests/Makefile.am
src/hooks/dhcp/high_availability/tests/ha_mt_unittest.cc [new file with mode: 0644]
src/hooks/dhcp/high_availability/tests/ha_service_unittest.cc
src/lib/config/cmd_http_listener.h

index 2f9f1644ebd29936acdefaebe992301d3df59056..2d7d42a15d3ca0e750e3e63fd56ebb73cb995dee 100644 (file)
@@ -14,6 +14,7 @@
 #include <cc/data.h>
 #include <config/timeouts.h>
 #include <dhcp/iface_mgr.h>
+#include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <http/date_time.h>
@@ -52,7 +53,7 @@ const int HAService::HA_CONTROL_RESULT_MAINTENANCE_NOT_ALLOWED;
 HAService::HAService(const IOServicePtr& io_service, const NetworkStatePtr& network_state,
                      const HAConfigPtr& config, const HAServerType& server_type)
     : io_service_(io_service), network_state_(network_state), config_(config),
-      server_type_(server_type), client_(*io_service), communication_state_(),
+      server_type_(server_type), client_(), listener_(), communication_state_(),
       query_filter_(config), mutex_(), pending_requests_(),
       lease_update_backlog_(config->getDelayedUpdatesLimit()) {
 
@@ -67,12 +68,18 @@ HAService::HAService(const IOServicePtr& io_service, const NetworkStatePtr& netw
 
     startModel(HA_WAITING_ST);
 
+    // Start client and/or listener.
+    startClientAndListener();
+
     LOG_INFO(ha_logger, HA_SERVICE_STARTED)
         .arg(HAConfig::HAModeToString(config->getHAMode()))
         .arg(HAConfig::PeerConfig::roleToString(config->getThisServerConfig()->getRole()));
 }
 
 HAService::~HAService() {
+    // Stop client and/or listener.
+    stopClientAndListener();
+
     network_state_->reset(NetworkState::Origin::HA_COMMAND);
 }
 
@@ -989,7 +996,7 @@ HAService::adjustNetworkState() {
 
 bool
 HAService::shouldPartnerDown() const {
-    // Checking whether the communication with the partner is ok is the
+    // Checking whether the communication with the partner is OK is the
     // first step towards verifying if the server is up.
     if (communication_state_->isCommunicationInterrupted()) {
         // If the communication is interrupted, we also have to check
@@ -1336,7 +1343,7 @@ HAService::asyncSendLeaseUpdate(const QueryPtrType& query,
                 }
             } else {
                 // This was a response from the backup server and we're configured to
-                // not wait for their ackowledgments, so there is nothing more to do.
+                // not wait for their acknowledgments, so there is nothing more to do.
                 return;
             }
 
@@ -1356,7 +1363,7 @@ HAService::asyncSendLeaseUpdate(const QueryPtrType& query,
     );
 
     // The number of pending requests is the number of requests for which we
-    // expect an acknowledgement prior to responding to the DHCP clients. If
+    // expect an acknowledgment prior to responding to the DHCP clients. If
     // we're configured to wait for the acks from the backups or it is not
     // a backup increase the number of pending requests.
     if (config_->amWaitingBackupAck() || (config->getRole() != HAConfig::PeerConfig::BACKUP)) {
@@ -1856,7 +1863,7 @@ HAService::asyncSyncLeases() {
         dhcp_disable_timeout = 1;
     }
 
-    asyncSyncLeases(client_, config_->getFailoverPeerConfig()->getName(),
+    asyncSyncLeases(*client_, config_->getFailoverPeerConfig()->getName(),
                     dhcp_disable_timeout, LeasePtr(), null_action);
 }
 
@@ -2404,7 +2411,7 @@ HAService::processMaintenanceNotify(const bool cancel) {
         // have a way to distinguish between the errors caused by the communication
         // issues and the cases when there is no communication error but the server
         // is not allowed to enter the in-maintenance state. In the former case, the
-        // parter would go to partner-down. In the case signaled by the special
+        // partner would go to partner-down. In the case signaled by the special
         // result code entering the maintenance state is not allowed.
         return (createAnswer(HA_CONTROL_RESULT_MAINTENANCE_NOT_ALLOWED,
                              "Unable to transition the server from the "
@@ -2704,7 +2711,7 @@ HAService::verifyAsyncResponse(const HttpResponsePtr& response, int& rcode) {
 bool
 HAService::clientConnectHandler(const boost::system::error_code& ec, int tcp_native_fd) {
 
-    // If things look ok register the socket with Interface Manager. Note
+    // If things look OK register the socket with Interface Manager. Note
     // we don't register if the FD is < 0 to avoid an exception throw.
     // It is unlikely that this will occur but we want to be liberal
     // and avoid issues.
@@ -2712,7 +2719,7 @@ HAService::clientConnectHandler(const boost::system::error_code& ec, int tcp_nat
         && (tcp_native_fd >= 0)) {
         // External socket callback is a NOP. Ready events handlers are
         // run by an explicit call IOService ready in kea-dhcp<n> code.
-        // We are registerin the socket only to interrupt main-thread
+        // We are registering the socket only to interrupt main-thread
         // select().
         IfaceMgr::instance().addExternalSocket(tcp_native_fd,
             std::bind(&HAService::socketReadyHandler, this, ph::_1)
@@ -2732,7 +2739,7 @@ HAService::socketReadyHandler(int tcp_native_fd) {
     // ongoing transactions, we close it.  This will unregister it from
     // IfaceMgr and ensure the client starts over with a fresh connection
     // if it needs to do so.
-    client_.closeIfOutOfBand(tcp_native_fd);
+    client_->closeIfOutOfBand(tcp_native_fd);
 }
 
 void
@@ -2773,6 +2780,55 @@ HAService::getPendingRequestInternal(const QueryPtrType& query) {
     }
 }
 
+void
+HAService::startClientAndListener() {
+    // If we're not configured for multi-threading, then we start
+    // a client in ST mode and return.
+    if (!config_->getEnableMultiThreading()) {
+        client_.reset(new HttpClient(*io_service_, 0));
+        return;
+    }
+
+    // Start a client in MT mode.
+    client_.reset(new HttpClient(*io_service_, 
+                  config_->getHttpClientThreads()));
+
+    // If we're configured to use our own listener create and start it.
+    if (config_->getHttpDedicatedListener()) {
+        // Get the server address and port from this server's URL.
+        auto my_url = config_->getThisServerConfig()->getUrl();
+        IOAddress server_address(IOAddress::IPV4_ZERO_ADDRESS());
+        try {
+            // Since we do not currently support hostname resolution,
+            // we need to make sure we have an IP address here.
+            server_address = IOAddress(my_url.getStrippedHostname());
+        } catch (const std::exception& ex) {
+            isc_throw(Unexpected, "server Url:" << my_url.getStrippedHostname()
+                      << " is not a valid IP address");
+        }
+
+        // Fetch how many threads the listener will use.
+        uint32_t listener_threads = config_->getHttpListenerThreads();
+
+        // Instantiate the listener.
+        listener_.reset(new CmdHttpListener(server_address, my_url.getPort(),
+                                            listener_threads));
+        // Start the listener listening.
+        listener_->start();
+    }
+}
+
+void
+HAService::stopClientAndListener() {
+    if (client_) {
+        client_->stop();
+    }
+
+    if (listener_) {
+        listener_->stop();
+    }
+}
+
 // Explicit instantiations.
 template int HAService::getPendingRequest(const Pkt4Ptr&);
 template int HAService::getPendingRequest(const Pkt6Ptr&);
index 8b33498f5875fc3675c52ddd18e6a74c4faaf822..41d9724c1b3a74fee34e5ac5af119d657799d379 100644 (file)
@@ -16,6 +16,7 @@
 #include <asiolink/io_service.h>
 #include <asiolink/tls_socket.h>
 #include <cc/data.h>
+#include <config/cmd_http_listener.h>
 #include <dhcp/pkt4.h>
 #include <http/response.h>
 #include <dhcpsrv/lease.h>
@@ -89,7 +90,8 @@ public:
     /// @brief Constructor.
     ///
     /// It clears the DHCP state using origin HA internal command and starts the
-    /// state model in waiting state.
+    /// state model in waiting state.  Creates and starts the client and the
+    /// listener (if one).
     ///
     /// @param io_service Pointer to the IO service used by the DHCP server.
     /// @param config Parsed HA hook library configuration.
@@ -103,7 +105,8 @@ public:
 
     /// @brief Destructor.
     ///
-    /// It clears the DHCP state using origin HA internal command.
+    /// Stops the client and listener (if one). It clears the DHCP
+    /// state using origin HA internal command.
     virtual ~HAService();
 
     /// @brief Returns HA server type used in object construction.
@@ -353,7 +356,7 @@ protected:
     /// @param state the new value to assign to the current state.
     void verboseTransition(const unsigned state);
 
-    /// @brief Returns normal operation state for the current configruation.
+    /// @brief Returns normal operation state for the current configuration.
     ///
     /// @return "load-balancing" for active servers in load balancing mode,
     /// "hot-standby" for active servers in hot-standby mode, "backup" for
@@ -895,7 +898,7 @@ protected:
     /// entering the load-balancing state. It ensures that all outstanding lease
     /// updates are sent to the partner before the server can continue normal
     /// operation in the load-balancing state. In order to prevent collisions
-    /// between new allocations and oustanding updates this method is synchronous.
+    /// between new allocations and outstanding updates this method is synchronous.
     ///
     /// This method creates its own instances of the HttpClient and IOService and
     /// invokes IOService::run().
@@ -944,12 +947,12 @@ public:
 
     /// @brief Processes ha-maintenance-notify command and returns a response.
     ///
-    /// This command attempts to tramsition the server to the in-maintenance state
+    /// This command attempts to transition the server to the in-maintenance state
     /// if the cancel flag is set to false. Such transition is not allowed if
     /// the server is currently in one of the following states:
     /// - backup: because maintenance is not supported for backup servers,
     /// - partner-in-maintenance: because only one server is in maintenance while
-    ///   the partner must be in parter-in-maintenance state,
+    ///   the partner must be in partner-in-maintenance state,
     /// - terminated: because the only way to resume HA service is by shutting
     ///   down the server, fixing the clock skew and restarting.
     ///
@@ -1000,6 +1003,9 @@ public:
     data::ConstElementPtr processMaintenanceCancel();
 
 protected:
+    void startClientAndListener();
+
+    void stopClientAndListener();
 
     /// @brief Checks if the response is valid or contains an error.
     ///
@@ -1020,7 +1026,7 @@ protected:
     ///
     /// @param ec Error status of the ASIO connect
     /// @param tcp_native_fd socket descriptor to register
-    /// @return always true. Registeration cannot fail, and if ec indicates a real
+    /// @return always true. Registration cannot fail, and if ec indicates a real
     /// error we want Connection logic to process it.
     bool clientConnectHandler(const boost::system::error_code& ec, int tcp_native_fd);
 
@@ -1073,8 +1079,12 @@ protected:
     /// @brief DHCP server type.
     HAServerType server_type_;
 
-    /// @brief HTTP client instance used to send lease updates.
-    http::HttpClient client_;
+    /// @brief HTTP client instance used to send HA commands and lease updates.
+    http::HttpClientPtr client_;
+
+    /// @brief HTTP listener instance used to receive and respond to HA commands
+    /// and lease updates.
+    config::CmdHttpListenerPtr listener_;
 
     /// @brief Holds communication state with a peer.
     CommunicationStatePtr communication_state_;
index 275963dc5ae838be43c6465a7e0d17a1f8bcd92f..322992a4bcb6ad42dd55d24123a8ba0c334d5cec 100644 (file)
@@ -32,6 +32,7 @@ ha_unittests_SOURCES += ha_config_unittest.cc
 ha_unittests_SOURCES += ha_impl_unittest.cc
 ha_unittests_SOURCES += ha_service_unittest.cc
 ha_unittests_SOURCES += ha_test.cc ha_test.h
+ha_unittests_SOURCES += ha_mt_unittest.cc
 ha_unittests_SOURCES += lease_update_backlog_unittest.cc
 ha_unittests_SOURCES += query_filter_unittest.cc
 ha_unittests_SOURCES += run_unittests.cc
diff --git a/src/hooks/dhcp/high_availability/tests/ha_mt_unittest.cc b/src/hooks/dhcp/high_availability/tests/ha_mt_unittest.cc
new file mode 100644 (file)
index 0000000..3c47568
--- /dev/null
@@ -0,0 +1,417 @@
+// Copyright (C) 2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/asio_wrapper.h>
+#include <ha_test.h>
+#include <ha_config.h>
+#include <ha_service.h>
+#include <ha_service_states.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/io_address.h>
+#include <asiolink/io_service.h>
+#include <cc/command_interpreter.h>
+#include <cc/data.h>
+#include <dhcp/classify.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/pkt4.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/network_state.h>
+#include <dhcpsrv/subnet_id.h>
+#include <hooks/parking_lots.h>
+#include <http/basic_auth_config.h>
+#include <http/date_time.h>
+#include <http/http_types.h>
+#include <http/listener.h>
+#include <http/post_request_json.h>
+#include <http/response_creator.h>
+#include <http/response_creator_factory.h>
+#include <http/response_json.h>
+#include <util/multi_threading_mgr.h>
+#include <testutils/gtest_utils.h>
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/pointer_cast.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <functional>
+#include <sstream>
+#include <set>
+#include <string>
+#include <vector>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::ha;
+using namespace isc::ha::test;
+using namespace isc::hooks;
+using namespace isc::http;
+using namespace isc::util;
+
+namespace {
+
+/// @brief IP address to which HTTP service is bound.
+const std::string SERVER_ADDRESS = "127.0.0.1";
+
+/// @brief Port number to which HTTP service is bound.
+const unsigned short SERVER_PORT = 18123;
+
+/// @brief Request Timeout used in most of the tests (ms).
+const long REQUEST_TIMEOUT = 10000;
+
+/// @brief Persistent connection idle timeout used in most of the tests (ms).
+const long IDLE_TIMEOUT = 10000;
+
+/// @brief Test timeout (ms).
+const long TEST_TIMEOUT = 10000;
+
+/// @brief Generates IPv4 leases to be used by the tests.
+///
+/// @param [out] leases reference to the container where leases are stored.
+void generateTestLeases(std::vector<Lease4Ptr>& leases) {
+    for (uint8_t i = 1; i <= 10; ++i) {
+        uint32_t lease_address = 0xC0000201 + 256 * i;
+        std::vector<uint8_t> hwaddr(6, i);
+        Lease4Ptr lease(new Lease4(IOAddress(lease_address),
+                                   HWAddrPtr(new HWAddr(hwaddr, HTYPE_ETHER)),
+                                   ClientIdPtr(),
+                                   60,
+                                   static_cast<time_t>(1000 + i),
+                                   SubnetID(i)));
+        leases.push_back(lease);
+    }
+}
+
+/// @brief Generates IPv6 leases to be used by the tests.
+///
+/// @param [out] leases reference to the container where leases are stored.
+void generateTestLeases(std::vector<Lease6Ptr>& leases) {
+    std::vector<uint8_t> address_bytes = IOAddress("2001:db8:1::1").toBytes();
+    for (uint8_t i = 1; i <= 10; ++i) {
+        DuidPtr duid(new DUID(std::vector<uint8_t>(10, i)));
+        address_bytes[6] += i;
+        Lease6Ptr lease(new Lease6(Lease::TYPE_NA,
+                                   IOAddress::fromBytes(AF_INET6, &address_bytes[0]),
+                                   duid, 1, 50, 60, SubnetID(i)));
+        leases.push_back(lease);
+    }
+}
+
+/// @brief Returns generated leases in JSON format.
+///
+/// @tparam LeasesVec vector of IPv4 or IPv6 lease pointers.
+/// @param leases reference to the container holding leases to be
+/// converted to JSON format.
+template<typename LeasesVec>
+ConstElementPtr getLeasesAsJson(const LeasesVec& leases) {
+    ElementPtr leases_json = Element::createList();
+    for (auto l = leases.begin(); l != leases.end(); ++l) {
+        leases_json->add((*l)->toElement());
+    }
+    return (leases_json);
+}
+
+/// @brief Derivation of the @c HAService which provides access to
+/// protected methods and members.
+class TestHAService : public HAService {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service Pointer to the IO service used by the DHCP server.
+    /// @param network_state Object holding state of the DHCP service
+    /// (enabled/disabled).
+    /// @param config Parsed HA hook library configuration.
+    /// @param server_type Server type, i.e. DHCPv4 or DHCPv6 server.
+    TestHAService(const IOServicePtr& io_service,
+                  const NetworkStatePtr& network_state,
+                  const HAConfigPtr& config,
+                  const HAServerType& server_type = HAServerType::DHCPv4)
+        : HAService(io_service, network_state, config, server_type) {
+    }
+
+    /// @brief Test version of the @c HAService::runModel.
+    ///
+    /// The original implementation of this method returns control when
+    /// @c NOP_EVT is found. This implementation runs a
+    /// single handler to allow the tests to verify if the state machine
+    /// transitions to an expected state before it is run again.
+    virtual void runModel(unsigned int event) {
+        try {
+            postNextEvent(event);
+            getState(getCurrState())->run();
+
+        } catch (const std::exception& ex) {
+            abortModel(ex.what());
+        }
+    }
+
+    /// @brief Schedules asynchronous "dhcp-disable" command to the specified
+    /// server.
+    ///
+    /// This variant of the method uses default HTTP client for communication.
+    ///
+    /// @param server_name name of the server to which the command should be
+    /// sent.
+    /// @param max_period maximum number of seconds for which the DHCP service
+    /// should be disabled.
+    /// @param post_request_action pointer to the function to be executed when
+    /// the request is completed.
+    void asyncDisableDHCPService(const std::string& server_name,
+                                 const unsigned int max_period,
+                                 const PostRequestCallback& post_request_action) {
+        HAService::asyncDisableDHCPService(*client_, server_name, max_period,
+                                           post_request_action);
+    }
+
+    /// @brief Schedules asynchronous "dhcp-enable" command to the specified
+    /// server.
+    ///
+    /// This variant of the method uses default HTTP client for communication.
+    ///
+    /// @param server_name name of the server to which the command should be
+    /// sent.
+    /// @param post_request_action pointer to the function to be executed when
+    /// the request is completed.
+    void asyncEnableDHCPService(const std::string& server_name,
+                                const PostRequestCallback& post_request_action) {
+        HAService::asyncEnableDHCPService(*client_, server_name, post_request_action);
+    }
+
+    using HAService::asyncSendHeartbeat;
+    using HAService::asyncSyncLeases;
+    using HAService::postNextEvent;
+    using HAService::transition;
+    using HAService::verboseTransition;
+    using HAService::shouldSendLeaseUpdates;
+    using HAService::shouldQueueLeaseUpdates;
+    using HAService::pendingRequestSize;
+    using HAService::getPendingRequest;
+    using HAService::network_state_;
+    using HAService::config_;
+    using HAService::communication_state_;
+    using HAService::query_filter_;
+    using HAService::lease_update_backlog_;
+    using HAService::client_;
+    using HAService::listener_;
+};
+
+/// @brief Pointer to the @c TestHAService.
+typedef boost::shared_ptr<TestHAService> TestHAServicePtr;
+
+/// @brief Test fixture class for @c HAService multi-threading.
+class HAMtServiceTest : public HATest {
+public:
+
+    /// @brief Constructor.
+    HAMtServiceTest()
+        : HATest() {
+        MultiThreadingMgr::instance().setMode(true);
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Stops all test servers.
+    ~HAMtServiceTest() {
+        io_service_->get_io_service().reset();
+        io_service_->poll();
+        MultiThreadingMgr::instance().setMode(false);
+    }
+
+    /// @brief Callback function invoke upon test timeout.
+    ///
+    /// It stops the IO service and reports test timeout.
+    ///
+    /// @param fail_on_timeout Specifies if test failure should be reported.
+    void timeoutHandler(const bool fail_on_timeout) {
+        if (fail_on_timeout) {
+            ADD_FAILURE() << "Timeout occurred while running the test!";
+        }
+        io_service_->stop();
+    }
+};
+
+// Verifies permuations of HA+MT configuration and start-up.
+TEST_F(HAMtServiceTest, multiThreadingStartup) {
+
+    // Structure describing a test scenario.
+    struct Scenario {
+        std::string desc_;              // Description of the scenario.
+        std::string mt_json_;           // multi-threading config to use.
+        bool dhcp_mt_enabled_;          // True if DHCP multi-threading is enabled.
+        uint32_t dhcp_threads_;         // Value of DHCP thread-pool-size.
+        bool exp_ha_mt_enabled_;        // If HA+MT should be enabled
+        bool exp_listener_;             // if HA+MT should use dedicated listener.
+        uint32_t exp_listener_threads_; // Expected number of listener threads.
+        uint32_t exp_client_threads_;   // Expected number of client threads.
+    };
+
+    // Mnemonic constants.
+    bool dhcp_mt = true;
+    bool ha_mt = true;
+    bool listener = true;
+
+    // Number of threads the system reports as supported.
+    uint32_t sys_threads = MultiThreadingMgr::detectThreadCount();
+
+    std::vector<Scenario> scenarios {
+        {
+            "1 no ha+mt/default",
+            "",
+            dhcp_mt, 4,
+            !ha_mt, !listener, 0, 0
+        },
+        {
+            "2 dhcp mt enabled, ha mt disabled",
+            makeHAMtJson(!ha_mt, !listener, 0, 0),
+            dhcp_mt, 4,
+            !ha_mt, !listener, 0, 0
+        },
+        {
+            "3 dhcp mt disabled, mt enabled",
+            makeHAMtJson(ha_mt, listener, 0, 0),
+            !dhcp_mt, 4,
+            !ha_mt, !listener, 0, 0
+        },
+        {
+            "4 dhcp mt enabled, mt enabled, listener disabled",
+            makeHAMtJson(ha_mt, !listener, 0, 0),
+            dhcp_mt, 4,
+            ha_mt, !listener, 4, 4
+        },
+        {
+            "5 dhcp mt enabled, mt enabled, listener enabled",
+            makeHAMtJson(ha_mt, !listener, 0, 0),
+            dhcp_mt, 4,
+            ha_mt, !listener, 4, 4
+        },
+        {
+            "6 explicit DHCP threads, explicit thread values",
+            makeHAMtJson(ha_mt, listener, 5, 6),
+            dhcp_mt, 4,
+            ha_mt, listener, 5, 6
+        }
+        ,
+        {
+            "7 explicit DHCP threads, zero thread values",
+            makeHAMtJson(ha_mt, listener, 0, 0),
+            dhcp_mt, 8,
+            ha_mt, listener, 8, 8
+        },
+        {
+            "8 DHCP auto detect threads, zero thread values",
+            // Special case: if system reports supported threads as 0
+            // then HA+MT should be disabled.  Otherwise it should
+            // be enabled with listener and client threads set to the
+            // reported value.
+            makeHAMtJson(ha_mt, listener, 0, 0),
+            dhcp_mt, 0,
+            (sys_threads > 0), listener, sys_threads, sys_threads
+        }
+    };
+
+    // Iterate over the scenarios.
+    for (auto const& scenario : scenarios) {
+        SCOPED_TRACE(scenario.desc_);
+
+        // Build the HA JSON configuration.
+        std::stringstream ss;
+        ss <<
+            "["
+            "    {"
+            "        \"this-server-name\": \"server1\","
+            "        \"mode\": \"passive-backup\","
+            "        \"wait-backup-ack\": true,"
+            "        \"peers\": ["
+            "            {"
+            "                \"name\": \"server1\","
+            "                \"url\": \"http://127.0.0.1:8080/\","
+            "                \"role\": \"primary\""
+            "            },"
+            "            {"
+            "                \"name\": \"server2\","
+            "                \"url\": \"http://127.0.0.1:8081/\","
+            "                \"role\": \"backup\""
+            "            }"
+            "        ]";
+
+        if (!scenario.mt_json_.empty()) {
+            ss << "," << scenario.mt_json_;
+        }
+
+        ss << "}]";
+        ConstElementPtr config_json;
+        ASSERT_NO_THROW_LOG(config_json = Element::fromJSON(ss.str()));
+
+        // Set DHCP multi-threading configuration in CfgMgr.
+        setDHCPMultiThreadingConfig(scenario.dhcp_mt_enabled_, scenario.dhcp_threads_);
+        /// @todo this is a hack... we have chicken-egg...  CmdHttpListener won't
+        /// start if MT is not enabled BUT that happens after config hook point
+        MultiThreadingMgr::instance().setMode(scenario.dhcp_mt_enabled_);
+
+        // Create the HA configuration
+        HAConfigPtr ha_config(new HAConfig());
+        HAConfigParser parser;
+        ASSERT_NO_THROW_LOG(parser.parse(ha_config, config_json));
+
+        // Instanatiate the service.
+        TestHAServicePtr service;
+        ASSERT_NO_THROW_LOG(service.reset(new TestHAService(io_service_, network_state_,
+                                                            ha_config)));
+        // Verify the configuration is as expected.
+        if (!scenario.exp_ha_mt_enabled_) {
+            // When HA+MT is disabled, client should be single-threaded.
+            ASSERT_TRUE(service->client_);
+            EXPECT_FALSE(service->client_->getThreadIOService());
+            EXPECT_EQ(service->client_->getThreadPoolSize(), 0);
+            EXPECT_EQ(service->client_->getThreadCount(), 0);
+
+            // Listener should not exist.
+            ASSERT_FALSE(service->listener_);
+            continue;
+        }
+
+        // Multi-threading should be enabled.
+        ASSERT_TRUE(ha_config->getEnableMultiThreading());
+
+        // When HA+MT is enabled, client should be multi-threaded.
+        ASSERT_TRUE(service->client_);
+        EXPECT_TRUE(service->client_->getThreadIOService());
+        EXPECT_EQ(service->client_->getThreadPoolSize(), scenario.exp_client_threads_);
+        // Currently thread count should be the same as thread pool size.  This might
+        // change if we go to so some sort of dynamic thread instance management.
+        EXPECT_EQ(service->client_->getThreadCount(), scenario.exp_client_threads_);
+
+        if (!scenario.exp_listener_) {
+            // We should not have a listener.
+            ASSERT_FALSE(service->listener_);
+            continue;
+        }
+
+        // We sould have a listening listener with the expected number of threads.
+        ASSERT_TRUE(service->listener_);
+        EXPECT_TRUE(service->listener_->isListening());
+        EXPECT_EQ(service->listener_->getThreadPoolSize(), scenario.exp_listener_threads_);
+        // Currently thread count should be the same as thread pool size.  This might
+        // change if we go to so some sort of dynamic thread instance management.
+        EXPECT_EQ(service->listener_->getThreadCount(), scenario.exp_listener_threads_);
+    }
+}
+
+TEST_F(HAMtServiceTest, twoServicesFun) {
+}
+
+} // end of anonymous namespace
index 995ea3b0470ad540bf7dd56de5d5ae7f27d394e5..5abea871df5f6b2c760b2f48fae13218b3c10d13 100644 (file)
@@ -22,6 +22,7 @@
 #include <dhcp/duid.h>
 #include <dhcp/hwaddr.h>
 #include <dhcp/pkt4.h>
+#include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
@@ -37,6 +38,7 @@
 #include <http/response_creator_factory.h>
 #include <http/response_json.h>
 #include <util/multi_threading_mgr.h>
+#include <testutils/gtest_utils.h>
 
 #include <boost/date_time/posix_time/posix_time.hpp>
 #include <boost/pointer_cast.hpp>
@@ -59,6 +61,10 @@ using namespace isc::hooks;
 using namespace isc::http;
 using namespace isc::util;
 
+/// @file The tests herein were created prior to HA+MT but are very valuable
+/// for testing HA single-threaded operation and overall HA behavior.
+/// HA+MT testing is done elsewhere.
+
 namespace {
 
 /// @brief IP address to which HTTP service is bound.
@@ -171,7 +177,7 @@ public:
     void asyncDisableDHCPService(const std::string& server_name,
                                  const unsigned int max_period,
                                  const PostRequestCallback& post_request_action) {
-        HAService::asyncDisableDHCPService(client_, server_name, max_period,
+        HAService::asyncDisableDHCPService(*client_, server_name, max_period,
                                            post_request_action);
     }
 
@@ -186,7 +192,7 @@ public:
     /// the request is completed.
     void asyncEnableDHCPService(const std::string& server_name,
                                 const PostRequestCallback& post_request_action) {
-        HAService::asyncEnableDHCPService(client_, server_name, post_request_action);
+        HAService::asyncEnableDHCPService(*client_, server_name, post_request_action);
     }
 
     using HAService::asyncSendHeartbeat;
@@ -203,6 +209,8 @@ public:
     using HAService::communication_state_;
     using HAService::query_filter_;
     using HAService::lease_update_backlog_;
+    using HAService::client_;
+    using HAService::listener_;
 };
 
 /// @brief Pointer to the @c TestHAService.
@@ -626,6 +634,25 @@ public:
         io_service_->stop();
     }
 
+    /// @brief Creates a TestHAService instance with HA+MT disabled.
+    ///
+    /// @param network_state Object holding state of the DHCP service
+    /// (enabled/disabled).
+    /// @param config Parsed HA hook library configuration.
+    /// @param server_type server type, i.e. DHCPv4 or DHCPv6.
+    void createSTService(
+              const NetworkStatePtr& state,
+              const HAConfigPtr config,
+              const HAServerType& server_type = HAServerType::DHCPv4) {
+
+        ASSERT_FALSE(config->getEnableMultiThreading());
+        ASSERT_NO_THROW_LOG(service_.reset(new TestHAService(io_service_, state,
+                                           config, server_type)));
+        ASSERT_TRUE(service_->client_);
+        ASSERT_FALSE(service_->client_->getThreadIOService());
+        ASSERT_FALSE(service_->listener_);
+    }
+
     /// @brief Generates IPv4 leases to be used by the tests.
     void generateTestLeases4() {
         generateTestLeases(leases4_);
@@ -820,7 +847,7 @@ public:
         state->modifyPokeTime(-30);
 
         // Create HA service and schedule lease updates.
-        service_.reset(new TestHAService(io_service_, network_state_, config_storage));
+        createSTService(network_state_, config_storage);
         service_->communication_state_ = state;
 
         service_->transition(my_state.state_, HAService::NOP_EVT);
@@ -922,8 +949,7 @@ public:
         state->modifyPokeTime(-30);
 
         // Create HA service and schedule lease updates.
-        service_.reset(new TestHAService(io_service_, network_state_, config_storage,
-                                         HAServerType::DHCPv6));
+        createSTService(network_state_, config_storage, HAServerType::DHCPv6);
         service_->communication_state_ = state;
 
         service_->transition(my_state.state_, HAService::NOP_EVT);
@@ -4763,7 +4789,7 @@ class HAServiceStateMachineTest : public HAServiceTest {
 public:
     /// @brief Constructor.
     HAServiceStateMachineTest()
-        : HAServiceTest(), service_(), state_(),
+        : HAServiceTest(), state_(),
           partner_(new HAPartner(listener2_, factory2_)) {
     }
 
@@ -4778,8 +4804,7 @@ public:
                       const HAServerType& server_type = HAServerType::DHCPv4) {
         config->setHeartbeatDelay(1);
         config->setSyncPageLimit(1000);
-        service_.reset(new TestHAService(io_service_, network_state_, config,
-                                         server_type));
+        createSTService(network_state_, config, server_type);
         // Replace default communication state with custom state which exposes
         // protected members and methods.
         state_.reset(new NakedCommunicationState4(io_service_, config));
@@ -5121,8 +5146,6 @@ public:
         return (isDoingHeartbeat());
     }
 
-    /// @brief Pointer to the HA service under test.
-    TestHAServicePtr service_;
     /// @brief Pointer to the communication state used in the tests.
     NakedCommunicationState4Ptr state_;
     /// @brief Pointer to the partner used in some tests.
@@ -7411,4 +7434,4 @@ TEST_F(HAServiceStateMachineTest, shouldSendLeaseUpdatesPassiveBackup) {
     EXPECT_TRUE(expectLeaseUpdates(MyState(HA_WAITING_ST), peer_config));
 }
 
-}
+} // end of anonymous namespace
index a9191bb51e9846c7c72008e83c59d9456bf457b6..9473076722352ebc42cf769b21fc1396fb1b1405 100644 (file)
 namespace isc {
 namespace config {
 
+/// @brief A multi-threaded HTTP listener that can process API commands
+/// requests.
+///
+/// This class will listen for Command API client requests on a given
+/// IP address and port.  It uses its own IOService instance to drive
+/// a thread-pool which can service multiple connections concurrently.  
+/// The number of concurrent connections is currently limited to the
+/// configured thread pool size.
+///
+/// @note This class is NOT compatible with Kea core single-threading.
+/// It is incumbant upon the owner to ensure the Kea core multi-threading
+/// is (or will be) enabled when creating instances of this class.
 class CmdHttpListener {
 public:
     /// @brief Constructor