]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add stale-answer-client-timeout option
authorDiego Fronza <diego@isc.org>
Fri, 11 Dec 2020 17:10:31 +0000 (14:10 -0300)
committerDiego Fronza <diego@isc.org>
Mon, 25 Jan 2021 13:47:14 +0000 (10:47 -0300)
The general logic behind the addition of this new feature works as
folows:

When a client query arrives, the basic path (query.c / ns_query_recurse)
was to create a fetch, waiting for completion in fetch_callback.

With the introduction of stale-answer-client-timeout, a new event of
type DNS_EVENT_TRYSTALE may invoke fetch_callback, whenever stale
answers are enabled and the fetch took longer than
stale-answer-client-timeout to complete.

When an event of type DNS_EVENT_TRYSTALE triggers fetch_callback, we
must ensure that the folowing happens:

1. Setup a new query context with the sole purpose of looking up for
   stale RRset only data, for that matters a new flag was added
   'DNS_DBFIND_STALEONLY' used in database lookups.

    . If a stale RRset is found, mark the original client query as
      answered (with a new query attribute named NS_QUERYATTR_ANSWERED),
      so when the fetch completion event is received later, we avoid
      answering the client twice.

    . If a stale RRset is not found, cleanup and wait for the normal
      fetch completion event.

2. In ns_query_done, we must change this part:
/*
 * If we're recursing then just return; the query will
 * resume when recursion ends.
 */
if (RECURSING(qctx->client)) {
return (qctx->result);
}

   To this:

if (RECURSING(qctx->client) && !QUERY_STALEONLY(qctx->client)) {
return (qctx->result);
}

   Otherwise we would not proceed to answer the client if it happened
   that a stale answer was found when looking up for stale only data.

When an event of type DNS_EVENT_FETCHDONE triggers fetch_callback, we
proceed as before, resuming query, updating stats, etc, but a few
exceptions had to be added, most important of which are two:

1. Before answering the client (ns_client_send), check if the query
   wasn't already answered before.

2. Before detaching a client, e.g.
   isc_nmhandle_detach(&client->reqhandle), ensure that this is the
   fetch completion event, and not the one triggered due to
   stale-answer-client-timeout, so a correct call would be:
   if (!QUERY_STALEONLY(client)) {
        isc_nmhandle_detach(&client->reqhandle);
   }

Other than these notes, comments were added in code in attempt to make
these updates easier to follow.

12 files changed:
bin/named/config.c
bin/named/server.c
lib/dns/include/dns/db.h
lib/dns/include/dns/events.h
lib/dns/include/dns/resolver.h
lib/dns/include/dns/view.h
lib/dns/rbtdb.c
lib/dns/resolver.c
lib/isccfg/namedconf.c
lib/ns/client.c
lib/ns/include/ns/query.h
lib/ns/query.c

index 77c0abaaa54f1fa7755b70fe3102fbd5cf99941a..2850ff996087deeae7aa6d85d7a9a89721b65722 100644 (file)
@@ -194,9 +194,10 @@ options {\n\
        servfail-ttl 1;\n\
 #      sortlist <none>\n\
        stale-answer-enable false;\n\
-       stale-refresh-time 30; /* 30 seconds */\n\
+       stale-answer-client-timeout 1800; /* in milliseconds */\n\
        stale-answer-ttl 30; /* 30 seconds */\n\
        stale-cache-enable false;\n\
+       stale-refresh-time 30; /* 30 seconds */\n\
        synth-from-dnssec no;\n\
 #      topology <none>\n\
        transfer-format many-answers;\n\
index 839438161c87bf0f9be1ff7cc40fa5e275fa9e04..6888333087c6417e499bd39e93185589b905eb32 100644 (file)
@@ -4485,6 +4485,11 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config,
                view->staleanswersok = dns_stale_answer_conf;
        }
 
+       obj = NULL;
+       result = named_config_get(maps, "stale-answer-client-timeout", &obj);
+       INSIST(result == ISC_R_SUCCESS);
+       view->staleanswerclienttimeout = cfg_obj_asuint32(obj);
+
        obj = NULL;
        result = named_config_get(maps, "stale-refresh-time", &obj);
        INSIST(result == ISC_R_SUCCESS);
index b79dcae0fa5f8b5d63518664004ea5716ab39fea..1e01c43dfda87f753a84bfcd93568ae842b969b6 100644 (file)
@@ -239,8 +239,35 @@ struct dns_dbonupdatelistener {
 #define DNS_DBFIND_FORCENSEC3  0x0080
 #define DNS_DBFIND_ADDITIONALOK 0x0100
 #define DNS_DBFIND_NOZONECUT   0x0200
-#define DNS_DBFIND_STALEOK     0x0400
+
+/*
+ * DNS_DBFIND_STALEOK: This flag is set when BIND fails to refresh a
+ * RRset due to timeout (resolver-query-timeout), its intent is to
+ * try to look for stale data in cache as a fallback, but only if
+ * stale answers are enabled in configuration.
+ *
+ * This flag is also used to activate stale-refresh-time window, since it
+ * is the only way the database knows that a resolution has failed.
+ */
+#define DNS_DBFIND_STALEOK 0x0400
+
+/*
+ * DNS_DBFIND_STALEENABLED: This flag is used as a hint to the database
+ * that it may use stale data. It is always set during query lookup if
+ * stale answers are enabled, but only effectively used during
+ * stale-refresh-time window. Also during this window, the resolver will
+ * not try to resolve the query, in other words no attempt to refresh the
+ * data in cache is made when the stale-refresh-time window is active.
+ */
 #define DNS_DBFIND_STALEENABLED 0x0800
+
+/*
+ * DNS_DBFIND_STALEONLY: This new introduced flag is used when we want
+ * stale data from the database, but not due to a failure in resolution,
+ * it also doesn't require stale-refresh-time window timer to be active.
+ * As long as there is a stale RRset available, it should be returned.
+ */
+#define DNS_DBFIND_STALEONLY 0x1000
 /*@}*/
 
 /*@{*/
index 0e066310d1ea91d6260390361ecf827a6d4bf9b3..50a8d01ac6da7abe485db0d47dc8301f5b7440a7 100644 (file)
@@ -78,6 +78,7 @@
 #define DNS_EVENT_CATZDELZONE       (ISC_EVENTCLASS_DNS + 56)
 #define DNS_EVENT_RPZUPDATED        (ISC_EVENTCLASS_DNS + 57)
 #define DNS_EVENT_STARTUPDATE       (ISC_EVENTCLASS_DNS + 58)
+#define DNS_EVENT_TRYSTALE          (ISC_EVENTCLASS_DNS + 59)
 
 #define DNS_EVENT_FIRSTEVENT (ISC_EVENTCLASS_DNS + 0)
 #define DNS_EVENT_LASTEVENT  (ISC_EVENTCLASS_DNS + 65535)
index b9b20e15774429f44cce6998b9ab2987b62002e0..07b94fdcd1a7d519fb8d43cc5570fd33e7c4e0ae 100644 (file)
@@ -129,9 +129,10 @@ typedef enum { dns_quotatype_zone = 0, dns_quotatype_server } dns_quotatype_t;
                    *   if possible. */
 
 /* Reserved in use by adb.c            0x00400000 */
-#define DNS_FETCHOPT_EDNSVERSIONSET   0x00800000
-#define DNS_FETCHOPT_EDNSVERSIONMASK  0xff000000
-#define DNS_FETCHOPT_EDNSVERSIONSHIFT 24
+#define DNS_FETCHOPT_EDNSVERSIONSET    0x00800000
+#define DNS_FETCHOPT_EDNSVERSIONMASK   0xff000000
+#define DNS_FETCHOPT_EDNSVERSIONSHIFT  24
+#define DNS_FETCHOPT_TRYSTALE_ONTIMEOUT 0x01000000
 
 /*
  * Upper bounds of class of query RTT (ms).  Corresponds to
index 1da372a89a7047627351dba162b61b554cd2b74a..c0a4eb5d53c654468cfb93241495b93c2c870eeb 100644 (file)
@@ -173,6 +173,7 @@ struct dns_view {
        dns_stale_answer_t    staleanswersok;     /* rndc setting */
        bool                  staleanswersenable; /* named.conf setting
                                                   * */
+       uint32_t          staleanswerclienttimeout;
        uint16_t          nocookieudp;
        uint16_t          padding;
        dns_acl_t *       pad_acl;
index b9a46cea403f07321b0c75d587e4174c1484d686..24442bd3ef6a1df827fd3e2b8bf6a1edcfa424d6 100644 (file)
@@ -4562,6 +4562,13 @@ check_stale_header(dns_rbtnode_t *node, rdatasetheader_t *header,
                         */
                        if ((search->options & DNS_DBFIND_STALEOK) != 0) {
                                header->last_refresh_fail_ts = search->now;
+                       } else if ((search->options & DNS_DBFIND_STALEONLY) !=
+                                  0) {
+                               /*
+                                * We want stale RRset only, so we don't skip
+                                * it.
+                                */
+                               return (false);
                        } else if ((search->options &
                                    DNS_DBFIND_STALEENABLED) != 0 &&
                                   search->now <
index b0a0ec7820580ebebf5abe6c32367441a3efdf0c..066db581ea329715282f3470c814d4dc3f097139 100644 (file)
@@ -307,7 +307,9 @@ struct fetchctx {
        dns_rdataset_t nameservers;
        atomic_uint_fast32_t attributes;
        isc_timer_t *timer;
+       isc_timer_t *timer_try_stale;
        isc_time_t expires;
+       isc_time_t expires_try_stale;
        isc_interval_t interval;
        dns_message_t *qmessage;
        ISC_LIST(resquery_t) queries;
@@ -1135,6 +1137,16 @@ fctx_starttimer(fetchctx_t *fctx) {
                                NULL, true));
 }
 
+static inline isc_result_t
+fctx_starttimer_trystale(fetchctx_t *fctx) {
+       /*
+        * Start the stale-answer-client-timeout timer for fctx.
+        */
+
+       return (isc_timer_reset(fctx->timer_try_stale, isc_timertype_once,
+                               &fctx->expires_try_stale, NULL, true));
+}
+
 static inline void
 fctx_stoptimer(fetchctx_t *fctx) {
        isc_result_t result;
@@ -1153,6 +1165,22 @@ fctx_stoptimer(fetchctx_t *fctx) {
        }
 }
 
+static inline void
+fctx_stoptimer_trystale(fetchctx_t *fctx) {
+       isc_result_t result;
+
+       if (fctx->timer_try_stale != NULL) {
+               result = isc_timer_reset(fctx->timer_try_stale,
+                                        isc_timertype_inactive, NULL, NULL,
+                                        true);
+               if (result != ISC_R_SUCCESS) {
+                       UNEXPECTED_ERROR(__FILE__, __LINE__,
+                                        "isc_timer_reset(): %s",
+                                        isc_result_totext(result));
+               }
+       }
+}
+
 static inline isc_result_t
 fctx_startidletimer(fetchctx_t *fctx, isc_interval_t *interval) {
        /*
@@ -1537,6 +1565,7 @@ fctx_stopqueries(fetchctx_t *fctx, bool no_response, bool age_untried) {
        FCTXTRACE("stopqueries");
        fctx_cancelqueries(fctx, no_response, age_untried);
        fctx_stoptimer(fctx);
+       fctx_stoptimer_trystale(fctx);
 }
 
 static inline void
@@ -1697,6 +1726,16 @@ fctx_sendevents(fetchctx_t *fctx, isc_result_t result, int line) {
             event = next_event) {
                next_event = ISC_LIST_NEXT(event, ev_link);
                ISC_LIST_UNLINK(fctx->events, event, ev_link);
+               if (event->ev_type == DNS_EVENT_TRYSTALE) {
+                       /*
+                        * Not applicable to TRY STALE events, this function is
+                        * called when the fetch has either completed or timed
+                        * out due to resolver-query-timeout being reached.
+                        */
+                       isc_task_detach((isc_task_t **)&event->ev_sender);
+                       isc_event_free((isc_event_t **)&event);
+                       continue;
+               }
                task = event->ev_sender;
                event->ev_sender = fctx;
                event->vresult = fctx->vresult;
@@ -4182,7 +4221,13 @@ fctx_try(fetchctx_t *fctx, bool retrying, bool badcache) {
         */
        if (fctx->minimized && !fctx->forwarding) {
                unsigned int options = fctx->options;
-               options &= ~DNS_FETCHOPT_QMINIMIZE;
+               /*
+                * Also clear DNS_FETCHOPT_TRYSTALE_ONTIMEOUT here, otherwise
+                * every query minimization step will activate the try-stale
+                * timer again.
+                */
+               options &= ~(DNS_FETCHOPT_QMINIMIZE |
+                            DNS_FETCHOPT_TRYSTALE_ONTIMEOUT);
 
                /*
                 * Is another QNAME minimization fetch still running?
@@ -4222,6 +4267,7 @@ fctx_try(fetchctx_t *fctx, bool retrying, bool badcache) {
                fctx_increference(fctx);
                task = res->buckets[bucketnum].task;
                fctx_stoptimer(fctx);
+               fctx_stoptimer_trystale(fctx);
                result = dns_resolver_createfetch(
                        fctx->res, &fctx->qminname, fctx->qmintype,
                        &fctx->domain, &fctx->nameservers, NULL, NULL, 0,
@@ -4247,6 +4293,7 @@ fctx_try(fetchctx_t *fctx, bool retrying, bool badcache) {
        }
 
        fctx_increference(fctx);
+
        result = fctx_query(fctx, addrinfo, fctx->options);
        if (result != ISC_R_SUCCESS) {
                fctx_done(fctx, result, __LINE__);
@@ -4504,6 +4551,9 @@ fctx_destroy(fetchctx_t *fctx) {
        isc_counter_detach(&fctx->qc);
        fcount_decr(fctx);
        isc_timer_detach(&fctx->timer);
+       if (fctx->timer_try_stale != NULL) {
+               isc_timer_detach(&fctx->timer_try_stale);
+       }
        dns_message_detach(&fctx->qmessage);
        if (dns_name_countlabels(&fctx->domain) > 0) {
                dns_name_free(&fctx->domain, fctx->mctx);
@@ -4579,6 +4629,57 @@ fctx_timeout(isc_task_t *task, isc_event_t *event) {
        isc_event_free(&event);
 }
 
+/*
+ * Fetch event handlers called if stale answers are enabled
+ * (stale-answer-enabled) and the fetch took more than
+ * stale-answer-client-timeout to complete.
+ */
+static void
+fctx_timeout_try_stale(isc_task_t *task, isc_event_t *event) {
+       fetchctx_t *fctx = event->ev_arg;
+       dns_fetchevent_t *dns_event, *next_event;
+       isc_task_t *sender_task;
+       unsigned int count = 0;
+
+       REQUIRE(VALID_FCTX(fctx));
+
+       UNUSED(task);
+
+       FCTXTRACE("timeout_try_stale");
+
+       if (event->ev_type != ISC_TIMEREVENT_LIFE) {
+               return;
+       }
+
+       LOCK(&fctx->res->buckets[fctx->bucketnum].lock);
+
+       /*
+        * Trigger events of type DNS_EVENT_TRYSTALE.
+        */
+       for (dns_event = ISC_LIST_HEAD(fctx->events); dns_event != NULL;
+            dns_event = next_event)
+       {
+               next_event = ISC_LIST_NEXT(dns_event, ev_link);
+
+               if (dns_event->ev_type != DNS_EVENT_TRYSTALE) {
+                       continue;
+               }
+
+               ISC_LIST_UNLINK(fctx->events, dns_event, ev_link);
+               sender_task = dns_event->ev_sender;
+               dns_event->ev_sender = fctx;
+               dns_event->vresult = ISC_R_TIMEDOUT;
+               dns_event->result = ISC_R_TIMEDOUT;
+
+               isc_task_sendanddetach(&sender_task, ISC_EVENT_PTR(&dns_event));
+               count++;
+       }
+
+       UNLOCK(&fctx->res->buckets[fctx->bucketnum].lock);
+
+       isc_event_free(&event);
+}
+
 static void
 fctx_shutdown(fetchctx_t *fctx) {
        isc_event_t *cevent;
@@ -4760,6 +4861,9 @@ fctx_start(isc_task_t *task, isc_event_t *event) {
                 * All is well.  Start working on the fetch.
                 */
                result = fctx_starttimer(fctx);
+               if (result == ISC_R_SUCCESS && fctx->timer_try_stale != NULL) {
+                       result = fctx_starttimer_trystale(fctx);
+               }
                if (result != ISC_R_SUCCESS) {
                        fctx_done(fctx, result, __LINE__);
                } else {
@@ -4826,6 +4930,34 @@ fctx_join(fetchctx_t *fctx, isc_task_t *task, const isc_sockaddr_t *client,
        return (ISC_R_SUCCESS);
 }
 
+static inline void
+fctx_add_event(fetchctx_t *fctx, isc_task_t *task, const isc_sockaddr_t *client,
+              dns_messageid_t id, isc_taskaction_t action, void *arg,
+              dns_fetch_t *fetch, isc_eventtype_t event_type) {
+       isc_task_t *tclone;
+       dns_fetchevent_t *event;
+       /*
+        * We store the task we're going to send this event to in the
+        * sender field.  We'll make the fetch the sender when we actually
+        * send the event.
+        */
+       tclone = NULL;
+       isc_task_attach(task, &tclone);
+       event = (dns_fetchevent_t *)isc_event_allocate(fctx->res->mctx, tclone,
+                                                      event_type, action, arg,
+                                                      sizeof(*event));
+       event->result = DNS_R_SERVFAIL;
+       event->qtype = fctx->type;
+       event->db = NULL;
+       event->node = NULL;
+       event->rdataset = NULL;
+       event->sigrdataset = NULL;
+       event->fetch = fetch;
+       event->client = client;
+       event->id = id;
+       ISC_LIST_APPEND(fctx->events, event, ev_link);
+}
+
 static inline void
 log_ns_ttl(fetchctx_t *fctx, const char *where) {
        char namebuf[DNS_NAME_FORMATSIZE];
@@ -4854,6 +4986,7 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type,
        char typebuf[DNS_RDATATYPE_FORMATSIZE];
        isc_mem_t *mctx;
        size_t p;
+       bool try_stale;
 
        /*
         * Caller must be holding the lock for bucket number 'bucketnum'.
@@ -5078,6 +5211,29 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type,
                goto cleanup_qmessage;
        }
 
+       try_stale = ((options & DNS_FETCHOPT_TRYSTALE_ONTIMEOUT) != 0);
+       if (try_stale) {
+               INSIST(res->view->staleanswerclienttimeout <=
+                      (res->query_timeout - 1000));
+               /*
+                * Compute an expiration time after which stale data will
+                * attempted to be served, if stale answers are enabled and
+                * target RRset is available in cache.
+                */
+               isc_interval_set(
+                       &interval, res->view->staleanswerclienttimeout / 1000,
+                       res->view->staleanswerclienttimeout % 1000 * 1000000);
+               iresult = isc_time_nowplusinterval(&fctx->expires_try_stale,
+                                                  &interval);
+               if (iresult != ISC_R_SUCCESS) {
+                       UNEXPECTED_ERROR(__FILE__, __LINE__,
+                                        "isc_time_nowplusinterval: %s",
+                                        isc_result_totext(iresult));
+                       result = ISC_R_UNEXPECTED;
+                       goto cleanup_qmessage;
+               }
+       }
+
        /*
         * Default retry interval initialization.  We set the interval now
         * mostly so it won't be uninitialized.  It will be set to the
@@ -5086,10 +5242,11 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type,
        isc_interval_set(&fctx->interval, 2, 0);
 
        /*
-        * Create an inactive timer.  It will be made active when the fetch
-        * is actually started.
+        * Create an inactive timer for resolver-query-timeout. It
+        * will be made active when the fetch is actually started.
         */
        fctx->timer = NULL;
+
        iresult = isc_timer_create(res->timermgr, isc_timertype_inactive, NULL,
                                   NULL, res->buckets[bucketnum].task,
                                   fctx_timeout, fctx, &fctx->timer);
@@ -5100,6 +5257,26 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type,
                goto cleanup_qmessage;
        }
 
+       /*
+        * If stale answers are enabled, then create an inactive timer
+        * for stale-answer-client-timeout. It will be made active when
+        * the fetch is actually started.
+        */
+       fctx->timer_try_stale = NULL;
+       if (try_stale) {
+               iresult = isc_timer_create(
+                       res->timermgr, isc_timertype_inactive, NULL, NULL,
+                       res->buckets[bucketnum].task, fctx_timeout_try_stale,
+                       fctx, &fctx->timer_try_stale);
+               if (iresult != ISC_R_SUCCESS) {
+                       UNEXPECTED_ERROR(__FILE__, __LINE__,
+                                        "isc_timer_create: %s",
+                                        isc_result_totext(iresult));
+                       result = ISC_R_UNEXPECTED;
+                       goto cleanup_qmessage;
+               }
+       }
+
        /*
         * Attach to the view's cache and adb.
         */
@@ -5144,6 +5321,7 @@ cleanup_mctx:
        dns_adb_detach(&fctx->adb);
        dns_db_detach(&fctx->cache);
        isc_timer_detach(&fctx->timer);
+       isc_timer_detach(&fctx->timer_try_stale);
 
 cleanup_qmessage:
        dns_message_detach(&fctx->qmessage);
@@ -5357,6 +5535,23 @@ clone_results(fetchctx_t *fctx) {
        for (event = ISC_LIST_NEXT(hevent, ev_link); event != NULL;
             event = ISC_LIST_NEXT(event, ev_link))
        {
+               if (event->ev_type == DNS_EVENT_TRYSTALE) {
+                       /*
+                        * We don't need to clone resulting data to this
+                        * type of event, as its associated callback is only
+                        * called when stale-answer-client-timeout triggers,
+                        * and the logic in there doesn't expect any result
+                        * as input, as it will itself lookup for stale data
+                        * in cache to use as result, if any is available.
+                        *
+                        * Also, if we reached this point, then the whole fetch
+                        * context is done, it will cancel timers, process
+                        * associated callbacks of type DNS_EVENT_FETCHDONE, and
+                        * silently remove/free events of type
+                        * DNS_EVENT_TRYSTALE.
+                        */
+                       continue;
+               }
                name = dns_fixedname_name(&event->foundname);
                dns_name_copynf(hname, name);
                event->result = hevent->result;
@@ -6126,6 +6321,7 @@ cache_name(fetchctx_t *fctx, dns_name_t *name, dns_message_t *message,
            (!need_validation)) {
                have_answer = true;
                event = ISC_LIST_HEAD(fctx->events);
+
                if (event != NULL) {
                        adbp = &event->db;
                        aname = dns_fixedname_name(&event->foundname);
@@ -10815,6 +11011,14 @@ dns_resolver_createfetch(dns_resolver_t *res, const dns_name_t *name,
 
        result = fctx_join(fctx, task, client, id, action, arg, rdataset,
                           sigrdataset, fetch);
+
+       if (result == ISC_R_SUCCESS &&
+           ((options & DNS_FETCHOPT_TRYSTALE_ONTIMEOUT) != 0))
+       {
+               fctx_add_event(fctx, task, client, id, action, arg, fetch,
+                              DNS_EVENT_TRYSTALE);
+       }
+
        if (new_fctx) {
                if (result == ISC_R_SUCCESS) {
                        /*
index b7ed28d24e8bf65ee677acefcdd498cb588a935b..6b7c6fbabefaf4ea240751f0d84545d65029c8fa 100644 (file)
@@ -2027,6 +2027,7 @@ static cfg_clausedef_t view_clauses[] = {
        { "servfail-ttl", &cfg_type_duration, 0 },
        { "sortlist", &cfg_type_bracketed_aml, 0 },
        { "stale-answer-enable", &cfg_type_boolean, 0 },
+       { "stale-answer-client-timeout", &cfg_type_uint32, 0 },
        { "stale-answer-ttl", &cfg_type_duration, 0 },
        { "stale-cache-enable", &cfg_type_boolean, 0 },
        { "stale-refresh-time", &cfg_type_duration, 0 },
index 0889688d49a6fbde3246d10f07bd1e8efe29d73c..7cc96efc99c5b491a0452f6aba533b63f886aa82 100644 (file)
@@ -879,7 +879,9 @@ ns_client_error(ns_client_t *client, isc_result_t result) {
                }
        }
 
-       ns_client_send(client);
+       if ((client->query.attributes & NS_QUERYATTR_ANSWERED) == 0) {
+               ns_client_send(client);
+       }
 }
 
 isc_result_t
index 011a8b4c131a6d7c1d3568f2964bc62164870e8f..55060a3a8c354e4883cd1442356160952f5ec34e 100644 (file)
@@ -117,6 +117,7 @@ struct ns_query {
 #define NS_QUERYATTR_DNS64EXCLUDE    0x08000
 #define NS_QUERYATTR_RRL_CHECKED     0x10000
 #define NS_QUERYATTR_REDIRECT       0x20000
+#define NS_QUERYATTR_ANSWERED       0x40000
 
 typedef struct query_ctx query_ctx_t;
 
index a8712f3a993869d8acf591d8db3bef1a921ea23a..9dcbc7ab7ca4a8570e04d603d285c3d856aa60f3 100644 (file)
 
 #define REDIRECT(c) (((c)->query.attributes & NS_QUERYATTR_REDIRECT) != 0)
 
+/*% Was the query already answered due to stale-answer-client-timeout? */
+#define QUERY_ANSWERED(c) (((c)->query.attributes & NS_QUERYATTR_ANSWERED) != 0)
+
+/*% Does the query only wants to check for stale RRset? */
+#define QUERY_STALEONLY(c) (((c)->query.dboptions & DNS_DBFIND_STALEONLY) != 0)
+
 /*% Does the rdataset 'r' have an attached 'No QNAME Proof'? */
 #define NOQNAME(r) (((r)->attributes & DNS_RDATASETATTR_NOQNAME) != 0)
 
@@ -557,8 +563,13 @@ query_send(ns_client_t *client) {
        }
 
        inc_stats(client, counter);
-       ns_client_send(client);
-       isc_nmhandle_detach(&client->reqhandle);
+       if (!QUERY_ANSWERED(client)) {
+               ns_client_send(client);
+       }
+
+       if (!QUERY_STALEONLY(client)) {
+               isc_nmhandle_detach(&client->reqhandle);
+       }
 }
 
 static void
@@ -585,7 +596,10 @@ query_error(ns_client_t *client, isc_result_t result, int line) {
        log_queryerror(client, result, line, loglevel);
 
        ns_client_error(client, result);
-       isc_nmhandle_detach(&client->reqhandle);
+
+       if (!QUERY_STALEONLY(client)) {
+               isc_nmhandle_detach(&client->reqhandle);
+       }
 }
 
 static void
@@ -598,7 +612,10 @@ query_next(ns_client_t *client, isc_result_t result) {
                inc_stats(client, ns_statscounter_failure);
        }
        ns_client_drop(client, result);
-       isc_nmhandle_detach(&client->reqhandle);
+
+       if (!QUERY_STALEONLY(client)) {
+               isc_nmhandle_detach(&client->reqhandle);
+       }
 }
 
 static inline void
@@ -5158,7 +5175,7 @@ qctx_freedata(query_ctx_t *qctx) {
                dns_db_detach(&qctx->zdb);
        }
 
-       if (qctx->event != NULL) {
+       if (qctx->event != NULL && !QUERY_STALEONLY(qctx->client)) {
                free_devent(qctx->client, ISC_EVENT_PTR(&qctx->event),
                            &qctx->event);
        }
@@ -5583,6 +5600,7 @@ query_lookup(query_ctx_t *qctx) {
        unsigned int dboptions;
        dns_ttl_t stale_refresh = 0;
        bool dbfind_stale = false;
+       bool stale_ok;
 
        CCTRACE(ISC_LOG_DEBUG(3), "query_lookup");
 
@@ -5681,10 +5699,10 @@ query_lookup(query_ctx_t *qctx) {
         * answer, otherwise "fresh" answers are also treated as stale.
         */
        dbfind_stale = ((dboptions & DNS_DBFIND_STALEOK) != 0);
-       if (dbfind_stale != 0 ||
-           (((dboptions & DNS_DBFIND_STALEENABLED) != 0) &&
-            STALE(qctx->rdataset)))
-       {
+       stale_ok = ((dboptions &
+                    (DNS_DBFIND_STALEENABLED | DNS_DBFIND_STALEONLY)) != 0);
+
+       if (dbfind_stale != 0 || (stale_ok && STALE(qctx->rdataset))) {
                char namebuf[DNS_NAME_FORMATSIZE];
                bool success;
 
@@ -5709,6 +5727,12 @@ query_lookup(query_ctx_t *qctx) {
                                      "%s resolver failure, stale answer %s",
                                      namebuf,
                                      success ? "used" : "unavailable");
+               } else if ((dboptions & DNS_DBFIND_STALEONLY) != 0) {
+                       isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE,
+                                     NS_LOGMODULE_QUERY, ISC_LOG_INFO,
+                                     "%s client timeout, stale answer %s",
+                                     namebuf,
+                                     success ? "used" : "unavailable");
                } else {
                        isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE,
                                      NS_LOGMODULE_QUERY, ISC_LOG_INFO,
@@ -5719,21 +5743,75 @@ query_lookup(query_ctx_t *qctx) {
                }
 
                if (!success) {
-                       QUERY_ERROR(qctx, DNS_R_SERVFAIL);
-                       return (ns_query_done(qctx));
+                       /*
+                        * If DNS_DBFIND_STALEONLY is set then it means
+                        * stale-answer-client-timeout was triggered, in
+                        * that case we only want to check if a stale RRset is
+                        * available, if that's the case we promptly answer the
+                        * client with the stale data found. If a stale RRset is
+                        * not available then we must wait for the original
+                        * query to be resumed in order to build a proper
+                        * answer.
+                        */
+                       if ((dboptions & DNS_DBFIND_STALEONLY) == 0) {
+                               QUERY_ERROR(qctx, DNS_R_SERVFAIL);
+                               return (ns_query_done(qctx));
+                       }
+
+                       return (result);
                }
        }
-       return (query_gotanswer(qctx, result));
+
+       /*
+        * If DNS_DBFIND_STALEONLY is disabled then we proceed as normal,
+        * otherwise we only proceed with query_gotanswer if we
+        * successfully found a stale RRset in cache.
+        */
+       if (((dboptions & DNS_DBFIND_STALEONLY) == 0) ||
+           result == ISC_R_SUCCESS || result == DNS_R_GLUE ||
+           result == DNS_R_ZONECUT)
+       {
+               return (query_gotanswer(qctx, result));
+       }
 
 cleanup:
        return (result);
 }
 
 /*
- * Event handler to resume processing a query after recursion.
- * If the query has timed out or been canceled or the system
- * is shutting down, clean up and exit; otherwise, call
- * query_resume() to continue the ongoing work.
+ * Create a new query context with the sole intent
+ * of looking up for a stale RRset in cache.
+ * If an entry is found, we mark the original query as
+ * answered, in order to avoid answering the query twice,
+ * when the original fetch finishes.
+ */
+static inline void
+query_lookup_staleonly(ns_client_t *client, dns_fetchevent_t *devent) {
+       query_ctx_t qctx;
+       isc_result_t result;
+
+       qctx_init(client, &devent, client->query.qtype, &qctx);
+       dns_db_attach(client->view->cachedb, &qctx.db);
+       client->query.dboptions |= DNS_DBFIND_STALEONLY;
+       result = query_lookup(&qctx);
+       if (result == ISC_R_SUCCESS) {
+               client->query.attributes |= NS_QUERYATTR_ANSWERED;
+       }
+       if (qctx.node != NULL) {
+               dns_db_detachnode(qctx.db, &qctx.node);
+       }
+       qctx_freedata(&qctx);
+       client->query.dboptions &= ~DNS_DBFIND_STALEONLY;
+       qctx_destroy(&qctx);
+       isc_event_free(ISC_EVENT_PTR(&qctx.event));
+}
+
+/*
+ * Event handler to resume processing a query after recursion, or when a
+ * client timeout is triggered. If the query has timed out or been cancelled
+ * or the system is shutting down, clean up and exit. If a client timeout is
+ * triggered, see if we can respond with a stale answer from cache. Otherwise,
+ * call query_resume() to continue the ongoing work.
  */
 static void
 fetch_callback(isc_task_t *task, isc_event_t *event) {
@@ -5748,7 +5826,8 @@ fetch_callback(isc_task_t *task, isc_event_t *event) {
 
        UNUSED(task);
 
-       REQUIRE(event->ev_type == DNS_EVENT_FETCHDONE);
+       REQUIRE(event->ev_type == DNS_EVENT_FETCHDONE ||
+               event->ev_type == DNS_EVENT_TRYSTALE);
        client = devent->ev_arg;
        REQUIRE(NS_CLIENT_VALID(client));
        REQUIRE(task == client->task);
@@ -5756,6 +5835,11 @@ fetch_callback(isc_task_t *task, isc_event_t *event) {
 
        CTRACE(ISC_LOG_DEBUG(3), "fetch_callback");
 
+       if (event->ev_type == DNS_EVENT_TRYSTALE) {
+               query_lookup_staleonly(client, devent);
+               return;
+       }
+
        LOCK(&client->query.fetchlock);
        if (client->query.fetch != NULL) {
                /*
@@ -6076,6 +6160,10 @@ ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname,
                peeraddr = &client->peeraddr;
        }
 
+       if (dns_view_staleanswerenabled(client->view)) {
+               client->query.fetchoptions |= DNS_FETCHOPT_TRYSTALE_ONTIMEOUT;
+       }
+
        isc_nmhandle_attach(client->handle, &client->fetchhandle);
        result = dns_resolver_createfetch(
                client->view->resolver, qname, qtype, qdomain, nameservers,
@@ -7678,7 +7766,9 @@ query_addanswer(query_ctx_t *qctx) {
                query_filter64(qctx);
                ns_client_putrdataset(qctx->client, &qctx->rdataset);
        } else {
-               if (!qctx->is_zone && RECURSIONOK(qctx->client)) {
+               if (!qctx->is_zone && RECURSIONOK(qctx->client) &&
+                   !QUERY_STALEONLY(qctx->client))
+               {
                        query_prefetch(qctx->client, qctx->fname,
                                       qctx->rdataset);
                }
@@ -7786,7 +7876,7 @@ query_respond(query_ctx_t *qctx) {
         * We shouldn't ever fail to add 'rdataset'
         * because it's already in the answer.
         */
-       INSIST(qctx->rdataset == NULL);
+       INSIST(qctx->rdataset == NULL || QUERY_ANSWERED(qctx->client));
 
        query_addauth(qctx);
 
@@ -11271,7 +11361,7 @@ ns_query_done(query_ctx_t *qctx) {
         * If we're recursing then just return; the query will
         * resume when recursion ends.
         */
-       if (RECURSING(qctx->client)) {
+       if (RECURSING(qctx->client) && !QUERY_STALEONLY(qctx->client)) {
                return (qctx->result);
        }
 
@@ -11282,8 +11372,10 @@ ns_query_done(query_ctx_t *qctx) {
         * to the AA bit if the auth-nxdomain config option
         * says so, then render and send the response.
         */
-       query_setup_sortlist(qctx);
-       query_glueanswer(qctx);
+       if (!QUERY_ANSWERED(qctx->client)) {
+               query_setup_sortlist(qctx);
+               query_glueanswer(qctx);
+       }
 
        if (qctx->client->message->rcode == dns_rcode_nxdomain &&
            qctx->view->auth_nxdomain)