Commit 9281b972 authored by JINMEI Tatuya's avatar JINMEI Tatuya
Browse files

[master] Merge branch 'trac2212-2'

parents 61fc577d cf72f036
......@@ -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
......
......@@ -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.
......
......@@ -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);