DNS: Let requests specify a callback for future cache hits

To measure the effectiveness of various speculative DNS measures, allow
DNS requests to contain a cache hit callback that will be called every
time another request returns synchronously with cached data from the
same entry that the original request wrote or read.

To clarify, here's an example sequence of operations (assuming all
requests have callbacks set):

1. Request A resolves a name.
2. Request B resolves the same name while the result is still valid;
   the resolver calls request A's cache hit callback.
3. Request C resolves the same name again; the resolver calls the
   callbacks for both request A and B.
4. The cache is cleared on a network change.
5. Requests D and E both resolve the name. No callbacks are called,
   since there was no result cached for either request.
6. Request F resolves the name; the resolver calls the callbacks for
   requests D and E.

Notably, if multiple speculative resolutions occur, the cache hit
callbacks pile up, and future cache hits will call *all* of them.

BUG=621554

Review-Url: https://codereview.chromium.org/2083643003
Cr-Commit-Position: refs/heads/master@{#409262}
diff --git a/net/dns/host_resolver_impl.cc b/net/dns/host_resolver_impl.cc
index 8d1cfda..a599adc 100644
--- a/net/dns/host_resolver_impl.cc
+++ b/net/dns/host_resolver_impl.cc
@@ -1775,8 +1775,12 @@
 
     bool did_complete = (entry.error() != ERR_NETWORK_CHANGED) &&
                         (entry.error() != ERR_HOST_RESOLVER_QUEUE_TOO_LARGE);
-    if (did_complete)
+    if (did_complete) {
       resolver_->CacheResult(key_, entry, ttl);
+      // Erase any previous cache hit callbacks, since a new DNS request went
+      // out since they were set.
+      resolver_->cache_hit_callbacks_.erase(key_);
+    }
 
     // Complete all of the requests that were attached to the job and
     // detach them.
@@ -1787,6 +1791,7 @@
       // Update the net log and notify registered observers.
       LogFinishRequest(req->source_net_log(), req->info(), entry.error());
       if (did_complete) {
+        resolver_->MaybeAddCacheHitCallback(key_, req->info());
         // Record effective total time from creation to completion.
         RecordTotalTime(had_dns_config_, req->info().is_speculative(),
                         base::TimeTicks::Now() - req->request_time());
@@ -1933,6 +1938,7 @@
   int rv = ResolveHelper(key, info, ip_address_ptr, addresses, false, nullptr,
                          source_net_log);
   if (rv != ERR_DNS_CACHE_MISS) {
+    MaybeAddCacheHitCallback(key, info);
     LogFinishRequest(source_net_log, info, rv);
     RecordTotalTime(HaveDnsConfig(), info.is_speculative(), base::TimeDelta());
     return rv;
@@ -2061,6 +2067,7 @@
                      stale_info)) {
     source_net_log.AddEvent(NetLog::TYPE_HOST_RESOLVER_IMPL_CACHE_HIT);
     // |ServeFromCache()| will set |*stale_info| as needed.
+    RunCacheHitCallbacks(key, info);
     return net_error;
   }
   // TODO(szym): Do not do this if nsswitch.conf instructs not to.
@@ -2418,8 +2425,10 @@
   last_ipv6_probe_time_ = base::TimeTicks();
   // Abandon all ProbeJobs.
   probe_weak_ptr_factory_.InvalidateWeakPtrs();
-  if (cache_.get())
+  if (cache_.get()) {
     cache_->clear();
+    cache_hit_callbacks_.clear();
+  }
 #if defined(OS_POSIX) && !defined(OS_MACOSX) && !defined(OS_ANDROID)
   RunLoopbackProbeJob();
 #endif
@@ -2478,8 +2487,10 @@
     // have to drop our internal cache :( Note that OS level DNS caches, such
     // as NSCD's cache should be dropped automatically by the OS when
     // resolv.conf changes so we don't need to do anything to clear that cache.
-    if (cache_.get())
+    if (cache_.get()) {
       cache_->clear();
+      cache_hit_callbacks_.clear();
+    }
 
     // Life check to bail once |this| is deleted.
     base::WeakPtr<HostResolverImpl> self = weak_ptr_factory_.GetWeakPtr();
@@ -2525,6 +2536,28 @@
                               std::abs(net_error));
 }
 
+void HostResolverImpl::OnCacheEntryEvicted(const HostCache::Key& key,
+                                           const HostCache::Entry& entry) {
+  cache_hit_callbacks_.erase(key);
+}
+
+void HostResolverImpl::MaybeAddCacheHitCallback(const HostCache::Key& key,
+                                                const RequestInfo& info) {
+  const RequestInfo::CacheHitCallback& callback = info.cache_hit_callback();
+  if (callback.is_null())
+    return;
+  cache_hit_callbacks_[key].push_back(callback);
+}
+
+void HostResolverImpl::RunCacheHitCallbacks(const HostCache::Key& key,
+                                            const RequestInfo& info) {
+  auto it = cache_hit_callbacks_.find(key);
+  if (it == cache_hit_callbacks_.end())
+    return;
+  for (auto& callback : it->second)
+    callback.Run(info);
+}
+
 void HostResolverImpl::SetDnsClient(std::unique_ptr<DnsClient> dns_client) {
   // DnsClient and config must be updated before aborting DnsTasks, since doing
   // so may start new jobs.
@@ -2552,6 +2585,13 @@
     ApplyPersistentData(std::move(old_data));
 }
 
+void HostResolverImpl::ApplyPersistentData(
+    std::unique_ptr<const base::Value> data) {}
+
+std::unique_ptr<const base::Value> HostResolverImpl::GetPersistentData() {
+  return std::unique_ptr<const base::Value>();
+}
+
 void HostResolverImpl::SchedulePersist() {
   if (!persist_initialized_ || persist_timer_.IsRunning())
     return;
@@ -2565,13 +2605,6 @@
   persist_callback_.Run(GetPersistentData());
 }
 
-void HostResolverImpl::ApplyPersistentData(
-    std::unique_ptr<const base::Value> data) {}
-
-std::unique_ptr<const base::Value> HostResolverImpl::GetPersistentData() {
-  return std::unique_ptr<const base::Value>();
-}
-
 HostResolverImpl::RequestImpl::~RequestImpl() {
   if (job_)
     job_->CancelRequest(this);