BIND 10 exp/res-research, updated. ae34359677345d4fd2dc49111c3d5d8d69d9d3a3 [res-research] implemented 'query_replay', emulating resolver behavior.

BIND 10 source code commits bind10-changes at lists.isc.org
Thu Jul 12 02:02:31 UTC 2012


The branch, exp/res-research has been updated
       via  ae34359677345d4fd2dc49111c3d5d8d69d9d3a3 (commit)
       via  912836edabc86920439752345297a74eee501dee (commit)
       via  29f30ae30d2f3d97e9b7e56362694c04a1ff93af (commit)
       via  ce24ff503144adb1c60d93d967d6fdd55b4ba1af (commit)
       via  434ba5f414c152c1acdc508e9df85fbc7350a855 (commit)
       via  3baa2a5e7c3c710a4cbd4b35d089f6dc8de889df (commit)
       via  296a42873bfd22043d0bbb90b26cbf60cf79c80d (commit)
       via  93efac83d98d511e59d50ebe3c4b7440ca9f402b (commit)
       via  22f14c0a19f235ae8c4c6e0dbafb8f2b096a5daa (commit)
       via  94b24f0e4aaaccf9de98f9c4029672bcd610bed7 (commit)
       via  ffdbede1decf3005e16ca8ef8fc7dec310835a17 (commit)
       via  6ca1c54a4accaa64f8d5525667653fbd3545ff4e (commit)
      from  e9d3fd9a3d5bb8d6df9143a25d590cd301db86f1 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit ae34359677345d4fd2dc49111c3d5d8d69d9d3a3
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Wed Jul 11 19:01:46 2012 -0700

    [res-research] implemented 'query_replay', emulating resolver behavior.

commit 912836edabc86920439752345297a74eee501dee
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Wed Jul 11 18:13:44 2012 -0700

    [res-research] improved CNAME consistency

commit 29f30ae30d2f3d97e9b7e56362694c04a1ff93af
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Wed Jul 11 17:09:13 2012 -0700

    [res-research] support updates to existing cache entries

commit ce24ff503144adb1c60d93d967d6fdd55b4ba1af
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Wed Jul 11 11:41:51 2012 -0700

    [res-research] exclude A/AAAA glues if IPv4/IPv6 sockets are disabled.

commit 434ba5f414c152c1acdc508e9df85fbc7350a855
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Wed Jul 11 11:22:59 2012 -0700

    [res-research] consolidate NS-fetch conversion code.

commit 3baa2a5e7c3c710a4cbd4b35d089f6dc8de889df
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Wed Jul 11 11:21:04 2012 -0700

    [res-research] handle NXDOMAIN per rrtype. it's better for experiments.

commit 296a42873bfd22043d0bbb90b26cbf60cf79c80d
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Wed Jul 11 10:39:26 2012 -0700

    [res-research] added find_all for type ANY, and prefer CNAME with higher trust.

commit 93efac83d98d511e59d50ebe3c4b7440ca9f402b
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Tue Jul 10 21:59:45 2012 -0700

    [res-research] handled some more minor cases

commit 22f14c0a19f235ae8c4c6e0dbafb8f2b096a5daa
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Tue Jul 10 18:43:10 2012 -0700

    [res-research] made the code a bit more reusable.

commit 94b24f0e4aaaccf9de98f9c4029672bcd610bed7
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Tue Jul 10 18:42:36 2012 -0700

    [res-research] fixed a typo

commit ffdbede1decf3005e16ca8ef8fc7dec310835a17
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Tue Jul 10 13:27:00 2012 -0700

    [res-research] supported program mode; return rcode with rrset from find()

commit 6ca1c54a4accaa64f8d5525667653fbd3545ff4e
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Tue Jul 10 13:26:24 2012 -0700

    [res-research] handle some signals for better shutdown; improve stability.

-----------------------------------------------------------------------

Summary of changes:
 exp/res-research/analysis/dns_cache.py     |  225 ++++++++++++++-------
 exp/res-research/analysis/mini_resolver.py |  252 ++++++++++++++++-------
 exp/res-research/analysis/parse_qrylog.py  |    7 +-
 exp/res-research/analysis/query_replay.py  |  303 ++++++++++++++++++++++++++++
 4 files changed, 639 insertions(+), 148 deletions(-)
 create mode 100755 exp/res-research/analysis/query_replay.py

-----------------------------------------------------------------------
diff --git a/exp/res-research/analysis/dns_cache.py b/exp/res-research/analysis/dns_cache.py
index afc961f..32d3488 100755
--- a/exp/res-research/analysis/dns_cache.py
+++ b/exp/res-research/analysis/dns_cache.py
@@ -16,6 +16,7 @@
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 from isc.dns import *
+from optparse import OptionParser
 import struct
 
 # "root hint"
@@ -67,12 +68,14 @@ class CacheEntry:
 
     '''
 
-    def __init__(self, ttl, rdata_list, trust, msglen, rcode):
+    def __init__(self, ttl, rdata_list, trust, msglen, rcode, id):
         self.ttl = ttl
         self.rdata_list = rdata_list
         self.trust = trust
         self.msglen = msglen
         self.rcode = rcode.get_code()
+        self.id = id
+        self.time_updated = None # timestamp of 'creation' or 'update'
 
     def copy(self, other):
         self.ttl = other.ttl
@@ -80,6 +83,7 @@ class CacheEntry:
         self.trust = other.trust
         self.msglen = other.msglen
         self.rcode = other.rcode
+        self.id = other.id
 
 # Don't worry about cache expire; just record the RRs
 class SimpleDNSCache:
@@ -111,54 +115,118 @@ class SimpleDNSCache:
     def __init__(self):
         # top level cache table
         self.__table = {}
+        self.__counter = 0      # unique ID for CacheEntry's
+        self.__entries = {}     # ID => CacheEntry
 
     def find(self, name, rrclass, rrtype, options=FIND_DEFAULT):
         key = (name, rrclass)
+        rdata_map = self.__table.get((name, rrclass))
+        rcode, rrset, trust, id = self.__find_type(name, rrclass, rrtype,
+                                                   rdata_map, options)
+        if ((options & self.FIND_ALLOW_CNAME) != 0 and
+            rrtype != RRType.CNAME()):
+            # If CNAME is allowed, see if there is one.  If there is and it
+            # has a higher trust level, we use it.
+            rcode_cname, rrset_cname, trust_cname, id_cname = \
+                self.__find_type(name, rrclass, RRType.CNAME(), rdata_map,
+                                 options)
+            if (rrset_cname is not None and
+                (trust is None or trust_cname < trust)):
+                return rcode_cname, rrset_cname, id_cname
+        return rcode, rrset, id
+
+    def __find_nxdomain_dummy(self):
         if key in self.__table and isinstance(self.__table[key], CacheEntry):
-            # the name doesn't exist; the dict value is its negative TTL.
+            # the name doesn't exist.
             # lazy shortcut: we assume NXDOMAIN is always authoritative,
             # skipping trust level check
+            rcode = Rcode(self.__table[key].rcode)
+            if (options & self.FIND_ALLOW_NEGATIVE) != 0:
+                return rcode, RRset(name, rrclass, rrtype,
+                                    RRTTL(self.__table[key].ttl))
+            else:
+                return rcode, None
+
+    def __find_type(self, name, rrclass, type, rdata_map, options):
+        '''A subroutine of find, finding an RRset of a given type.'''
+        if rdata_map is not None and type in rdata_map:
+            entries = rdata_map[type]
+            entry = entries[0]
+            if (options & self.FIND_ALLOW_NOANSWER) == 0:
+                entry = self.__find_cache_entry(entries, self.TRUST_ANSWER)
+            if entry is None:
+                return None, None, None, None
+            rcode = Rcode(entry.rcode)
+            (ttl, rdata_list) = (entry.ttl, entry.rdata_list)
+            rrset = RRset(name, rrclass, type, RRTTL(ttl))
+            for rdata in rdata_list:
+                rrset.add_rdata(rdata)
+            if rrset.get_rdata_count() == 0 and \
+                    (options & self.FIND_ALLOW_NEGATIVE) == 0:
+                return rcode, None, None, None
+            return rcode, rrset, entry.trust, entry.id
+        return None, None, None, None
+
+        if key in self.__table and isinstance(self.__table[key], CacheEntry):
+            rcode = Rcode(self.__table[key].rcode)
             if (options & self.FIND_ALLOW_NEGATIVE) != 0:
-                return RRset(name, rrclass, rrtype,
-                             RRTTL(self.__table[key].ttl))
+                return rcode, RRset(name, rrclass, RRType.ANY(),
+                                    RRTTL(self.__table[key].ttl))
             else:
-                return None
+                return rcode, None
+
+    def find_all(self, name, rrclass, options=FIND_DEFAULT):
         rdata_map = self.__table.get((name, rrclass))
-        search_types = [rrtype]
-        if (options & self.FIND_ALLOW_CNAME) != 0 and \
-                rrtype != RRType.CNAME():
-            search_types.append(RRType.CNAME())
-        for type in search_types:
-            if rdata_map is not None and type in rdata_map:
-                entries = rdata_map[type]
-                entry = entries[0]
-                if (options & self.FIND_ALLOW_NOANSWER) == 0:
-                    entry = self.__find_cache_entry(entries, self.TRUST_ANSWER)
-                if entry is None:
-                    return None
-                (ttl, rdata_list) = (entry.ttl, entry.rdata_list)
-                rrset = RRset(name, rrclass, type, RRTTL(ttl))
-                for rdata in rdata_list:
-                    rrset.add_rdata(rdata)
-                if rrset.get_rdata_count() == 0 and \
-                        (options & self.FIND_ALLOW_NEGATIVE) == 0:
-                    return None
-                return rrset
-        return None
+        rrsets = []
+        for rrtype, entries in rdata_map.items():
+            entry = entries[0]
+            if (options & self.FIND_ALLOW_NOANSWER) == 0:
+                entry = self.__find_cache_entry(entries, self.TRUST_ANSWER)
+            if entry is None:
+                continue
+            rcode = Rcode(entry.rcode)
+
+            # If there's at least one NXDOMAIN result, this cancels all others.
+            if rcode == Rcode.NXDOMAIN():
+                nx_rrset = RRset(name, rrclass, RRType.ANY(),
+                                 RRTTL(entry.ttl))
+                return rcode, [(nx_rrset, entry.id)]
+
+            (ttl, rdata_list) = (entry.ttl, entry.rdata_list)
+            if len(rdata_list) == 0: # skip empty type
+                continue
+            rrset = RRset(name, rrclass, rrtype, RRTTL(ttl))
+            for rdata in rdata_list:
+                rrset.add_rdata(rdata)
+            rrsets.append((rrset, entry.id))
+        rcode = Rcode.NOERROR() if len(rrsets) > 0 else Rcode.NXRRSET()
+        return rcode, rrsets
+
+    def update(self, entry_id, now):
+        '''Check the specified cache entry is still "alive" wrt TTL.
+
+        If not, update its update/creation time to "now".
+        Return True if the timestamp is updated; False otherwise.
+
+        '''
+        entry = self.__entries[entry_id]
+        if entry.time_updated is None or entry.time_updated + entry.ttl < now:
+            entry.time_updated = now
+            return True
+        return False
 
     def add(self, rrset, trust=TRUST_LOCAL, msglen=0, rcode=Rcode.NOERROR()):
+        '''Add a new cache item.
+
+        Note: this cache always handles cached data per RR type; even if
+        NXDOMAIN is type independent, it's still specific to the associated
+        type within this cache.
+
+        '''
         key = (rrset.get_name(), rrset.get_class())
-        if rcode == Rcode.NXDOMAIN():
-            # Special case for NXDOMAIN: the table consists of a single cache
-            # entry.
-            self.__table[key] = CacheEntry(rrset.get_ttl().get_value(), [],
-                                           trust, msglen, rcode)
-            return
-        elif key in self.__table and isinstance(self.__table[key], RRset):
-            # Overriding a previously-NXDOMAIN cache entry
-            del self.__table[key]
-        new_entry = CacheEntry(rrset.get_ttl().get_value(), rrset.get_rdata(),
-                               trust, msglen, rcode)
+        new_entry = self.__create_cache_entry(rrset.get_ttl().get_value(),
+                                              rrset.get_rdata(), trust, msglen,
+                                              rcode)
         if not key in self.__table:
             self.__table[key] = {rrset.get_type(): [new_entry]}
         else:
@@ -169,6 +237,13 @@ class SimpleDNSCache:
             else:
                 self.__insert_cache_entry(cur_entries, new_entry)
 
+    def __create_cache_entry(self, ttl, rdata_list, trust, msglen, rcode):
+        new_entry = CacheEntry(ttl, rdata_list, trust, msglen, rcode,
+                               self.__counter)
+        self.__entries[self.__counter] = new_entry
+        self.__counter += 1
+        return new_entry
+
     def __insert_cache_entry(self, entries, new_entry):
         old = self.__find_cache_entry(entries, new_entry.trust, True)
         if old is not None and old.trust == new_entry.trust:
@@ -179,7 +254,7 @@ class SimpleDNSCache:
 
     def __find_cache_entry(self, entries, trust, exact=False):
         for entry in entries:
-            if entry.trust == trust or (not exact and entry.trust < trus):
+            if entry.trust == trust or (not exact and entry.trust < trust):
                 return entry
         return None
 
@@ -199,11 +274,6 @@ class SimpleDNSCache:
         for key, entry in self.__table.items():
             name = key[0]
             rrclass = key[1]
-            if isinstance(entry, CacheEntry):
-                f.write(';; [%s, TTL=%d, msglen=%d] %s/%s\n' %
-                        (str(Rcode(entry.rcode)), entry.ttl, entry.msglen,
-                         str(name), str(rrclass)))
-                continue
             rdata_map = entry
             for rrtype, entries in rdata_map.items():
                 for entry in entries:
@@ -229,21 +299,18 @@ class SimpleDNSCache:
           <domain name (wire)>
           <RR class (numeric, wire)>
           <# of cache entries, 2 bytes>
-        If #-of-entries is 0:
-          <Rcode value, 1 byte><TTL value, 4 bytes><msglen, 2 bytes>
-          <trust, 1 byte>
-        Else: sequence of serialized cache entries.  Each of which is:
-          <RR type value, wire>
-          <# of cache entries of the type, 1 byte>
-          sequence of cache entries of the type, each of which is:
-            <RCODE value, 1 byte>
-            <TTL, 4 bytes>
-            <msglen, 2 bytes>
-            <trust, 1 byte>
-            <# of RDATAs, 2 bytes>
-            sequence of RDATA, each of which is:
-              <RDATA length, 2 bytes>
-              <RDATA, wire>
+          Sequence of serialized cache entries.  Each of which is:
+            <RR type value, wire>
+            <# of cache entries of the type, 1 byte>
+            Sequence of cache entries of the type, each of which is:
+              <RCODE value, 1 byte>
+              <TTL, 4 bytes>
+              <msglen, 2 bytes>
+              <trust, 1 byte>
+              <# of RDATAs, 2 bytes>
+              sequence of RDATA, each of which is:
+                <RDATA length, 2 bytes>
+                <RDATA, wire>
 
         '''
         for key, entry in self.__table.items():
@@ -253,15 +320,6 @@ class SimpleDNSCache:
             f.write(name.to_wire(b''))
             f.write(rrclass.to_wire(b''))
 
-            if isinstance(entry, CacheEntry):
-                data = struct.pack('H', 0) # #-of-entries is 0
-                data += struct.pack('B', entry.rcode)
-                data += struct.pack('I', entry.ttl)
-                data += struct.pack('H', entry.msglen)
-                data += struct.pack('B', entry.trust)
-                f.write(data)
-                continue
-
             rdata_map = entry
             data = struct.pack('H', len(rdata_map)) # #-of-cache entries
             for rrtype, entries in rdata_map.items():
@@ -296,14 +354,6 @@ class SimpleDNSCache:
             rrclass = RRClass(f.read(2))
             key = (name, rrclass)
             n_types = struct.unpack('H', f.read(2))[0]
-            if n_types == 0:
-                rcode = struct.unpack('B', f.read(1))[0]
-                ttl = struct.unpack('I', f.read(4))[0]
-                msglen = struct.unpack('H', f.read(2))[0]
-                trust = struct.unpack('B', f.read(1))[0]
-                entry = CacheEntry(ttl, [], trust, msglen, Rcode(rcode))
-                self.__table[key] = entry
-                continue
 
             self.__table[key] = {}
             while n_types > 0:
@@ -324,8 +374,29 @@ class SimpleDNSCache:
                         rdata_len = struct.unpack('H', f.read(2))[0]
                         rdata_list.append(Rdata(rrtype, rrclass,
                                                 f.read(rdata_len)))
-                    entry = CacheEntry(ttl, rdata_list, trust, msglen,
-                                       Rcode(rcode))
+                    entry = self.__create_cache_entry(ttl, rdata_list, trust,
+                                                      msglen, Rcode(rcode))
                     entries.append(entry)
                 entries.sort(key=lambda x: x.trust)
                 self.__table[key][rrtype] = entries
+
+def get_option_parser():
+    parser = OptionParser(usage='usage: %prog [options] cache_db_file')
+    parser.add_option("-f", "--dump-file", dest="dump_file", action="store",
+                      default=None,
+                      help="if specified, file name to dump the cache " + \
+                          "content in text format")
+    return parser
+
+def run(db_file, options):
+    cache = SimpleDNSCache()
+    cache.load(db_file)
+    if options.dump_file is not None:
+        cache.dump(options.dump_file)
+
+if __name__ == '__main__':
+    parser = get_option_parser()
+    (options, args) = parser.parse_args()
+    if len(args) == 0:
+        parser.error('cache DB file is missing')
+    run(args[0], options)
diff --git a/exp/res-research/analysis/mini_resolver.py b/exp/res-research/analysis/mini_resolver.py
index 74daed1..ee183ff 100755
--- a/exp/res-research/analysis/mini_resolver.py
+++ b/exp/res-research/analysis/mini_resolver.py
@@ -18,10 +18,12 @@
 from isc.dns import *
 from dns_cache import SimpleDNSCache, install_root_hint
 import datetime
+import errno
 import heapq
 from optparse import OptionParser
 import random
 import re
+import signal
 import sys
 import socket
 import select
@@ -141,17 +143,21 @@ class ResolverContext:
         self.dprint(LOGLVL_DEBUG5, 'query timeout')
         cur_ns_addr = self.__try_next_server()
         if cur_ns_addr is not None:
-            return self.__qid, cur_ns_addr
+            return ResQuery(self, self.__qid, cur_ns_addr)
         self.dprint(LOGLVL_DEBUG1, 'no reachable server')
         fail_rrset = RRset(self.__qname, self.__qclass, self.__qtype,
                            RRTTL(self.SERVFAIL_TTL))
         self.__cache.add(fail_rrset, SimpleDNSCache.TRUST_ANSWER, 0,
                          Rcode.SERVFAIL())
-        return None, None
+        return self.__resume_parents()
 
     def handle_response(self, resp_msg, msglen):
         next_qry = None
         try:
+            if not resp_msg.get_header_flag(Message.HEADERFLAG_QR):
+                self.dprint(LOGLVL_INFO,
+                            'received query when expecting a response')
+                raise InternalLame('lame server')
             if resp_msg.get_rr_count(Message.SECTION_QUESTION) != 1:
                 self.dprint(LOGLVL_INFO,
                             'unexpected # of question in response: %s',
@@ -173,12 +179,21 @@ class ResolverContext:
                 raise InternalLame('lame server')
 
             # Look into the response
-            if resp_msg.get_header_flag(Message.HEADERFLAG_AA):
+            if (resp_msg.get_header_flag(Message.HEADERFLAG_AA) or
+                self.__is_cname_response(resp_msg)):
                 next_qry = self.__handle_auth_answer(resp_msg, msglen)
                 self.__handle_auth_othersections(resp_msg)
-            elif resp_msg.get_rcode() == Rcode.NOERROR() and \
-                    (not resp_msg.get_header_flag(Message.HEADERFLAG_AA)):
+            elif (resp_msg.get_rr_count(Message.SECTION_ANSWER) == 0 and
+                  resp_msg.get_rr_count(Message.SECTION_AUTHORITY) == 0 and
+                  (resp_msg.get_rcode() == Rcode.NOERROR() or
+                   resp_msg.get_rcode() == Rcode.NXDOMAIN())):
+                # Some servers return a negative response without setting AA.
+                # (Leave next_qry None)
+                self.__handle_negative_answer(resp_msg, msglen)
+            elif (resp_msg.get_rcode() == Rcode.NOERROR() and
+                  not resp_msg.get_header_flag(Message.HEADERFLAG_AA)):
                 authorities = resp_msg.get_section(Message.SECTION_AUTHORITY)
+                ns_name = None
                 for ns_rrset in authorities:
                     if ns_rrset.get_type() == RRType.NS():
                         ns_name =  ns_rrset.get_name()
@@ -191,7 +206,11 @@ class ResolverContext:
                                                          msglen)
                         if ns_addr is not None:
                             next_qry = ResQuery(self, self.__qid, ns_addr)
+                        elif len(self.__fetch_queries) == 0:
+                            raise InternalLame('no further recursion possible')
                         break
+                if ns_name is None:
+                    raise InternalLame('delegation with no NS')
             else:
                 raise InternalLame('lame server, rcode=' +
                                    str(resp_msg.get_rcode()))
@@ -206,24 +225,37 @@ class ResolverContext:
                                    RRTTL(self.SERVFAIL_TTL))
                 self.__cache.add(fail_rrset, SimpleDNSCache.TRUST_ANSWER, 0,
                                  Rcode.SERVFAIL())
-        if next_qry is None and not self.__fetch_queries and \
-                self.__parent is not None:
-            # This context is completed, resume the parent
-            next_qry = self.__parent.__resume(self)
+        if next_qry is None:
+             next_qry = self.__resume_parents()
         return next_qry
 
+    def __is_cname_response(self, resp_msg):
+        # From BIND 9: A BIND8 server could return a non-authoritative
+        # answer when a CNAME is followed.  We should treat it as a valid
+        # answer.
+        # This is real; for example some amazon.com servers behave that way.
+        # For simplicity we just check the first answer RR.
+        if (resp_msg.get_rcode() == Rcode.NOERROR() and
+            resp_msg.get_rr_count(Message.SECTION_ANSWER) > 0 and
+            resp_msg.get_section(Message.SECTION_ANSWER)[0].get_type() ==
+            RRType.CNAME()):
+            return True
+        return False
+
     def __handle_auth_answer(self, resp_msg, msglen):
         '''Subroutine of handle_response, handling an authoritative answer.'''
         if (resp_msg.get_rcode() == Rcode.NOERROR() or
             resp_msg.get_rcode() == Rcode.NXDOMAIN()) and \
             resp_msg.get_rr_count(Message.SECTION_ANSWER) > 0:
             any_query = resp_msg.get_question()[0].get_type() == RRType.ANY()
+            found = False
             for answer_rrset in resp_msg.get_section(Message.SECTION_ANSWER):
-                if answer_rrset.get_name() == self.__qname and \
-                        answer_rrset.get_class() == self.__qclass:
-                    self.__cache.add(answer_rrset, SimpleDNSCache.TRUST_ANSWER,
-                                     msglen)
+                if (answer_rrset.get_name() == self.__qname and
+                    answer_rrset.get_class() == self.__qclass):
                     if any_query or answer_rrset.get_type() == self.__qtype:
+                        found = True
+                        self.__cache.add(answer_rrset,
+                                         SimpleDNSCache.TRUST_ANSWER, msglen)
                         self.dprint(LOGLVL_DEBUG10, 'got a response: %s',
                                     [answer_rrset])
                         if not any_query:
@@ -231,31 +263,10 @@ class ResolverContext:
                             # simply ignore the rest.
                             return None
                     elif answer_rrset.get_type() == RRType.CNAME():
-                        self.dprint(LOGLVL_DEBUG10, 'got an alias: %s',
-                                    [answer_rrset])
-                        # Chase CNAME with a separate resolver context with
-                        # loop prevention
-                        if self.__nest > self.CNAME_LOOP_MAX:
-                            self.dprint(LOGLVL_INFO, 'possible CNAME loop')
-                            return None
-                        if self.__parent is not None:
-                            # Don't chase CNAME in an internal fetch context
-                            self.dprint(LOGLVL_INFO, 'CNAME in internal fetch')
-                            return None
-                        cname = Name(answer_rrset.get_rdata()[0].to_text())
-                        cname_ctx = ResolverContext(self.__sock4, self.__sock6,
-                                                    self.__renderer, cname,
-                                                    self.__qclass,
-                                                    self.__qtype, self.__cache,
-                                                    self.__qtable,
-                                                    self.__nest + 1)
-                        cname_ctx.set_debug_level(self.__debug_level)
-                        (qid, ns_addr) = cname_ctx.start()
-                        if ns_addr is not None:
-                            return ResQuery(cname_ctx, qid, ns_addr)
-                        return None
-            if any_query:
+                        return self.__handle_cname(answer_rrset, msglen)
+            if found:
                 return None
+            raise InternalLame('no answer found in answer section')
         elif resp_msg.get_rcode() == Rcode.NXDOMAIN() or \
                 (resp_msg.get_rcode() == Rcode.NOERROR() and
                  resp_msg.get_rr_count(Message.SECTION_ANSWER) == 0):
@@ -265,18 +276,61 @@ class ResolverContext:
         raise InternalLame('unexpected answer rcode=' +
                            str(resp_msg.get_rcode()))
 
+    def __handle_cname(self, cname_rrset, msglen):
+        self.dprint(LOGLVL_DEBUG10, 'got an alias: %s', [cname_rrset])
+        # Chase CNAME with a separate resolver context with
+        # loop prevention
+        if self.__nest > self.CNAME_LOOP_MAX:
+            self.dprint(LOGLVL_INFO, 'possible CNAME loop')
+            return None
+        if self.__parent is not None:
+            # Don't chase CNAME in an internal fetch context
+            self.dprint(LOGLVL_INFO, 'CNAME in internal fetch')
+            return None
+        cname = Name(cname_rrset.get_rdata()[0].to_text())
+
+        # Examine the current cache: Sometimes it's possisble CNAME has
+        # changed at the server side within a short period.
+        # It's not necessarily bogus, but that confuses our experiments.
+        # We consistently use cached one.
+        _, cached_rrset, _ = self.__cache.find(self.__qname, self.__qclass,
+                                               RRType.CNAME())
+        if cached_rrset is not None:
+            cached_cname = Name(cached_rrset.get_rdata()[0].to_text())
+            if cname != cached_cname:
+                self.dprint(LOGLVL_INFO, 'received CNAME %s' +
+                            ' different from cached %s', [cname, cached_cname])
+                cname = cached_cname
+        else:
+            self.__cache.add(cname_rrset, SimpleDNSCache.TRUST_ANSWER, msglen)
+
+        cname_ctx = ResolverContext(self.__sock4, self.__sock6,
+                                    self.__renderer, cname, self.__qclass,
+                                    self.__qtype, self.__cache, self.__qtable,
+                                    self.__nest + 1)
+        cname_ctx.set_debug_level(self.__debug_level)
+        (qid, ns_addr) = cname_ctx.start()
+        if ns_addr is not None:
+            return ResQuery(cname_ctx, qid, ns_addr)
+        return None
+
     def __handle_auth_othersections(self, resp_msg):
         ns_names = []
         for auth_rrset in resp_msg.get_section(Message.SECTION_AUTHORITY):
             if auth_rrset.get_type() == RRType.NS():
                 ns_owner =  auth_rrset.get_name()
                 cmp_reln = ns_owner.compare(self.__cur_zone).get_relation()
-                if cmp_reln == NameComparisonResult.SUBDOMAIN or \
-                        cmp_reln == NameComparisonResult.EQUAL:
+                if (cmp_reln == NameComparisonResult.SUBDOMAIN or
+                    cmp_reln == NameComparisonResult.EQUAL):
                     self.__cache.add(auth_rrset,
                                      SimpleDNSCache.TRUST_AUTHAUTHORITY, 0)
                     for ns_rdata in auth_rrset.get_rdata():
-                        ns_names.append(Name(ns_rdata.to_text()))
+                        ns_name = Name(ns_rdata.to_text())
+                        cmp_reln = \
+                            ns_name.compare(self.__cur_zone).get_relation()
+                        if (cmp_reln == NameComparisonResult.SUBDOMAIN or
+                            cmp_reln == NameComparisonResult.EQUAL):
+                            ns_names.append(Name(ns_rdata.to_text()))
         for ad_rrset in resp_msg.get_section(Message.SECTION_ADDITIONAL):
             if ad_rrset.get_type() == RRType.A() or \
                     ad_rrset.get_type() == RRType.AAAA():
@@ -311,14 +365,14 @@ class ResolverContext:
                 break      # Ignore any other records once we find SOA
 
         if neg_ttl is None:
-            self.dprint(LOGLVL_INFO, 'negative answer, code=%s, (missing SOA)',
-                        [rcode])
+            self.dprint(LOGLVL_DEBUG1,
+                        'negative answer, code=%s, (missing SOA)', [rcode])
             neg_ttl = self.DEFAULT_NEGATIVE_TTL
         neg_rrset = RRset(self.__qname, self.__qclass, self.__qtype,
                           RRTTL(neg_ttl))
         self.__cache.add(neg_rrset, SimpleDNSCache.TRUST_ANSWER, 0, rcode)
 
-    def __handle_referral(self, resp_msg, ns_rrset,msglen):
+    def __handle_referral(self, resp_msg, ns_rrset, msglen):
         self.dprint(LOGLVL_DEBUG10, 'got a referral: %s', [ns_rrset])
         self.__cache.add(ns_rrset, SimpleDNSCache.TRUST_GLUE, msglen)
         additionals = resp_msg.get_section(Message.SECTION_ADDITIONAL)
@@ -332,7 +386,7 @@ class ResolverContext:
                 continue
             if ad_rrset.get_type() == RRType.A() or \
                     ad_rrset.get_type() == RRType.AAAA():
-                self.dprint(LOGLVL_DEBUG10, 'got glue for referral:%s',
+                self.dprint(LOGLVL_DEBUG10, 'got glue for referral: %s',
                             [ad_rrset])
                 self.__cache.add(ad_rrset, SimpleDNSCache.TRUST_GLUE)
         self.__cur_zone = ns_rrset.get_name()
@@ -384,8 +438,9 @@ class ResolverContext:
         ns_rrset = None
         for l in range(0, zname.get_labelcount()):
             zname = qname.split(l)
-            ns_rrset = self.__cache.find(zname, self.__qclass, RRType.NS(),
-                                         SimpleDNSCache.FIND_ALLOW_NOANSWER)
+            _, ns_rrset, _ = \
+                self.__cache.find(zname, self.__qclass, RRType.NS(),
+                                  SimpleDNSCache.FIND_ALLOW_NOANSWER)
             if ns_rrset is not None:
                 return zname, ns_rrset
         raise MiniResolverException('no name server found for ' + str(qname))
@@ -393,23 +448,35 @@ class ResolverContext:
     def __find_ns_addrs(self, nameservers, fetch_if_notfound=True):
         v4_addrs = []
         v6_addrs = []
+        rcode4 = None
+        rcode6 = None
         ns_names = []
         ns_class = nameservers.get_class()
         for ns in nameservers.get_rdata():
             ns_name = Name(ns.to_text())
             ns_names.append(ns_name)
-            rrset4 = self.__cache.find(ns_name, ns_class, RRType.A(),
-                                       SimpleDNSCache.FIND_ALLOW_NOANSWER)
-            if rrset4 is not None:
-                for rdata in rrset4.get_rdata():
-                    v4_addrs.append((rdata.to_text(), DNS_PORT))
-            rrset6 = self.__cache.find(ns_name, ns_class, RRType.AAAA(),
+            if self.__sock4:
+                rcode4, rrset4, _ = \
+                    self.__cache.find(ns_name, ns_class, RRType.A(),
                                       SimpleDNSCache.FIND_ALLOW_NOANSWER)
-            if rrset6 is not None:
-                for rdata in rrset6.get_rdata():
-                    # specify 0 for flowinfo and scopeid unconditionally
-                    v6_addrs.append((rdata.to_text(), DNS_PORT, 0, 0))
-        if fetch_if_notfound and not v4_addrs and not v6_addrs:
+                if rrset4 is not None:
+                    for rdata in rrset4.get_rdata():
+                        v4_addrs.append((rdata.to_text(), DNS_PORT))
+            if self.__sock6:
+                rcode6, rrset6, _ = \
+                    self.__cache.find(ns_name, ns_class, RRType.AAAA(),
+                                      SimpleDNSCache.FIND_ALLOW_NOANSWER)
+                if rrset6 is not None:
+                    for rdata in rrset6.get_rdata():
+                        # specify 0 for flowinfo and scopeid unconditionally
+                        v6_addrs.append((rdata.to_text(), DNS_PORT, 0, 0))
+
+        # If necessary and required, invoke NS-fetch queries.  If rcodeN is not
+        # None, we know the corresponding AAAA/A is not available (either
+        # due to server failure or because records don't exist), in which case
+        # we don't bother to fetch them.
+        if (fetch_if_notfound and not v4_addrs and not v6_addrs and
+            rcode4 is None and rcode6 is None):
             self.dprint(LOGLVL_DEBUG5,
                         'no address found for any nameservers')
             if self.__nest > self.FETCH_DEPTH_MAX:
@@ -432,7 +499,34 @@ class ResolverContext:
             query = ResQuery(res_ctx, qid, ns_addr)
             self.__fetch_queries.add(query)
 
+    def __resume_parents(self):
+        ctx = self
+        while not ctx.__fetch_queries and ctx.__parent is not None:
+            resumed, next_qry = ctx.__parent.__resume(ctx)
+            if next_qry is not None:
+                return next_qry
+            if not resumed:
+                # this parent is still waiting for some queries.  We're done
+                # for now.
+                return None
+
+            # There's no more hope for completing the parent context.
+            # Cache SERVFAIL.
+            ctx.__parent.dprint(LOGLVL_DEBUG1, 'resumed context failed')
+            fail_rrset = RRset(ctx.__parent.__qname, ctx.__parent.__qclass,
+                               ctx.__parent.__qtype, RRTTL(self.SERVFAIL_TTL))
+            self.__cache.add(fail_rrset, SimpleDNSCache.TRUST_ANSWER, 0,
+                             Rcode.SERVFAIL())
+            # Recursively check grand parents
+            ctx = ctx.__parent
+        return None
+
     def __resume(self, fetch_ctx):
+        # Resume the parent if the current context completes the last
+        # outstanding fetch query.
+        # Return (bool, ResQuery): boolean is true/false if the parent is
+        #   actually resumed/still suspended, respectively; ResQuery is not
+        #   None iff the parrent is resumed and restarts a new query.
         for qry in self.__fetch_queries:
             if qry.res_ctx == fetch_ctx:
                 self.__fetch_queries.remove(qry)
@@ -443,11 +537,11 @@ class ResolverContext:
                         self.__find_ns_addrs(self.__cur_nameservers, False)
                     ns_addr = self.__try_next_server()
                     if ns_addr is not None:
-                        return ResQuery(self, self.__qid, ns_addr)
+                        return True, ResQuery(self, self.__qid, ns_addr)
                     else:
-                        return None
+                        return True, None
                 else:
-                    return None
+                    return False, None # still waiting for some queries
         raise MiniResolverException('unexpected case: fetch query not found')
 
 class TimerQueue:
@@ -536,6 +630,8 @@ class FileResolver:
         else:
             self.__sock6 = None
 
+        self.__shutdown = False
+
         # Create shared resource
         self.__renderer = MessageRenderer()
         self.__msg = Message(Message.PARSE)
@@ -558,7 +654,13 @@ class FileResolver:
         sys.stdout.write(('%s ' + msg + '\n') %
                          tuple([date_time] + [str(p) for p in params]))
 
+    def shutdown(self):
+        self.dprint(LOGLVL_INFO, 'resolver shutting down')
+        self.__shutdown = True
+
     def __check_status(self):
+        if self.__shutdown:
+            return False
         for i in range(len(self.__res_ctxs), self.__max_ctxts):
             res_ctx = self.__get_next_query()
             if res_ctx is None:
@@ -597,7 +699,12 @@ class FileResolver:
                 timo = expire - now if expire > now else 0
             else:
                 timo = None
-            (r, _, _) = select.select(self.__select_socks, [], [], timo)
+            try:
+                (r, _, _) = select.select(self.__select_socks, [], [], timo)
+            except select.error as ex:
+                self.dprint(LOGLVL_INFO, 'select failure: %s', [ex])
+                if ex.args[0] == errno.EINTR:
+                    continue
             if not r:
                 # timeout
                 now = time.time()
@@ -656,17 +763,21 @@ class FileResolver:
 
     def _qry_timeout(self, res_qry):
         del self.__query_table[(res_qry.qid, res_qry.ns_addr)]
-        (qid, addr) = res_qry.res_ctx.query_timeout(res_qry.ns_addr)
-        if addr is not None:
-            next_res_qry = ResQuery(res_qry.res_ctx, qid, addr)
-            self.__query_table[(qid, addr)] = next_res_qry
-            timer = QueryTimer(self, next_res_qry)
-            self.__timerq.add(next_res_qry.expire, timer)
-        else:
+        next_res_qry = res_qry.res_ctx.query_timeout(res_qry.ns_addr)
+        if next_res_qry is None or next_res_qry.res_ctx != res_qry.res_ctx:
+            # the current context is completed.  remove it from the queue.
             res_qry.res_ctx.dprint(LOGLVL_DEBUG1,
                                    'resolution timeout, remaining ctx=%s',
                                    [len(self.__res_ctxs)])
             self.__res_ctxs.remove(res_qry.res_ctx)
+        if next_res_qry is not None:
+            if next_res_qry.res_ctx != res_qry.res_ctx:
+                # context has been replaced.  push it to the queue
+                self.__res_ctxs.add(next_res_qry.res_ctx)
+            self.__query_table[(next_res_qry.qid, next_res_qry.ns_addr)] = \
+                next_res_qry
+            timer = QueryTimer(self, next_res_qry)
+            self.__timerq.add(next_res_qry.expire, timer)
 
 def get_option_parser():
     parser = OptionParser(usage='usage: %prog [options] query_file')
@@ -701,6 +812,11 @@ if __name__ == '__main__':
 
     if len(args) == 0:
         parser.error('query file is missing')
+
     resolver = FileResolver(args[0], options)
+
+    signal.signal(signal.SIGINT, lambda sig, frame: resolver.shutdown())
+    signal.signal(signal.SIGTERM, lambda sig, frame: resolver.shutdown())
+
     resolver.run()
     resolver.done()
diff --git a/exp/res-research/analysis/parse_qrylog.py b/exp/res-research/analysis/parse_qrylog.py
index ee32401..ab5dcbb 100755
--- a/exp/res-research/analysis/parse_qrylog.py
+++ b/exp/res-research/analysis/parse_qrylog.py
@@ -20,6 +20,9 @@ import sys
 from isc.dns import *
 from optparse import OptionParser
 
+# ssss.mmm ip_addr#port qname qclass qtype
+RE_LOGLINE = re.compile(r'^([\d\.]*) ([\d\.]*)#\d+ (\S*) (\S*) (\S*)$')
+
 queries = {}
 
 def convert_rrtype(type_txt):
@@ -35,13 +38,11 @@ def convert_rrtype(type_txt):
     return type_txt
 
 def parse_logfile(log_file):
-    # ssss.mmm ip_addr#port qname qclass qtype
-    re_logline = re.compile(r'^([\d\.]*) ([\d\.]*)#\d+ (\S*) (\S*) (\S*)$')
     n_queries = 0
     with open(log_file) as log:
         for log_line in log:
             n_queries += 1
-            m = re.match(re_logline, log_line)
+            m = re.match(RE_LOGLINE, log_line)
             if not m:
                 sys.stderr.write('unexpected line: ' + log_line)
                 continue
diff --git a/exp/res-research/analysis/query_replay.py b/exp/res-research/analysis/query_replay.py
new file mode 100755
index 0000000..c821689
--- /dev/null
+++ b/exp/res-research/analysis/query_replay.py
@@ -0,0 +1,303 @@
+#!/usr/bin/env python3.2
+
+# Copyright (C) 2012  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from isc.dns import *
+import parse_qrylog
+import dns_cache
+from optparse import OptionParser
+import re
+import sys
+
+convert_rrtype = parse_qrylog.convert_rrtype
+RE_LOGLINE = parse_qrylog.RE_LOGLINE
+
+class QueryReplaceError(Exception):
+    pass
+
+class QueryTrace:
+    '''consists of...
+
+    ttl(int): TTL
+    resp_size(int): estimated size of the response to the query.
+
+    '''
+    def __init__(self, ttl, cache_entries):
+        self.ttl = ttl
+        self.__cache_entries = cache_entries
+        self.cname_trace = []
+        self.__cache_log = []
+
+    def get_query_count(self):
+        '''Return the total count of this query'''
+        n_queries = 0
+        for log in self.__cache_log:
+            n_queries += log.misses + log.hits
+        return n_queries
+
+    def get_cache_hits(self):
+        '''Return the total count of cache hits for the query'''
+        hits = 0
+        for log in self.__cache_log:
+            hits += log.hits
+        return hits
+
+    def add_cache_log(self, cache_log):
+        self.__cache_log.append(cache_log)
+
+    def get_last_cache(self):
+        if len(self.__cache_log) == 0:
+            return None
+        return self.__cache_log[-1]
+
+    def cache_expired(self, cache, now):
+        '''Check if the cache for this query has expired or is still valid.
+
+        For type ANY query, __cache_entries may contain multiple entry IDs.
+        In that case we consider it valid as long as one of them is valid.
+
+        '''
+        expired = False
+        for trace in [self] + self.cname_trace:
+            if trace.__cache_expired(cache, now):
+               expired = True
+        return expired
+
+    def __cache_expired(self, cache, now):
+        updated = 0
+        for cache_entry_id in self.__cache_entries:
+            if cache.update(cache_entry_id, now):
+                updated += 1
+        return len(self.__cache_entries) == updated
+
+class CacheLog:
+    '''consists of...
+
+    __time_created (float): Timestamp when an answer for the query is cached.
+    hits (int): number of cache hits
+    misses (int): 1 if this is created on cache miss (normal); otherwise 0
+    TBD:
+      number of external queries involved along with the response sizes
+
+    '''
+    def __init__(self, now, on_miss=True):
+        self.__time_created = now
+        self.hits = 0
+        self.misses = 1 if on_miss else 0
+
+class QueryReplay:
+    CACHE_OPTIONS = dns_cache.SimpleDNSCache.FIND_ALLOW_CNAME | \
+        dns_cache.SimpleDNSCache.FIND_ALLOW_NEGATIVE
+
+    def __init__(self, log_file, cache):
+        self.__log_file = log_file
+        self.__cache = cache
+        # Replay result.
+        # dict from (Name, RR type, RR class) to QueryTrace
+        self.__queries = {}
+        self.__total_queries = 0
+        self.__query_params = None
+        self.__resp_msg = Message(Message.RENDER) # for resp size estimation
+        self.__renderer = MessageRenderer()
+
+    def replay(self):
+        with open(self.__log_file) as f:
+            for log_line in f:
+                self.__total_queries += 1
+                try:
+                    self.__replay_query(log_line)
+                except Exception as ex:
+                    sys.stderr.write('error (' + str(ex) + ') at line: ' +
+                                     log_line)
+                    raise ex
+        return self.__total_queries, len(self.__queries)
+
+    def __replay_query(self, log_line):
+        '''Replay a single query.'''
+        m = re.match(RE_LOGLINE, log_line)
+        if not m:
+            sys.stderr.write('unexpected line: ' + log_line)
+            return
+        qry_time = float(m.group(1))
+        client_addr = m.group(2)
+        qry_name = Name(m.group(3))
+        qry_class = RRClass(m.group(4))
+        qry_type = RRType(convert_rrtype(m.group(5)))
+        qry_key = (qry_name, qry_type, qry_class)
+
+        qinfo = self.__queries.get(qry_key)
+        if qinfo is None:
+            qinfo, rrsets = \
+                self.__get_query_trace(qry_name, qry_class, qry_type)
+            qinfo.resp_size = self.__calc_resp_size(qry_name, rrsets)
+            self.__queries[qry_key] = qinfo
+        if qinfo.cache_expired(self.__cache, qry_time):
+            cache_log = CacheLog(qry_time)
+            qinfo.add_cache_log(cache_log)
+        else:
+            cache_log = qinfo.get_last_cache()
+            if cache_log is None:
+                cache_log = CacheLog(qry_time, False)
+                qinfo.add_cache_log(cache_log)
+            cache_log.hits += 1
+
+    def __calc_resp_size(self, qry_name, rrsets):
+        self.__renderer.clear()
+        self.__resp_msg.clear(Message.RENDER)
+        # Most of the header fields don't matter for the calculation
+        self.__resp_msg.set_opcode(Opcode.QUERY())
+        self.__resp_msg.set_rcode(Rcode.NOERROR())
+        # Likewise, rrclass and type don't affect the result.
+        self.__resp_msg.add_question(Question(qry_name, RRClass.IN(),
+                                              RRType.AAAA()))
+        for rrset in rrsets:
+            # For now, don't bother to try to include SOA for negative resp.
+            if rrset.get_rdata_count() == 0:
+                continue
+            self.__resp_msg.add_rrset(Message.SECTION_ANSWER, rrset)
+        self.__resp_msg.to_wire(self.__renderer)
+        return len(self.__renderer.get_data())
+
+    def __get_query_trace(self, qry_name, qry_class, qry_type):
+        '''Create a new trace object for a query.'''
+        # For type ANY queries there's no need for tracing CNAME chain
+        if qry_type == RRType.ANY():
+            rcode, val = self.__cache.find_all(qry_name, qry_class,
+                                               self.CACHE_OPTIONS)
+            ttl, ids, rrsets = self.__get_type_any_info(rcode, val)
+            return QueryTrace(ttl, ids), rrsets
+
+        rcode, rrset, id = self.__cache.find(qry_name, qry_class, qry_type,
+                                             self.CACHE_OPTIONS)
+        # Same for type CNAME query or when it's not CNAME substitution.
+        qtrace = QueryTrace(rrset.get_ttl().get_value(), [id])
+        if qry_type == RRType.CNAME() or rrset.get_type() != RRType.CNAME():
+            return qtrace, [rrset]
+
+        # This query is subject to CNAME chain.  It's possible it consists
+        # a loop, so we need to detect loops and exits.
+        rrtype = rrset.get_type()
+        if rrtype != RRType.CNAME():
+            raise QueryReplaceError('unexpected: type should be CNAME, not ' +
+                                    str(rrtype))
+        chain_trace = []
+        cnames = []
+        resp_rrsets = [rrset]
+        while rrtype == RRType.CNAME():
+            if len(chain_trace) == 16: # safety net: don't try too hard
+                break
+            cname = Name(rrset.get_rdata()[0].to_text())
+            for prev in cnames:
+                if cname == prev: # explicit loop detected
+                    break
+            rcode, rrset, id = self.__cache.find(cname, qry_class, qry_type,
+                                                 self.CACHE_OPTIONS)
+            try:
+                chain_trace.append(QueryTrace(rrset.get_ttl().get_value(),
+                                              [id]))
+                cnames.append(cname)
+                resp_rrsets.append(rrset)
+                rrtype = rrset.get_type()
+            except Exception as ex:
+                sys.stderr.write('CNAME trace failed for %s/%s/%s at %s\n' %
+                                 (qry_name, qry_class, qry_type, cname))
+                break
+        qtrace.cname_trace = chain_trace
+        return qtrace, resp_rrsets
+
+    def __get_type_any_info(self, rcode, find_val):
+        ttl = 0
+        cache_entries = []
+        resp_rrsets = []
+        for entry_info in find_val:
+            rrset = entry_info[0]
+            cache_entries.append(entry_info[1])
+            rrset_ttl = rrset.get_ttl().get_value()
+            if ttl < rrset_ttl:
+                ttl = rrset_ttl
+            resp_rrsets.append(rrset)
+        return ttl, cache_entries, resp_rrsets
+
+    def __get_query_params(self):
+        if self.__query_params is None:
+            self.__query_params = list(self.__queries.keys())
+            self.__query_params.sort(
+                key=lambda x: -self.__queries[x].get_query_count())
+        return self.__query_params
+
+    def dump_popularity_stat(self, dump_file):
+        cumulative_n_qry = 0
+        cumulative_cache_hits = 0
+        position = 1
+        with open(dump_file, 'w') as f:
+            f.write('position,% in total,hit rate,#CNAME,resp-size\n')
+            for qry_param in self.__get_query_params():
+                qinfo = self.__queries[qry_param]
+                n_queries = qinfo.get_query_count()
+                cumulative_n_qry += n_queries
+                cumulative_percentage = \
+                    (float(cumulative_n_qry) / self.__total_queries) * 100
+
+                cumulative_cache_hits += qinfo.get_cache_hits()
+                cumulative_hit_rate = \
+                    (float(cumulative_cache_hits) / cumulative_n_qry) * 100
+
+                f.write('%d,%.2f,%.2f,%d,%d\n' %
+                        (position, cumulative_percentage, cumulative_hit_rate,
+                         len(qinfo.cname_trace), qinfo.resp_size))
+                position += 1
+
+    def dump_queries(self, dump_file):
+        with open(dump_file, 'w') as f:
+            for qry_param in self.__get_query_params():
+                qinfo = self.__queries[qry_param]
+                f.write('%d/%s/%s/%s\n' % (qinfo.get_query_count(),
+                                           qry_param[2], qry_param[0],
+                                           qry_param[1]))
+
+def main(log_file, options):
+    cache = dns_cache.SimpleDNSCache()
+    cache.load(options.cache_dbfile)
+    replay = QueryReplay(log_file, cache)
+    total_queries, uniq_queries = replay.replay()
+    print('Replayed %d queries (%d unique)' % (total_queries, uniq_queries))
+    if options.popularity_file is not None:
+        replay.dump_popularity_stat(options.popularity_file)
+    if options.query_dump_file is not None:
+        replay.dump_queries(options.query_dump_file)
+
+def get_option_parser():
+    parser = OptionParser(usage='usage: %prog [options] log_file')
+    parser.add_option("-c", "--cache-dbfile",
+                      dest="cache_dbfile", action="store", default=None,
+                      help="Serialized DNS cache DB")
+    parser.add_option("-p", "--dump-popularity",
+                      dest="popularity_file", action="store",
+                      help="dump statistics per query popularity")
+    parser.add_option("-q", "--dump-queries",
+                      dest="query_dump_file", action="store",
+                      help="dump unique queries")
+    return parser
+
+if __name__ == "__main__":
+    parser = get_option_parser()
+    (options, args) = parser.parse_args()
+
+    if len(args) == 0:
+        parser.error('input file is missing')
+    if options.cache_dbfile is None:
+        parser.error('cache DB file is mandatory')
+    main(args[0], options)



More information about the bind10-changes mailing list