TTL Caching

Step 4: TTL Caching

Resolvers cache answers so they don't re-query for every request. Each record carries a TTL (seconds to live). Cache a name's A values until the TTL expires, using the injected clock.

  • update resolve(name) to serve from the cache while it's fresh; on a miss (or expiry) look up, then cache the A values with an expiry of now + ttl (use the smallest TTL among the A records)

While cached, changes to the underlying zone are not visible until the entry expires, that's the whole point of a cache.

Hints
Starter code
import time


class DNSResolver:
    def __init__(self, zone=None, clock=time.time):
        self.zone = zone if zone is not None else {}
        self.clock = clock
        self._rr = {}
        self._cache = {}              # name -> (a_values, expiry)

    def _lookup(self, name, seen=None):
        seen = seen or set()
        if name in seen:
            return ([], 0)
        seen.add(name)
        records = self.zone.get(name, [])
        a_records = [r for r in records if r["type"] == "A"]
        if a_records:
            values = [r["value"] for r in a_records]
            ttl = min(r["ttl"] for r in a_records)
            return (values, ttl)
        for r in records:
            if r["type"] == "CNAME":
                return self._lookup(r["value"], seen)
        return ([], 0)

    def resolve(self, name):
        now = self.clock()
        cached = self._cache.get(name)
        if cached and now < cached[1]:
            values = cached[0]
        else:
            # TODO: cache miss/expiry. Look up and cache:
            #   values, ttl = self._lookup(name)
            #   if not values: return None
            #   self._cache[name] = (values, now + ttl)
            pass
        i = self._rr.get(name, 0) % len(values)
        self._rr[name] = self._rr.get(name, 0) + 1
        return values[i]
Write it on a laptop

The editor and the test runner need a wide screen. Open this page on a desktop browser and your progress will be waiting.