Commit 56ee9810 authored by JINMEI Tatuya's avatar JINMEI Tatuya
Browse files

[master] Merge branch 'trac2905'

parents aafb86a4 c1d838b4
......@@ -386,6 +386,12 @@ Query::process(datasrc::ClientList& client_list,
response_->setHeaderFlag(Message::HEADERFLAG_AA, false);
response_->setRcode(Rcode::REFUSED());
return;
} else if (!result.finder_) {
// We found a matching zone in a data source but its data are not
// available.
response_->setHeaderFlag(Message::HEADERFLAG_AA, false);
response_->setRcode(Rcode::SERVFAIL());
return;
}
ZoneFinder& zfinder = *result.finder_;
......
......@@ -1210,6 +1210,38 @@ TEST_F(AuthSrvTest, updateWithInMemoryClient) {
opcode.getCode(), QR_FLAG, 1, 0, 0, 0);
}
TEST_F(AuthSrvTest, emptyZone) {
// Similar to the previous setup, but the configuration has an error
// (zone file doesn't exist) and the query should result in SERVFAIL.
// Here we check the rcode other header parameters, and statistics.
const ConstElementPtr config(Element::fromJSON("{"
"\"IN\": [{"
" \"type\": \"MasterFiles\","
" \"params\": {\"example.com\": \"nosuchfile.zone\"},"
" \"cache-enable\": true"
"}]}"));
installDataSrcClientLists(server, configureDataSource(config));
createDataFromFile("examplequery_fromWire.wire");
server.processMessage(*io_message, *parse_message, *response_obuffer,
&dnsserv);
EXPECT_TRUE(dnsserv.hasAnswer());
headerCheck(*parse_message, default_qid, Rcode::SERVFAIL(),
opcode.getCode(), QR_FLAG, 1, 0, 0, 0);
checkAllRcodeCountersZeroExcept(Rcode::SERVFAIL(), 1);
ConstElementPtr stats = server.getStatistics()->get("zones")->
get("_SERVER_");
std::map<std::string, int> expect;
expect["request.v4"] = 1;
expect["request.udp"] = 1;
expect["opcode.query"] = 1;
expect["responses"] = 1;
expect["qrynoauthans"] = 1;
expect["rcode.servfail"] = 1;
checkStatisticsCounters(stats, expect);
}
TEST_F(AuthSrvTest, queryWithInMemoryClientNoDNSSEC) {
// In this example, we do simple check that query is handled from the
// query handler class, and confirm it returns no error and a non empty
......
......@@ -133,6 +133,12 @@ const char* const unsigned_delegation_nsec3_txt =
"q81r598950igr1eqvc60aedlq66425b5.example.com. 3600 IN NSEC3 1 1 12 "
"aabbccdd 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom NS RRSIG\n";
// Name of an "empty" zone: used to simulate the case of
// configured-but-available zone (due to load errors, etc).
// Each tested data source client is expected to have this zone (SQLite3
// currently doesn't have this concept so it's skipped)
const char* const EMPTY_ZONE_NAME = "empty.example.org";
// A helper function that generates a textual representation of RRSIG RDATA
// for the given covered type. The resulting RRSIG may not necessarily make
// sense in terms of the DNSSEC protocol, but for our testing purposes it's
......@@ -799,11 +805,14 @@ createDataSrcClientList(DataSrcType type, DataSourceClient& client) {
return (boost::shared_ptr<ClientList>(new SingletonList(client)));
case INMEMORY:
list.reset(new ConfigurableClientList(RRClass::IN()));
// Configure one normal zone and one "empty" zone.
list->configure(isc::data::Element::fromJSON(
"[{\"type\": \"MasterFiles\","
" \"cache-enable\": true, "
" \"params\": {\"example.com\": \"" +
string(TEST_OWN_DATA_BUILDDIR "/example.zone") +
string(TEST_OWN_DATA_BUILDDIR "/example.zone\",") +
+ "\"" + EMPTY_ZONE_NAME + "\": \"" +
string(TEST_OWN_DATA_BUILDDIR "/nosuchfile.zone") +
"\"}}]"), true);
return (list);
case SQLITE3:
......@@ -834,39 +843,38 @@ createDataSrcClientList(DataSrcType type, DataSourceClient& client) {
class MockClient : public DataSourceClient {
public:
virtual FindResult findZone(const isc::dns::Name& origin) const {
const Name r_origin(origin.reverse());
std::map<Name, ZoneFinderPtr>::const_iterator it =
zone_finders_.lower_bound(r_origin);
if (it != zone_finders_.end()) {
const NameComparisonResult result =
origin.compare((it->first).reverse());
if (result.getRelation() == NameComparisonResult::EQUAL) {
return (FindResult(result::SUCCESS, it->second));
} else if (result.getRelation() == NameComparisonResult::SUBDOMAIN) {
return (FindResult(result::PARTIALMATCH, it->second));
}
}
// Identify the next (strictly) larger name than the given 'origin' in
// the map. Its predecessor (if any) is the longest matching name
// if it's either an exact match or a super domain; otherwise there's
// no match in the map. See also datasrc/tests/mock_client.cc.
// If it is at the beginning of the map, then the name was not
// found (we have already handled the element the iterator
// points to).
if (it == zone_finders_.begin()) {
// Eliminate the case of empty map to simply the rest of the code
if (zone_finders_.empty()) {
return (FindResult(result::NOTFOUND, ZoneFinderPtr()));
}
// Check if the previous element is a partial match.
--it;
const NameComparisonResult result =
origin.compare((it->first).reverse());
if (result.getRelation() == NameComparisonResult::SUBDOMAIN) {
return (FindResult(result::PARTIALMATCH, it->second));
std::map<Name, ZoneFinderPtr>::const_iterator it =
zone_finders_.upper_bound(origin);
if (it == zone_finders_.begin()) { // no predecessor
return (FindResult(result::NOTFOUND, ZoneFinderPtr()));
}
return (FindResult(result::NOTFOUND, ZoneFinderPtr()));
--it; // get the predecessor
const result::ResultFlags flags =
it->second ? result::FLAGS_DEFAULT : result::ZONE_EMPTY;
const NameComparisonResult compar(it->first.compare(origin));
switch (compar.getRelation()) {
case NameComparisonResult::EQUAL:
return (FindResult(result::SUCCESS, it->second, flags));
case NameComparisonResult::SUPERDOMAIN:
return (FindResult(result::PARTIALMATCH, it->second, flags));
default:
return (FindResult(result::NOTFOUND, ZoneFinderPtr()));
}
}
virtual ZoneUpdaterPtr getUpdater(const isc::dns::Name&, bool, bool) const {
virtual ZoneUpdaterPtr getUpdater(const isc::dns::Name&, bool, bool) const
{
isc_throw(isc::NotImplemented,
"Updater isn't supported in the MockClient");
}
......@@ -878,18 +886,21 @@ public:
}
result::Result addZone(ZoneFinderPtr finder) {
// Use the reverse of the name as the key, so we can quickly
// find partial matches in the map.
zone_finders_[finder->getOrigin().reverse()] = finder;
zone_finders_[finder->getOrigin()] = finder;
return (result::SUCCESS);
}
// "configure" a zone with no data. This will cause the ZONE_EMPTY flag
// on in finZone().
result::Result addEmptyZone(const Name& zone_name) {
zone_finders_[zone_name] = ZoneFinderPtr();
return (result::SUCCESS);
}
private:
// Note that because we no longer have the old RBTree, and the new
// in-memory DomainTree is not useful as it returns const nodes, we
// use a std::map instead. In this map, the key is a name stored in
// reverse order of labels to aid in finding partial matches
// quickly.
// use a std::map instead.
std::map<Name, ZoneFinderPtr> zone_finders_;
};
......@@ -916,9 +927,10 @@ protected:
response.setRcode(Rcode::NOERROR());
response.setOpcode(Opcode::QUERY());
// create and add a matching zone.
// create and add a matching zone. One is a "broken, empty" zone.
mock_finder = new MockZoneFinder();
mock_client.addZone(ZoneFinderPtr(mock_finder));
mock_client.addEmptyZone(Name(EMPTY_ZONE_NAME));
}
virtual void SetUp() {
......@@ -949,6 +961,12 @@ protected:
setNSEC3HashCreator(NULL);
}
bool isEmptyZoneSupported() const {
// Not all data sources support the concept of empty zones.
// Specifically for this test, SQLite3-based data source doesn't.
return (GetParam() != SQLITE3);
}
void enableNSEC3(const vector<string>& rrsets_to_add) {
boost::shared_ptr<ConfigurableClientList> new_list;
switch (GetParam()) {
......@@ -1144,11 +1162,29 @@ TEST_P(QueryTest, noZone) {
// REFUSED.
MockClient empty_mock_client;
SingletonList empty_list(empty_mock_client);
EXPECT_NO_THROW(query.process(empty_list, qname, qtype,
response));
EXPECT_NO_THROW(query.process(empty_list, qname, qtype, response));
EXPECT_EQ(Rcode::REFUSED(), response.getRcode());
}
TEST_P(QueryTest, emptyZone) {
// Query for an "empty (broken)" zone. If the concept is supported by
// the underlying data source, the result should be SERVFAIL; otherwise
// it would be handled as a nonexistent zone, resulting in REFUSED.
const Rcode expected_rcode =
isEmptyZoneSupported() ? Rcode::SERVFAIL() : Rcode::REFUSED();
query.process(*list_, Name(EMPTY_ZONE_NAME), qtype, response);
responseCheck(response, expected_rcode, 0, 0, 0, 0, NULL, NULL, NULL);
// Same for the partial match case
response.clear(isc::dns::Message::RENDER);
response.setRcode(Rcode::NOERROR());
response.setOpcode(Opcode::QUERY());
query.process(*list_, Name(string("www.") + EMPTY_ZONE_NAME), qtype,
response);
responseCheck(response, expected_rcode, 0, 0, 0, 0, NULL, NULL, NULL);
}
TEST_P(QueryTest, exactMatch) {
EXPECT_NO_THROW(query.process(*list_, qname, qtype, response));
// find match rrset
......@@ -1400,7 +1436,6 @@ TEST_F(QueryTestForMockOnly, badSecureDelegation) {
qtype, response));
}
TEST_P(QueryTest, nxdomain) {
EXPECT_NO_THROW(query.process(*list_,
Name("nxdomain.example.com"), qtype,
......
......@@ -132,8 +132,8 @@ public:
/// \brief A helper structure to represent the search result of
/// \c find().
///
/// This is a straightforward pair of the result code and a share pointer
/// to the found zone to represent the result of \c find().
/// This is a straightforward tuple of the result code/flags and a shared
/// pointer to the found zone to represent the result of \c find().
/// We use this in order to avoid overloading the return value for both
/// the result code ("success" or "not found") and the found object,
/// i.e., avoid using \c NULL to mean "not found", etc.
......@@ -146,10 +146,13 @@ public:
/// variables.
struct FindResult {
FindResult(result::Result param_code,
const ZoneFinderPtr param_zone_finder) :
code(param_code), zone_finder(param_zone_finder)
const ZoneFinderPtr param_zone_finder,
result::ResultFlags param_flags = result::FLAGS_DEFAULT) :
code(param_code), flags(param_flags),
zone_finder(param_zone_finder)
{}
const result::Result code;
const result::ResultFlags flags;
const ZoneFinderPtr zone_finder;
};
......@@ -184,8 +187,12 @@ public:
/// - \c result::PARTIALMATCH: A zone whose origin is a
/// super domain of \c name is found (but there is no exact match)
/// - \c result::NOTFOUND: For all other cases.
/// - \c flags: usually FLAGS_DEFAULT, but if the zone data are not
/// available (possibly because an error was detected at load time)
/// the ZONE_EMPTY flag is set.
/// - \c zone_finder: Pointer to a \c ZoneFinder object for the found zone
/// if one is found; otherwise \c NULL.
/// if one is found and is not empty (flags doesn't have ZONE_EMPTY);
/// otherwise \c NULL.
///
/// A specific derived version of this method may throw an exception.
/// This interface does not specify which exceptions can happen (at least
......@@ -215,6 +222,9 @@ public:
/// \throw Others Possibly implementation specific exceptions (it is
/// not fixed if a concrete implementation of this method can throw
/// anything else.)
/// \throw EmptyZone the zone is supposed to exist in the data source,
/// but its content is not available. This generally means there's an
/// error in the content.
///
/// \param name The name of zone apex to be traversed. It doesn't do
/// nearest match as findZone.
......
......@@ -90,15 +90,15 @@ ConfigurableClientList::configure(const ConstElementPtr& config,
for (; i < config->size(); ++i) {
// Extract the parameters
const ConstElementPtr dconf(config->get(i));
const ConstElementPtr typeElem(dconf->get("type"));
if (typeElem == ConstElementPtr()) {
const ConstElementPtr type_elem(dconf->get("type"));
if (type_elem == ConstElementPtr()) {
isc_throw(ConfigurationError, "Missing the type option in "
"data source no " << i);
}
const string type(typeElem->stringValue());
ConstElementPtr paramConf(dconf->get("params"));
if (paramConf == ConstElementPtr()) {
paramConf.reset(new NullElement());
const string type(type_elem->stringValue());
ConstElementPtr param_conf(dconf->get("params"));
if (param_conf == ConstElementPtr()) {
param_conf.reset(new NullElement());
}
// Get the name (either explicit, or guess)
const ConstElementPtr name_elem(dconf->get("name"));
......@@ -114,7 +114,7 @@ ConfigurableClientList::configure(const ConstElementPtr& config,
// no-op. In the latter case, it's of no use unless cache is
// allowed; we simply skip building it in that case.
const DataSourcePair dsrc_pair = getDataSourceClient(type,
paramConf);
param_conf);
if (!allow_cache && !dsrc_pair.first) {
LOG_WARN(logger, DATASRC_LIST_NOT_CACHED).
arg(datasrc_name).arg(rrclass_);
......@@ -159,18 +159,22 @@ ConfigurableClientList::configure(const ConstElementPtr& config,
cache_conf->getLoadAction(rrclass_, zname);
// in this loop this should be always true
assert(load_action);
memory::ZoneWriter writer(zt_segment,
load_action, zname, rrclass_);
writer.load();
// For the initial load, we'll let the writer handle
// loading error and install an empty zone in the table.
memory::ZoneWriter writer(zt_segment, load_action, zname,
rrclass_, true);
std::string error_msg;
writer.load(&error_msg);
if (!error_msg.empty()) {
LOG_ERROR(logger, DATASRC_LOAD_ZONE_ERROR).arg(zname).
arg(rrclass_).arg(datasrc_name).arg(error_msg);
}
writer.install();
writer.cleanup();
} catch (const NoSuchZone&) {
LOG_ERROR(logger, DATASRC_CACHE_ZONE_NOTFOUND).
arg(zname).arg(rrclass_).arg(datasrc_name);
} catch (const ZoneLoaderException& e) {
LOG_ERROR(logger, DATASRC_LOAD_ZONE_ERROR).
arg(zname).arg(rrclass_).arg(datasrc_name).
arg(e.what());
}
}
}
......@@ -357,7 +361,7 @@ ConfigurableClientList::getCachedZoneWriter(const Name& name,
ZoneWriterPtr(
new memory::ZoneWriter(
*info.ztable_segment_,
load_action, name, rrclass_))));
load_action, name, rrclass_, false))));
}
// We can't find the specified zone. If a specific data source was
......
......@@ -231,20 +231,29 @@ public:
/// of the searched name is needed. Therefore, the call would look like:
///
/// \code FindResult result(list->find(queried_name));
/// FindResult result(list->find(queried_name));
/// if (result.datasrc_) {
/// createTheAnswer(result.finder_);
/// if (result.dsrc_client_) {
/// if (result.finder_) {
/// createTheAnswer(result.finder_);
/// } else { // broken zone, return SERVFAIL
/// createErrorMessage(Rcode.SERVFAIL());
/// }
/// } else {
/// createNotAuthAnswer();
/// } \endcode
///
/// Note that it is possible that \c finder_ is NULL while \c datasrc_
/// is not. This happens if the zone is configured to be served from
/// the data source but it is broken in some sense and doesn't hold any
/// zone data, e.g., when the zone file has an error or the secondary
/// zone hasn't been transferred yet. The caller needs to expect the case
/// and handle it accordingly.
///
/// The other scenario is manipulating zone data (XfrOut, XfrIn, DDNS,
/// ...). In this case, the finder itself is not so important. However,
/// we need an exact match (if we want to manipulate zone data, we must
/// know exactly, which zone we are about to manipulate). Then the call
///
/// \code FindResult result(list->find(zone_name, true, false));
/// FindResult result(list->find(zone_name, true, false));
/// if (result.datasrc_) {
/// ZoneUpdaterPtr updater(result.datasrc_->getUpdater(zone_name);
/// ...
......
......@@ -374,8 +374,9 @@ database backend (sqlite3, for example) and use it from there.
During data source configuration, an error was found in the zone data
when it was being loaded in to memory on the shown data source. This
particular zone was not loaded, but data source configuration
continues, possibly loading other zones into memory. The specific
error is shown in the message, and should be addressed.
continues, possibly loading other zones into memory. Subsequent lookups
will treat this zone as broken. The specific error is shown in the
message, and should be addressed.
% DATASRC_MASTER_LOAD_ERROR %1:%2: Zone '%3/%4' contains error: %5
There's an error in the given master file. The zone won't be loaded for
......
......@@ -56,6 +56,17 @@ public:
DataSourceError(file, line, what) {}
};
/// \brief An error indicating a zone is recognized but its content is not
/// available.
///
/// This generally indicates a condition that there's an error in the zone
/// content and it's not successfully loaded.
class EmptyZone : public DataSourceError {
public:
EmptyZone(const char* file, size_t line, const char* what) :
DataSourceError(file, line, what) {}
};
/// Base class for a number of exceptions that are thrown while working
/// with zones.
struct ZoneException : public Exception {
......
......@@ -69,11 +69,11 @@ InMemoryClient::findZone(const isc::dns::Name& zone_name) const {
const ZoneTable::FindResult result(zone_table->findZone(zone_name));
ZoneFinderPtr finder;
if (result.code != result::NOTFOUND) {
if (result.code != result::NOTFOUND && result.zone_data) {
finder.reset(new InMemoryZoneFinder(*result.zone_data, getClass()));
}
return (DataSourceClient::FindResult(result.code, finder));
return (DataSourceClient::FindResult(result.code, finder, result.flags));
}
const ZoneData*
......@@ -242,7 +242,12 @@ InMemoryClient::getIterator(const Name& name, bool separate_rrs) const {
const ZoneTable* zone_table = ztable_segment_->getHeader().getTable();
const ZoneTable::FindResult result(zone_table->findZone(name));
if (result.code != result::SUCCESS) {
isc_throw(NoSuchZone, "No such zone: " + name.toText());
isc_throw(NoSuchZone, "no such zone for in-memory iterator: "
<< name.toText());
}
if (!result.zone_data) {
isc_throw(EmptyZone, "empty zone for in-memory iterator: "
<< name.toText());
}
return (ZoneIteratorPtr(new MemoryIterator(
......
......@@ -92,6 +92,12 @@ tried).
Debug information. A specific type RRset is requested at a zone origin
of an in-memory zone and it is found.
% DATASRC_MEMORY_MEM_ADD_EMPTY_ZONE adding an empty zone '%1/%2'
Debug information. An "empty" zone is being added into the in-memory
data source. This is conceptual data indicating the state where the
zone exists but its content isn't available. That would be the case,
for example, a broken zone specified in the configuration.
% DATASRC_MEMORY_MEM_ADD_RRSET adding RRset '%1/%2' into zone '%3'
Debug information. An RRset is being added to the in-memory data source.
......
......@@ -38,6 +38,10 @@ namespace isc {
namespace datasrc {
namespace memory {
// Definition of a class static constant. It's public and its address
// could be needed by applications, so we need an explicit definition.
const ZoneNode::Flags ZoneData::DNSSEC_SIGNED;
namespace {
void
rdataSetDeleter(RRClass rrclass, util::MemorySegment* mem_sgmt,
......@@ -179,6 +183,13 @@ ZoneData::create(util::MemorySegment& mem_sgmt, const Name& zone_origin) {
return (zone_data);
}
ZoneData*
ZoneData::create(util::MemorySegment& mem_sgmt) {
ZoneData* zone_data = create(mem_sgmt, Name::ROOT_NAME());
zone_data->origin_node_->setFlag(EMPTY_ZONE);
return (zone_data);
}
void
ZoneData::destroy(util::MemorySegment& mem_sgmt, ZoneData* zone_data,
RRClass zone_class)
......
......@@ -378,18 +378,28 @@ private:
/// It never throws an exception.
ZoneData(ZoneTree* zone_tree, ZoneNode* origin_node);
// Zone node flags.
// Zone node flags. When adding a new flag, it's generally advisable to
// keep existing values so the binary image of the data is as much
// backward compatible as possible. And it can be helpful in practice
// for file-mapped data.
private:
// Set in the origin node (which always exists at the same address)
// to indicate whether the zone is signed or not. Internal use,
// so defined as private.
static const ZoneNode::Flags DNSSEC_SIGNED = ZoneNode::FLAG_USER1;
public:
/// \brief Node flag indicating it is at a "wildcard level"
///
/// This means one of the node's immediate children is a wildcard.
static const ZoneNode::Flags WILDCARD_NODE = ZoneNode::FLAG_USER2;
private:
// Also set in the origin node, indicating this is a special "empty zone",
// that could be created only by the corresponding create() method to be
// used for some kind of sentinel data.
static const ZoneNode::Flags EMPTY_ZONE = ZoneNode::FLAG_USER3;
public:
/// \brief Allocate and construct \c ZoneData.
///
......@@ -410,6 +420,23 @@ public:
static ZoneData* create(util::MemorySegment& mem_sgmt,
const dns::Name& zone_origin);
/// \brief Allocate and construct a special "empty" \c ZoneData.
///
/// A ZoneData object created this way holds all internal integrity
/// that those created by the other \c create() method have, but is not
/// publicly associated with any actual zone data. It's intended to be
/// used as a kind of sentinel data to representing the concept such as
/// "broken zone".
///
/// Methods calls on empty \c ZoneData object except \c destroy() and
/// \c isEmpty() are meaningless, while they shouldn't cause disruption.
/// It's caller's responsibility to use empty zone data objects in the
/// intended way.
///
/// \param mem_sgmt A \c MemorySegment from which memory for the new
/// \c ZoneData is allocated.
static ZoneData* create(util::MemorySegment& mem_sgmt);
/// \brief Destruct and deallocate \c ZoneData.
///
/// It releases all resource allocated in the internal storage NSEC3 for
......@@ -482,6 +509,13 @@ public:
/// \throw none
bool isNSEC3Signed() const { return (nsec3_data_); }
/// \brief Return whether or not the zone data is "empty".
///
/// See the description of \c create() for the concept of empty zone data.
///
/// \throw None
bool isEmpty() const { return (origin_node_->getFlag(EMPTY_ZONE)); }
/// \brief Return NSEC3Data of the zone.
///
/// This method returns non-NULL valid pointer to \c NSEC3Data object
......
......@@ -18,6 +18,8 @@
#include <datasrc/memory/segment_object_holder.h>
#include <datasrc/memory/logger.h>
#include <exceptions/exceptions.h>
#include <util/memory_segment.h>
#include <dns/name.h>
......@@ -40,7 +42,10 @@ void
deleteZoneData(util::MemorySegment* mem_sgmt, ZoneData* zone_data,
RRClass rrclass)
{
if (zone_data != NULL) {
// We shouldn't delete empty zone data here; the only empty zone
// that can be passed here is the placeholder for broken zones maintained
// in the zone table. It will stay there until the table is destroyed.
if (zone_data && !zone_data->isEmpty()) {
ZoneData::destroy(*mem_sgmt, zone_data, rrclass);
}
}
......@@ -49,21 +54,30 @@ typedef boost::function<void(ZoneData*)> ZoneDataDeleterType;
ZoneTable*
ZoneTable::create(util::MemorySegment& mem_sgmt, const RRClass& zone_class) {
SegmentObjectHolder<ZoneTableTree, ZoneDataDeleterType> holder(
// Create a placeholder "null" zone data
SegmentObjectHolder<ZoneData, RRClass> zdholder(mem_sgmt, zone_class);
zdholder.set(ZoneData::create(mem_sgmt));
// create and setup the tree for the table.
SegmentObjectHolder<ZoneTableTree, ZoneDataDeleterType> tree_holder(
mem_sgmt, boost::bind(deleteZoneData, &mem_sgmt, _1, zone_class));
holder.set(ZoneTableTree::create(mem_sgmt));
tree_holder.set(ZoneTableTree::create(mem_sgmt));
void* p = mem_sgmt.allocate(sizeof(ZoneTable));
ZoneTable* zone_table = new(p) ZoneTable(zone_class, holder.get());
holder.release();
// Build zone table with the created objects. Its constructor doesn't
// throw, so we can release them from the holder at this point.
ZoneTable* zone_table = new(p) ZoneTable(zone_class, tree_holder.release(),
zdholder.release());
return (zone_table);
}
void
ZoneTable::destroy(util::MemorySegment& mem_sgmt, ZoneTable* ztable)
{</