Parcourir la source

[1067] DatabaseClient part of iteration

The implementation of the ZoneIterator for DatabaseClient (it isn't
publicly visible, it is hidden in the .cc file) and tests for it.
Michal 'vorner' Vaner il y a 13 ans
Parent
commit
cce2a00af5

+ 97 - 1
src/lib/datasrc/database.cc

@@ -13,11 +13,18 @@
 // PERFORMANCE OF THIS SOFTWARE.
 
 #include <datasrc/database.h>
+#include <datasrc/data_source.h>
+#include <datasrc/iterator.h>
 
 #include <exceptions/exceptions.h>
 #include <dns/name.h>
+#include <dns/rrclass.h>
+#include <dns/rrttl.h>
 
-using isc::dns::Name;
+#include <string>
+
+using namespace isc::dns;
+using std::string;
 
 namespace isc {
 namespace datasrc {
@@ -81,5 +88,94 @@ DatabaseClient::Finder::getClass() const {
     return isc::dns::RRClass::IN();
 }
 
+namespace {
+
+/*
+ * This needs, beside of converting all data from textual representation, group
+ * together rdata of the same RRsets. To do this, we hold one row of data ahead
+ * of iteration. When we get a request to provide data, we create it from this
+ * data and load a new one. If it is to be put to the same rrset, we add it.
+ * Otherwise we just return what we have and keep the row as the one ahead
+ * for next time.
+ */
+class Iterator : public ZoneIterator {
+public:
+    Iterator(const DatabaseConnection::IteratorContextPtr& context,
+             const RRClass& rrclass) :
+        context_(context),
+        class_(rrclass),
+        ready_(true)
+    {
+        // Prepare data for the next time
+        getData();
+    }
+    virtual isc::dns::ConstRRsetPtr getNextRRset() {
+        if (!ready_) {
+            isc_throw(isc::Unexpected, "Iterating past the zone end");
+        }
+        if (!data_ready_) {
+            // At the end of zone
+            ready_ = false;
+            return (ConstRRsetPtr());
+        }
+        string nameStr(name_), rtypeStr(rtype_);
+        int ttl(ttl_);
+        Name name(nameStr);
+        RRType rtype(rtypeStr);
+        RRsetPtr rrset(new RRset(name, class_, rtype, RRTTL(ttl)));
+        while (data_ready_ && name_ == nameStr && rtypeStr == rtype_) {
+            if (ttl_ != ttl) {
+                isc_throw(DataSourceError, "TTLs in rrset " + nameStr + "/" +
+                          rtypeStr + " differ");
+            }
+            rrset->addRdata(rdata::createRdata(rtype, class_, rdata_));
+            getData();
+        }
+        return (rrset);
+    }
+private:
+    // Load next row of data
+    void getData() {
+        data_ready_ = context_->getNext(name_, rtype_, ttl_, rdata_);
+    }
+    // The context
+    const DatabaseConnection::IteratorContextPtr context_;
+    // Class of the zone
+    RRClass class_;
+    // Status
+    bool ready_, data_ready_;
+    // Data of the next row
+    string name_, rtype_, rdata_;
+    int ttl_;
+};
+
+}
+
+ZoneIteratorPtr
+DatabaseClient::getIterator(const isc::dns::Name& name) const {
+    // Get the zone
+    std::pair<bool, int> zone(connection_->getZone(name));
+    if (!zone.first) {
+        // No such zone, can't continue
+        isc_throw(DataSourceError, "Zone " + name.toText() +
+                  " can not be iterated, because it doesn't exist "
+                  "in this data source");
+    }
+    // Request the context
+    DatabaseConnection::IteratorContextPtr
+        context(connection_->getIteratorContext(name, zone.second));
+    // It must not return NULL, that's a bug of the implementation
+    if (context == DatabaseConnection::IteratorContextPtr()) {
+        isc_throw(isc::Unexpected, "Iterator context null at " +
+                  name.toText());
+    }
+    // Create the iterator and return it
+    // TODO: Once #1062 is merged with this, we need to get the
+    // actual zone class from the connection, as the DatabaseClient
+    // doesn't know it and the iterator needs it (so it wouldn't query
+    // it each time)
+    return (ZoneIteratorPtr(new Iterator(context, RRClass::IN())));
+}
+
 }
 }

+ 2 - 1
src/lib/datasrc/database.h

@@ -271,7 +271,8 @@ public:
      *
      * \exception DataSourceError if the zone doesn't exist.
      * \exception isc::NotImplemented if the underlying DatabaseConnection
-     *     doesn't implement iteration.
+     *     doesn't implement iteration. But in case it is not implemented
+     *     and the zone doesn't exist, DataSourceError is thrown.
      * \exception Anything else the underlying DatabaseConnection might
      *     want to throw.
      * \param name The origin of the zone to iterate.

+ 213 - 5
src/lib/datasrc/tests/database_unittest.cc

@@ -15,36 +15,157 @@
 #include <gtest/gtest.h>
 
 #include <dns/name.h>
+#include <dns/rrttl.h>
 #include <exceptions/exceptions.h>
 
 #include <datasrc/database.h>
+#include <datasrc/data_source.h>
+#include <datasrc/iterator.h>
 
 using namespace isc::datasrc;
 using namespace std;
 using namespace boost;
-using isc::dns::Name;
+using namespace isc::dns;
 
 namespace {
 
 /*
- * A virtual database connection that pretends it contains single zone --
- * example.org.
+ * A connection with minimum implementation, keeping the original
+ * "NotImplemented" methods.
  */
-class MockConnection : public DatabaseConnection {
+class NopConnection : public DatabaseConnection {
 public:
     virtual std::pair<bool, int> getZone(const Name& name) const {
         if (name == Name("example.org")) {
             return (std::pair<bool, int>(true, 42));
+        } else if (name == Name("null.example.org")) {
+            return (std::pair<bool, int>(true, 13));
+        } else if (name == Name("empty.example.org")) {
+            return (std::pair<bool, int>(true, 0));
+        } else if (name == Name("bad.example.org")) {
+            return (std::pair<bool, int>(true, -1));
         } else {
             return (std::pair<bool, int>(false, 0));
         }
     }
 };
 
+/*
+ * A virtual database connection that pretends it contains single zone --
+ * example.org.
+ *
+ * It has the same getZone method as NopConnection, but it provides
+ * implementation of the optional functionality.
+ */
+class MockConnection : public NopConnection {
+private:
+    class MockIteratorContext : public IteratorContext {
+    private:
+        int step;
+    public:
+        MockIteratorContext() :
+            step(0)
+        { }
+        virtual bool getNext(string& name, string& rtype, int& ttl,
+                             string& data)
+        {
+            switch (step ++) {
+                case 0:
+                    name = "example.org";
+                    rtype = "SOA";
+                    ttl = 300;
+                    data = "ns1.example.org. admin.example.org. "
+                        "1234 3600 1800 2419200 7200";
+                    return (true);
+                case 1:
+                    name = "x.example.org";
+                    rtype = "A";
+                    ttl = 300;
+                    data = "192.0.2.1";
+                    return (true);
+                case 2:
+                    name = "x.example.org";
+                    rtype = "A";
+                    ttl = 300;
+                    data = "192.0.2.2";
+                    return (true);
+                case 3:
+                    name = "x.example.org";
+                    rtype = "AAAA";
+                    ttl = 300;
+                    data = "2001:db8::1";
+                    return (true);
+                case 4:
+                    name = "x.example.org";
+                    rtype = "AAAA";
+                    ttl = 300;
+                    data = "2001:db8::2";
+                    return (true);
+                default:
+                    ADD_FAILURE() <<
+                        "Request past the end of iterator context";
+                case 5:
+                    return (false);
+            }
+        }
+    };
+    class EmptyIteratorContext : public IteratorContext {
+    public:
+        virtual bool getNext(string&, string&, int&, string&) {
+            return (false);
+        }
+    };
+    class BadIteratorContext : public IteratorContext {
+    private:
+        int step;
+    public:
+        BadIteratorContext() :
+            step(0)
+        { }
+        virtual bool getNext(string& name, string& rtype, int& ttl,
+                             string& data)
+        {
+            switch (step ++) {
+                case 0:
+                    name = "x.example.org";
+                    rtype = "A";
+                    ttl = 300;
+                    data = "192.0.2.1";
+                    return (true);
+                case 1:
+                    name = "x.example.org";
+                    rtype = "A";
+                    ttl = 301;
+                    data = "192.0.2.2";
+                    return (true);
+                default:
+                    ADD_FAILURE() <<
+                        "Request past the end of iterator context";
+                case 2:
+                    return (false);
+            }
+        }
+    };
+public:
+    virtual IteratorContextPtr getIteratorContext(const Name&, int id) const {
+        if (id == 42) {
+            return (IteratorContextPtr(new MockIteratorContext()));
+        } else if (id == 13) {
+            return (IteratorContextPtr());
+        } else if (id == 0) {
+            return (IteratorContextPtr(new EmptyIteratorContext()));
+        } else if (id == -1) {
+            return (IteratorContextPtr(new BadIteratorContext()));
+        } else {
+            isc_throw(isc::Unexpected, "Unknown zone ID");
+        }
+    }
+};
+
 // This tests the default getIteratorContext behaviour, throwing NotImplemented
 TEST(DatabaseConnectionTest, getIteratorContext) {
     // The parameters don't matter
-    EXPECT_THROW(MockConnection().getIteratorContext(Name("."), 1),
+    EXPECT_THROW(NopConnection().getIteratorContext(Name("."), 1),
                  isc::NotImplemented);
 }
 
@@ -103,4 +224,91 @@ TEST_F(DatabaseClientTest, noConnException) {
                  isc::InvalidParameter);
 }
 
+// If the zone doesn't exist, exception is thrown
+TEST_F(DatabaseClientTest, noZoneIterator) {
+    EXPECT_THROW(client_->getIterator(Name("example.com")), DataSourceError);
+}
+
+// If the zone doesn't exist and iteration is not implemented, it still throws
+// the exception it doesn't exist
+TEST_F(DatabaseClientTest, noZoneNotImplementedIterator) {
+    EXPECT_THROW(DatabaseClient(auto_ptr<DatabaseConnection>(
+        new NopConnection())).getIterator(Name("example.com")),
+                 DataSourceError);
+}
+
+TEST_F(DatabaseClientTest, notImplementedIterator) {
+    EXPECT_THROW(DatabaseClient(auto_ptr<DatabaseConnection>(
+        new NopConnection())).getIterator(Name("example.org")),
+                 isc::NotImplemented);
+}
+
+// Pretend a bug in the connection and pass NULL as the context
+// Should not crash, but gracefully throw
+TEST_F(DatabaseClientTest, nullIteratorContext) {
+    EXPECT_THROW(client_->getIterator(Name("null.example.org")),
+                 isc::Unexpected);
+}
+
+// It doesn't crash or anything if the zone is completely empty
+TEST_F(DatabaseClientTest, emptyIterator) {
+    ZoneIteratorPtr it(client_->getIterator(Name("empty.example.org")));
+    EXPECT_EQ(ConstRRsetPtr(), it->getNextRRset());
+    // This is past the end, it should throw
+    EXPECT_THROW(it->getNextRRset(), isc::Unexpected);
+}
+
+// Iterate trough a zone
+TEST_F(DatabaseClientTest, iterator) {
+    ZoneIteratorPtr it(client_->getIterator(Name("example.org")));
+    ConstRRsetPtr rrset(it->getNextRRset());
+    ASSERT_NE(ConstRRsetPtr(), rrset);
+    EXPECT_EQ(Name("example.org"), rrset->getName());
+    EXPECT_EQ(RRClass::IN(), rrset->getClass());
+    EXPECT_EQ(RRType::SOA(), rrset->getType());
+    EXPECT_EQ(RRTTL(300), rrset->getTTL());
+    RdataIteratorPtr rit(rrset->getRdataIterator());
+    ASSERT_FALSE(rit->isLast());
+    rit->next();
+    EXPECT_TRUE(rit->isLast());
+
+    rrset = it->getNextRRset();
+    ASSERT_NE(ConstRRsetPtr(), rrset);
+    EXPECT_EQ(Name("x.example.org"), rrset->getName());
+    EXPECT_EQ(RRClass::IN(), rrset->getClass());
+    EXPECT_EQ(RRType::A(), rrset->getType());
+    EXPECT_EQ(RRTTL(300), rrset->getTTL());
+    rit = rrset->getRdataIterator();
+    ASSERT_FALSE(rit->isLast());
+    EXPECT_EQ("192.0.2.1", rit->getCurrent().toText());
+    rit->next();
+    ASSERT_FALSE(rit->isLast());
+    EXPECT_EQ("192.0.2.2", rit->getCurrent().toText());
+    rit->next();
+    EXPECT_TRUE(rit->isLast());
+
+    rrset = it->getNextRRset();
+    ASSERT_NE(ConstRRsetPtr(), rrset);
+    EXPECT_EQ(Name("x.example.org"), rrset->getName());
+    EXPECT_EQ(RRClass::IN(), rrset->getClass());
+    EXPECT_EQ(RRType::AAAA(), rrset->getType());
+    EXPECT_EQ(RRTTL(300), rrset->getTTL());
+    EXPECT_EQ(ConstRRsetPtr(), it->getNextRRset());
+    rit = rrset->getRdataIterator();
+    ASSERT_FALSE(rit->isLast());
+    EXPECT_EQ("2001:db8::1", rit->getCurrent().toText());
+    rit->next();
+    ASSERT_FALSE(rit->isLast());
+    EXPECT_EQ("2001:db8::2", rit->getCurrent().toText());
+    rit->next();
+    EXPECT_TRUE(rit->isLast());
+}
+
+// This has inconsistent TTL in the set (the rest, like nonsense in
+// the data is handled in rdata itself).
+TEST_F(DatabaseClientTest, badIterator) {
+    ZoneIteratorPtr it(client_->getIterator(Name("bad.example.org")));
+    EXPECT_THROW(it->getNextRRset(), DataSourceError);
+}
+
 }