Browse Source

[master] Merge branch 'trac2212-2'

JINMEI Tatuya 12 years ago
parent
commit
9281b972a8

+ 14 - 0
src/bin/auth/auth_messages.mes

@@ -61,6 +61,15 @@ the message.
 A debug message, showing when the separate thread for maintaining data
 source clients receives a command from the manager.
 
+% AUTH_DATASRC_CLIENTS_BUILDER_COMMAND_ERROR command execution failure: %1
+The separate thread for maintaining data source clients failed to complete a
+command given by the main thread.  In most cases this is some kind of
+configuration or temporary error such as an attempt to load a non-existent
+zone or a temporary DB connection failure.  So the event is just logged and
+the thread keeps running.  In some rare cases, however, this may indicate an
+internal bug and it may be better to restart the entire program, so the log
+message should be carefully examined.
+
 % AUTH_DATASRC_CLIENTS_BUILDER_FAILED data source builder thread stopped due to an exception: %1
 The separate thread for maintaining data source clients has been
 terminated due to some uncaught exception.  When this happens, the
@@ -79,6 +88,11 @@ indicate some run time failure than program errors.  As in the other
 failure case, the thread terminates the entire process immediately
 after logging this message.
 
+% AUTH_DATASRC_CLIENTS_BUILDER_LOAD_ZONE loaded zone %1/%2
+This debug message is issued when the separate thread for maintaining data
+source clients successfully loaded the named zone of the named class as a
+result of the 'loadzone' command.
+
 % AUTH_DATASRC_CLIENTS_BUILDER_RECONFIGURE_CONFIG_ERROR Error in data source configuration: %1
 The thread for maintaining data source clients has received a command to
 reconfigure, but the parameter data (the new configuration) contains an

+ 124 - 2
src/bin/auth/datasrc_clients_mgr.h

@@ -27,6 +27,7 @@
 
 #include <datasrc/data_source.h>
 #include <datasrc/client_list.h>
+#include <datasrc/memory/zone_writer.h>
 
 #include <auth/auth_log.h>
 #include <auth/datasrc_config.h>
@@ -37,6 +38,7 @@
 #include <boost/noncopyable.hpp>
 
 #include <exception>
+#include <cassert>
 #include <list>
 #include <utility>
 
@@ -55,6 +57,9 @@ enum CommandID {
     RECONFIGURE,  ///< Reconfigure the datasource client lists,
                   ///  the argument to the command is the full new
                   ///  datasources configuration.
+    LOADZONE,     ///< Load a new version of zone into a memory,
+                  ///  the argument to the command is a map containing 'class'
+                  ///  and 'origin' elements, both should have been validated.
     SHUTDOWN,     ///< Shutdown the builder; no argument
     NUM_COMMANDS
 };
@@ -290,7 +295,23 @@ namespace datasrc_clientmgr_internal {
 /// threads or locks.
 template <typename MutexType, typename CondVarType>
 class DataSrcClientsBuilderBase : boost::noncopyable {
+private:
+    typedef std::map<dns::RRClass,
+                     boost::shared_ptr<datasrc::ConfigurableClientList> >
+    ClientListsMap;
+
 public:
+    /// \brief Internal errors in handling commands.
+    ///
+    /// This exception is expected to be caught within the
+    /// \c DataSrcClientsBuilder implementation, but is defined as public
+    /// so tests can be checked it.
+    class InternalCommandError : public isc::Exception {
+    public:
+        InternalCommandError(const char* file, size_t line, const char* what) :
+            isc::Exception(file, line, what) {}
+    };
+
     /// \brief Constructor.
     ///
     /// It simply sets up a local copy of shared data with the manager.
@@ -365,6 +386,11 @@ private:
         }
     }
 
+    void doLoadZone(const isc::data::ConstElementPtr& arg);
+    boost::shared_ptr<datasrc::memory::ZoneWriter> getZoneWriter(
+        datasrc::ConfigurableClientList& client_list,
+        const dns::RRClass& rrclass, const dns::Name& origin);
+
     // The following are shared with the manager
     std::list<Command>* command_queue_;
     CondVarType* cond_;
@@ -397,7 +423,13 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::run() {
             } // the lock is released here.
 
             while (keep_running && !current_commands.empty()) {
-                keep_running = handleCommand(current_commands.front());
+                try {
+                    keep_running = handleCommand(current_commands.front());;
+                } catch (const InternalCommandError& e) {
+                    LOG_ERROR(auth_logger,
+                              AUTH_DATASRC_CLIENTS_BUILDER_COMMAND_ERROR).
+                        arg(e.what());
+                }
                 current_commands.pop_front();
             }
         }
@@ -426,7 +458,7 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::handleCommand(
     }
 
     const boost::array<const char*, NUM_COMMANDS> command_desc = {
-        {"NOOP", "RECONFIGURE", "SHUTDOWN"}
+        {"NOOP", "RECONFIGURE", "LOADZONE", "SHUTDOWN"}
     };
     LOG_DEBUG(auth_logger, DBGLVL_TRACE_BASIC,
               AUTH_DATASRC_CLIENTS_BUILDER_COMMAND).arg(command_desc.at(cid));
@@ -434,6 +466,9 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::handleCommand(
     case RECONFIGURE:
         doReconfigure(command.second);
         break;
+    case LOADZONE:
+        doLoadZone(command.second);
+        break;
     case SHUTDOWN:
         return (false);
     case NOOP:
@@ -444,6 +479,93 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::handleCommand(
     }
     return (true);
 }
+
+template <typename MutexType, typename CondVarType>
+void
+DataSrcClientsBuilderBase<MutexType, CondVarType>::doLoadZone(
+    const isc::data::ConstElementPtr& arg)
+{
+    // We assume some basic level validation as this method can only be
+    // called via the manager in practice.  manager is expected to do the
+    // minimal validation.
+    assert(arg);
+    assert(arg->get("class"));
+    assert(arg->get("origin"));
+
+    const dns::RRClass rrclass(arg->get("class")->stringValue());
+    const dns::Name origin(arg->get("origin")->stringValue());
+    ClientListsMap::iterator found = (*clients_map_)->find(rrclass);
+    if (found == (*clients_map_)->end()) {
+        isc_throw(InternalCommandError, "failed to load a zone " << origin <<
+                  "/" << rrclass << ": not configured for the class");
+    }
+
+    boost::shared_ptr<datasrc::ConfigurableClientList> client_list =
+        found->second;
+    assert(client_list);
+
+    try {
+        boost::shared_ptr<datasrc::memory::ZoneWriter> zwriter =
+            getZoneWriter(*client_list, rrclass, origin);
+
+        zwriter->load(); // this can take time but doesn't cause a race
+        {   // install() can cause a race and must be in a critical section
+            typename MutexType::Locker locker(*map_mutex_);
+            zwriter->install();
+        }
+        LOG_DEBUG(auth_logger, DBG_AUTH_OPS,
+                  AUTH_DATASRC_CLIENTS_BUILDER_LOAD_ZONE)
+            .arg(origin).arg(rrclass);
+
+        // same as load(). We could let the destructor do it, but do it
+        // ourselves explicitly just in case.
+        zwriter->cleanup();
+    } catch (const InternalCommandError& ex) {
+        throw;     // this comes from getZoneWriter.  just let it go through.
+    } catch (const isc::Exception& ex) {
+        // We catch our internal exceptions (which will be just ignored) and
+        // propagated others (which should generally be considered fatal and
+        // will make the thread terminate)
+        isc_throw(InternalCommandError, "failed to load a zone " << origin <<
+                  "/" << rrclass << ": error occurred in reload: " <<
+                  ex.what());
+    }
+}
+
+// A dedicated subroutine of doLoadZone().  Separated just for keeping the
+// main method concise.
+template <typename MutexType, typename CondVarType>
+boost::shared_ptr<datasrc::memory::ZoneWriter>
+DataSrcClientsBuilderBase<MutexType, CondVarType>::getZoneWriter(
+    datasrc::ConfigurableClientList& client_list,
+    const dns::RRClass& rrclass, const dns::Name& origin)
+{
+    const datasrc::ConfigurableClientList::ZoneWriterPair writerpair =
+        client_list.getCachedZoneWriter(origin);
+
+    switch (writerpair.first) {
+    case datasrc::ConfigurableClientList::ZONE_RELOADED: // XXX misleading name
+        assert(writerpair.second);
+        return (writerpair.second);
+    case datasrc::ConfigurableClientList::ZONE_NOT_FOUND:
+        isc_throw(InternalCommandError, "failed to load zone " << origin
+                  << "/" << rrclass << ": not found in any configured "
+                  "data source.");
+    case datasrc::ConfigurableClientList::ZONE_NOT_CACHED:
+        isc_throw(InternalCommandError, "failed to load zone " << origin
+                  << "/" << rrclass << ": not served from memory");
+    case datasrc::ConfigurableClientList::CACHE_DISABLED:
+        // This is an internal error. Auth server must have the cache
+        // enabled.
+        isc_throw(InternalCommandError, "failed to load zone " << origin
+                  << "/" << rrclass << ": internal failure, in-memory cache "
+                  "is somehow disabled");
+    }
+
+    // all cases above should either return or throw, but some compilers
+    // still need a return statement
+    return (boost::shared_ptr<datasrc::memory::ZoneWriter>());
+}
 } // namespace datasrc_clientmgr_internal
 
 /// \brief Shortcut type for normal data source clients manager.

+ 340 - 13
src/bin/auth/tests/datasrc_clients_builder_unittest.cc

@@ -12,36 +12,62 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 
+#include <util/unittests/check_valgrind.h>
+
+#include <dns/name.h>
+#include <dns/rrclass.h>
+
 #include <cc/data.h>
 
+#include <datasrc/client.h>
+#include <datasrc/factory.h>
+
 #include <auth/datasrc_clients_mgr.h>
+#include <auth/datasrc_config.h>
+
+#include <testutils/dnsmessage_test.h>
+
 #include "test_datasrc_clients_mgr.h"
+#include "datasrc_util.h"
 
 #include <gtest/gtest.h>
 
 #include <boost/function.hpp>
 
+#include <cstdlib>
+#include <string>
+#include <sstream>
+
 using isc::data::ConstElementPtr;
+using namespace isc::dns;
+using namespace isc::data;
 using namespace isc::datasrc;
 using namespace isc::auth::datasrc_clientmgr_internal;
+using namespace isc::auth::unittest;
+using namespace isc::testutils;
 
 namespace {
 class DataSrcClientsBuilderTest : public ::testing::Test {
 protected:
     DataSrcClientsBuilderTest() :
+        clients_map(new std::map<RRClass,
+                    boost::shared_ptr<ConfigurableClientList> >),
         builder(&command_queue, &cond, &queue_mutex, &clients_map, &map_mutex),
-        cond(command_queue, delayed_command_queue),
+        cond(command_queue, delayed_command_queue), rrclass(RRClass::IN()),
         shutdown_cmd(SHUTDOWN, ConstElementPtr()),
         noop_cmd(NOOP, ConstElementPtr())
     {}
 
-    TestDataSrcClientsBuilder builder;
+    void configureZones();      // used for loadzone related tests
+
+    ClientListMapPtr clients_map; // configured clients
     std::list<Command> command_queue; // test command queue
     std::list<Command> delayed_command_queue; // commands available after wait
-    ClientListMapPtr clients_map; // configured clients
+    TestDataSrcClientsBuilder builder;
     TestCondVar cond;
     TestMutex queue_mutex;
     TestMutex map_mutex;
+    const RRClass rrclass;
     const Command shutdown_cmd;
     const Command noop_cmd;
 };
@@ -74,13 +100,20 @@ TEST_F(DataSrcClientsBuilderTest, exception) {
     // them.  Right now, we simply abort to prevent the system from running
     // with half-broken state.  Eventually we should introduce a better
     // error handling.
-    command_queue.push_back(noop_cmd);
-    queue_mutex.throw_from_noop = TestMutex::EXCLASS;
-    EXPECT_DEATH_IF_SUPPORTED({builder.run();}, "");
+    if (!isc::util::unittests::runningOnValgrind()) {
+        command_queue.push_back(noop_cmd);
+        queue_mutex.throw_from_noop = TestMutex::EXCLASS;
+        EXPECT_DEATH_IF_SUPPORTED({builder.run();}, "");
+
+        command_queue.push_back(noop_cmd);
+        queue_mutex.throw_from_noop = TestMutex::INTEGER;
+        EXPECT_DEATH_IF_SUPPORTED({builder.run();}, "");
+    }
 
     command_queue.push_back(noop_cmd);
-    queue_mutex.throw_from_noop = TestMutex::INTEGER;
-    EXPECT_DEATH_IF_SUPPORTED({builder.run();}, "");
+    command_queue.push_back(shutdown_cmd); // we need to stop the loop
+    queue_mutex.throw_from_noop = TestMutex::INTERNAL;
+    builder.run();
 }
 
 TEST_F(DataSrcClientsBuilderTest, condWait) {
@@ -106,10 +139,10 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
     Command reconfig_cmd(RECONFIGURE, ConstElementPtr());
 
     // Initially, no clients should be there
-    EXPECT_EQ(ClientListMapPtr(), clients_map);
+    EXPECT_TRUE(clients_map->empty());
 
     // A config that doesn't do much except be accepted
-    ConstElementPtr good_config = isc::data::Element::fromJSON(
+    ConstElementPtr good_config = Element::fromJSON(
         "{"
         "\"IN\": [{"
         "   \"type\": \"MasterFiles\","
@@ -121,7 +154,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
 
     // A configuration that is 'correct' in the top-level, but contains
     // bad data for the type it specifies
-    ConstElementPtr bad_config = isc::data::Element::fromJSON(
+    ConstElementPtr bad_config = Element::fromJSON(
         "{"
         "\"IN\": [{"
         "   \"type\": \"MasterFiles\","
@@ -142,7 +175,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
     // If a 'bad' command argument got here, the config validation should
     // have failed already, but still, the handler should return true,
     // and the clients_map should not be updated.
-    reconfig_cmd.second = isc::data::Element::create("{ \"foo\": \"bar\" }");
+    reconfig_cmd.second = Element::create("{ \"foo\": \"bar\" }");
     EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
     EXPECT_EQ(working_config_clients, clients_map);
     // Building failed, so map mutex should not have been locked again
@@ -173,7 +206,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
     EXPECT_EQ(2, map_mutex.lock_count);
 
     // And finally, try an empty config to disable all datasource clients
-    reconfig_cmd.second = isc::data::Element::createMap();
+    reconfig_cmd.second = Element::createMap();
     EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
     EXPECT_EQ(0, clients_map->size());
     EXPECT_EQ(3, map_mutex.lock_count);
@@ -193,4 +226,298 @@ TEST_F(DataSrcClientsBuilderTest, badCommand) {
                  isc::Unexpected);
 }
 
+// A helper function commonly used for the "loadzone" command tests.
+// It configures the given data source client lists with a memory data source
+// containing two zones, and checks the zones are correctly loaded.
+void
+zoneChecks(ClientListMapPtr clients_map, RRClass rrclass) {
+    EXPECT_EQ(ZoneFinder::SUCCESS, clients_map->find(rrclass)->second->
+              find(Name("ns.test1.example")).finder_->
+              find(Name("ns.test1.example"), RRType::A())->code);
+    EXPECT_EQ(ZoneFinder::NXRRSET, clients_map->find(rrclass)->second->
+              find(Name("ns.test1.example")).finder_->
+              find(Name("ns.test1.example"), RRType::AAAA())->code);
+    EXPECT_EQ(ZoneFinder::SUCCESS, clients_map->find(rrclass)->second->
+              find(Name("ns.test2.example")).finder_->
+              find(Name("ns.test2.example"), RRType::A())->code);
+    EXPECT_EQ(ZoneFinder::NXRRSET, clients_map->find(rrclass)->second->
+              find(Name("ns.test2.example")).finder_->
+              find(Name("ns.test2.example"), RRType::AAAA())->code);
+}
+
+// Another helper that checks after completing loadzone command.
+void
+newZoneChecks(ClientListMapPtr clients_map, RRClass rrclass) {
+    EXPECT_EQ(ZoneFinder::SUCCESS, clients_map->find(rrclass)->second->
+              find(Name("ns.test1.example")).finder_->
+              find(Name("ns.test1.example"), RRType::A())->code);
+    // now test1.example should have ns/AAAA
+    EXPECT_EQ(ZoneFinder::SUCCESS, clients_map->find(rrclass)->second->
+              find(Name("ns.test1.example")).finder_->
+              find(Name("ns.test1.example"), RRType::AAAA())->code);
+
+    // test2.example shouldn't change
+    EXPECT_EQ(ZoneFinder::SUCCESS, clients_map->find(rrclass)->second->
+              find(Name("ns.test2.example")).finder_->
+              find(Name("ns.test2.example"), RRType::A())->code);
+    EXPECT_EQ(ZoneFinder::NXRRSET,
+              clients_map->find(rrclass)->second->
+              find(Name("ns.test2.example")).finder_->
+              find(Name("ns.test2.example"), RRType::AAAA())->code);
+}
+
+void
+DataSrcClientsBuilderTest::configureZones() {
+    ASSERT_EQ(0, std::system(INSTALL_PROG " -c " TEST_DATA_DIR "/test1.zone.in "
+                             TEST_DATA_BUILDDIR "/test1.zone.copied"));
+    ASSERT_EQ(0, std::system(INSTALL_PROG " -c " TEST_DATA_DIR "/test2.zone.in "
+                             TEST_DATA_BUILDDIR "/test2.zone.copied"));
+
+    const ConstElementPtr config(
+        Element::fromJSON(
+            "{"
+            "\"IN\": [{"
+            "   \"type\": \"MasterFiles\","
+            "   \"params\": {"
+            "       \"test1.example\": \"" +
+            std::string(TEST_DATA_BUILDDIR "/test1.zone.copied") + "\","
+            "       \"test2.example\": \"" +
+            std::string(TEST_DATA_BUILDDIR "/test2.zone.copied") + "\""
+            "   },"
+            "   \"cache-enable\": true"
+            "}]}"));
+    clients_map = configureDataSource(config);
+    zoneChecks(clients_map, rrclass);
+}
+
+TEST_F(DataSrcClientsBuilderTest, loadZone) {
+    // pre test condition checks
+    EXPECT_EQ(0, map_mutex.lock_count);
+    EXPECT_EQ(0, map_mutex.unlock_count);
+
+    configureZones();
+
+    EXPECT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR
+                        "/test1-new.zone.in "
+                        TEST_DATA_BUILDDIR "/test1.zone.copied"));
+    EXPECT_EQ(0, system(INSTALL_PROG " -c " TEST_DATA_DIR
+                        "/test2-new.zone.in "
+                        TEST_DATA_BUILDDIR "/test2.zone.copied"));
+
+    const Command loadzone_cmd(LOADZONE, Element::fromJSON(
+                                   "{\"class\": \"IN\","
+                                   " \"origin\": \"test1.example\"}"));
+    EXPECT_TRUE(builder.handleCommand(loadzone_cmd));
+    EXPECT_EQ(1, map_mutex.lock_count); // we should have acquired the lock
+    EXPECT_EQ(1, map_mutex.unlock_count); // and released it.
+
+    newZoneChecks(clients_map, rrclass);
+}
+
+TEST_F(DataSrcClientsBuilderTest,
+#ifdef USE_STATIC_LINK
+       DISABLED_loadZoneSQLite3
+#else
+       loadZoneSQLite3
+#endif
+    )
+{
+    // Prepare the database first
+    const std::string test_db = TEST_DATA_BUILDDIR "/auth_test.sqlite3.copied";
+    std::stringstream ss("example.org. 3600 IN SOA . . 0 0 0 0 0\n");
+    createSQLite3DB(rrclass, Name("example.org"), test_db.c_str(), ss);
+    // This describes the data source in the configuration
+    const ConstElementPtr config(Element::fromJSON("{"
+        "\"IN\": [{"
+        "    \"type\": \"sqlite3\","
+        "    \"params\": {\"database_file\": \"" + test_db + "\"},"
+        "    \"cache-enable\": true,"
+        "    \"cache-zones\": [\"example.org\"]"
+        "}]}"));
+    clients_map = configureDataSource(config);
+
+    // Check that the A record at www.example.org does not exist
+    EXPECT_EQ(ZoneFinder::NXDOMAIN,
+              clients_map->find(rrclass)->second->
+              find(Name("example.org")).finder_->
+              find(Name("www.example.org"), RRType::A())->code);
+
+    // Add the record to the underlying sqlite database, by loading
+    // it as a separate datasource, and updating it
+    ConstElementPtr sql_cfg = Element::fromJSON("{ \"type\": \"sqlite3\","
+                                                "\"database_file\": \""
+                                                + test_db + "\"}");
+    DataSourceClientContainer sql_ds("sqlite3", sql_cfg);
+    ZoneUpdaterPtr sql_updater =
+        sql_ds.getInstance().getUpdater(Name("example.org"), false);
+    sql_updater->addRRset(
+        *textToRRset("www.example.org. 60 IN A 192.0.2.1"));
+    sql_updater->commit();
+
+    EXPECT_EQ(ZoneFinder::NXDOMAIN,
+              clients_map->find(rrclass)->second->
+              find(Name("example.org")).finder_->
+              find(Name("www.example.org"), RRType::A())->code);
+
+    // Now send the command to reload it
+    const Command loadzone_cmd(LOADZONE, Element::fromJSON(
+                                   "{\"class\": \"IN\","
+                                   " \"origin\": \"example.org\"}"));
+    EXPECT_TRUE(builder.handleCommand(loadzone_cmd));
+    // And now it should be present too.
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              clients_map->find(rrclass)->second->
+              find(Name("example.org")).finder_->
+              find(Name("www.example.org"), RRType::A())->code);
+
+    // An error case: the zone has no configuration. (note .com here)
+    const Command nozone_cmd(LOADZONE, Element::fromJSON(
+                                 "{\"class\": \"IN\","
+                                 " \"origin\": \"example.com\"}"));
+    EXPECT_THROW(builder.handleCommand(nozone_cmd),
+                 TestDataSrcClientsBuilder::InternalCommandError);
+    // The previous zone is not hurt in any way
+    EXPECT_EQ(ZoneFinder::SUCCESS, clients_map->find(rrclass)->second->
+              find(Name("example.org")).finder_->
+              find(Name("example.org"), RRType::SOA())->code);
+
+    // attempt of reloading a zone but in-memory cache is disabled.
+    const ConstElementPtr config2(Element::fromJSON("{"
+        "\"IN\": [{"
+        "    \"type\": \"sqlite3\","
+        "    \"params\": {\"database_file\": \"" + test_db + "\"},"
+        "    \"cache-enable\": false,"
+        "    \"cache-zones\": [\"example.org\"]"
+        "}]}"));
+    clients_map = configureDataSource(config2);
+    EXPECT_THROW(builder.handleCommand(
+                     Command(LOADZONE, Element::fromJSON(
+                                 "{\"class\": \"IN\","
+                                 " \"origin\": \"example.org\"}"))),
+                 TestDataSrcClientsBuilder::InternalCommandError);
+
+    // basically impossible case: in-memory cache is completely disabled.
+    // In this implementation of manager-builder, this should never happen,
+    // but it catches it like other configuration error and keeps going.
+    clients_map->clear();
+    boost::shared_ptr<ConfigurableClientList> nocache_list(
+        new ConfigurableClientList(rrclass));
+    nocache_list->configure(
+        Element::fromJSON(
+            "[{\"type\": \"sqlite3\","
+            "  \"params\": {\"database_file\": \"" + test_db + "\"},"
+            "  \"cache-enable\": true,"
+            "  \"cache-zones\": [\"example.org\"]"
+            "}]"), false);           // false = disable cache
+    (*clients_map)[rrclass] = nocache_list;
+    EXPECT_THROW(builder.handleCommand(
+                     Command(LOADZONE, Element::fromJSON(
+                                 "{\"class\": \"IN\","
+                                 " \"origin\": \"example.org\"}"))),
+                 TestDataSrcClientsBuilder::InternalCommandError);
+}
+
+TEST_F(DataSrcClientsBuilderTest, loadBrokenZone) {
+    configureZones();
+
+    ASSERT_EQ(0, std::system(INSTALL_PROG " -c " TEST_DATA_DIR
+                             "/test1-broken.zone.in "
+                             TEST_DATA_BUILDDIR "/test1.zone.copied"));
+    // there's an error in the new zone file.  reload will be rejected.
+    const Command loadzone_cmd(LOADZONE, Element::fromJSON(
+                                   "{\"class\": \"IN\","
+                                   " \"origin\": \"test1.example\"}"));
+    EXPECT_THROW(builder.handleCommand(loadzone_cmd),
+                 TestDataSrcClientsBuilder::InternalCommandError);
+    zoneChecks(clients_map, rrclass);     // zone shouldn't be replaced
+}
+
+TEST_F(DataSrcClientsBuilderTest, loadUnreadableZone) {
+    configureZones();
+
+    // install the zone file as unreadable
+    ASSERT_EQ(0, std::system(INSTALL_PROG " -c -m 000 " TEST_DATA_DIR
+                             "/test1.zone.in "
+                             TEST_DATA_BUILDDIR "/test1.zone.copied"));
+    const Command loadzone_cmd(LOADZONE, Element::fromJSON(
+                                   "{\"class\": \"IN\","
+                                   " \"origin\": \"test1.example\"}"));
+    EXPECT_THROW(builder.handleCommand(loadzone_cmd),
+                 TestDataSrcClientsBuilder::InternalCommandError);
+    zoneChecks(clients_map, rrclass);     // zone shouldn't be replaced
+}
+
+TEST_F(DataSrcClientsBuilderTest, loadZoneWithoutDataSrc) {
+    // try to execute load command without configuring the zone beforehand.
+    // it should fail.
+    EXPECT_THROW(builder.handleCommand(
+                     Command(LOADZONE,
+                             Element::fromJSON(
+                                 "{\"class\": \"IN\", "
+                                 " \"origin\": \"test1.example\"}"))),
+                 TestDataSrcClientsBuilder::InternalCommandError);
+}
+
+TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
+    configureZones();
+
+    if (!isc::util::unittests::runningOnValgrind()) {
+        // null arg: this causes assertion failure
+        EXPECT_DEATH_IF_SUPPORTED({
+                builder.handleCommand(Command(LOADZONE, ElementPtr()));
+            }, "");
+    }
+
+    // zone class is bogus (note that this shouldn't happen except in tests)
+    EXPECT_THROW(builder.handleCommand(
+                     Command(LOADZONE,
+                             Element::fromJSON(
+                                 "{\"origin\": \"test1.example\","
+                                 " \"class\": \"no_such_class\"}"))),
+                 InvalidRRClass);
+
+    // not a string
+    EXPECT_THROW(builder.handleCommand(
+                     Command(LOADZONE,
+                             Element::fromJSON(
+                                 "{\"origin\": \"test1.example\","
+                                 " \"class\": 1}"))),
+                 isc::data::TypeError);
+
+    // class or origin is missing: result in assertion failure
+    if (!isc::util::unittests::runningOnValgrind()) {
+        EXPECT_DEATH_IF_SUPPORTED({
+                builder.handleCommand(
+                    Command(LOADZONE,
+                            Element::fromJSON(
+                                "{\"origin\": \"test1.example\"}")));
+            }, "");
+        EXPECT_DEATH_IF_SUPPORTED({
+                builder.handleCommand(Command(LOADZONE,
+                                              Element::fromJSON(
+                                                  "{\"class\": \"IN\"}")));
+            }, "");
+    }
+
+    // zone doesn't exist in the data source
+    EXPECT_THROW(
+        builder.handleCommand(
+            Command(LOADZONE,
+                    Element::fromJSON(
+                        "{\"class\": \"IN\", \"origin\": \"xx\"}"))),
+        TestDataSrcClientsBuilder::InternalCommandError);
+
+    // origin is bogus
+    EXPECT_THROW(builder.handleCommand(
+                     Command(LOADZONE,
+                             Element::fromJSON(
+                                 "{\"class\": \"IN\", \"origin\": \"...\"}"))),
+                 EmptyLabel);
+    EXPECT_THROW(builder.handleCommand(
+                     Command(LOADZONE,
+                             Element::fromJSON(
+                                 "{\"origin\": 10, \"class\": 1}"))),
+                 isc::data::TypeError);
+}
+
 } // unnamed namespace

+ 2 - 0
src/bin/auth/tests/test_datasrc_clients_mgr.cc

@@ -50,6 +50,8 @@ TestDataSrcClientsBuilder::doNoop() {
         isc_throw(Exception, "test exception");
     case TestMutex::INTEGER:
         throw 42;
+    case TestMutex::INTERNAL:
+        isc_throw(InternalCommandError, "internal error, should be ignored");
     }
 }
 } // namespace datasrc_clientmgr_internal

+ 2 - 1
src/bin/auth/tests/test_datasrc_clients_mgr.h

@@ -43,7 +43,8 @@ public:
     // None: no throw from specialized doNoop()
     // EXCLASS: throw some exception class object
     // INTEGER: throw an integer
-    enum ExceptionFromNoop { NONE, EXCLASS, INTEGER };
+    // INTERNAL: internal error (shouldn't terminate the thread)
+    enum ExceptionFromNoop { NONE, EXCLASS, INTEGER, INTERNAL };
 
     TestMutex() : lock_count(0), unlock_count(0), noop_count(0),
                   throw_from_noop(NONE)