From d3a76ba37e9ce13e7ba9a8cc3da39446748824d9 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Thu, 26 Mar 2026 13:17:06 +0100 Subject: [PATCH 01/11] Garbage collect unused SyncRes flag Signed-off-by: Otto Moerbeek Signed-off-by: Otto Moerbeek --- pdns/recursordist/syncres.cc | 3 +-- pdns/recursordist/syncres.hh | 6 ------ pdns/recursordist/test-syncres_cc.cc | 1 - 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/pdns/recursordist/syncres.cc b/pdns/recursordist/syncres.cc index b9fa905f12b6..26ac43d384a8 100644 --- a/pdns/recursordist/syncres.cc +++ b/pdns/recursordist/syncres.cc @@ -462,7 +462,7 @@ static inline void accountAuthLatency(uint64_t usec, int family) } SyncRes::SyncRes(const struct timeval& now) : - d_authzonequeries(0), d_outqueries(0), d_tcpoutqueries(0), d_dotoutqueries(0), d_throttledqueries(0), d_timeouts(0), d_unreachables(0), d_bytesReceived(0), d_totUsec(0), d_fixednow(now), d_now(now), d_cacheonly(false), d_doDNSSEC(false), d_doEDNS0(false), d_qNameMinimization(s_qnameminimization), d_lm(s_lm) + d_authzonequeries(0), d_outqueries(0), d_tcpoutqueries(0), d_dotoutqueries(0), d_throttledqueries(0), d_timeouts(0), d_unreachables(0), d_bytesReceived(0), d_totUsec(0), d_fixednow(now), d_now(now), d_cacheonly(false), d_doDNSSEC(false), d_qNameMinimization(s_qnameminimization), d_lm(s_lm) { d_validationContext.d_nsec3IterationsRemainingQuota = s_maxnsec3iterationsperq > 0 ? s_maxnsec3iterationsperq : std::numeric_limits::max(); } @@ -6296,7 +6296,6 @@ int SyncRes::getRootNS(struct timeval now, asyncresolve_t asyncCallback, unsigne } SyncRes resolver(now); resolver.d_prefix = "[getRootNS]"; - resolver.setDoEDNS0(true); resolver.setUpdatingRootNS(); resolver.setDoDNSSEC(g_dnssecmode != DNSSECMode::Off); resolver.setDNSSECValidationRequested(g_dnssecmode != DNSSECMode::Off && g_dnssecmode != DNSSECMode::ProcessNoValidate); diff --git a/pdns/recursordist/syncres.hh b/pdns/recursordist/syncres.hh index 2a349205e4e8..280cca85ce05 100644 --- a/pdns/recursordist/syncres.hh +++ b/pdns/recursordist/syncres.hh @@ -383,11 +383,6 @@ public: return d_qNameMinimizationFallbackMode; } - void setDoEDNS0(bool state = true) - { - d_doEDNS0 = state; - } - void setDoDNSSEC(bool state = true) { d_doDNSSEC = state; @@ -757,7 +752,6 @@ private: bool d_cacheonly; bool d_doDNSSEC; bool d_DNSSECValidationRequested{false}; - bool d_doEDNS0{true}; bool d_requireAuthData{true}; bool d_updatingRootNS{false}; bool d_wantsRPZ{true}; diff --git a/pdns/recursordist/test-syncres_cc.cc b/pdns/recursordist/test-syncres_cc.cc index 9eaa2e799efd..46bcc348d0af 100644 --- a/pdns/recursordist/test-syncres_cc.cc +++ b/pdns/recursordist/test-syncres_cc.cc @@ -240,7 +240,6 @@ void initSR(std::unique_ptr& sr, bool dnssec, bool debug, time_t fakeNo initSR(debug); sr = std::make_unique(now); - sr->setDoEDNS0(true); if (dnssec) { sr->setDoDNSSEC(dnssec); } From 6504eab59fc42e9d1ff418c32dce08cbfdd3c4f1 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Thu, 26 Mar 2026 13:37:24 +0100 Subject: [PATCH 02/11] Refactor SyncRes constructor Signed-off-by: Otto Moerbeek --- pdns/recursordist/rec-taskqueue.cc | 4 ++-- pdns/recursordist/syncres.cc | 9 +++++--- pdns/recursordist/syncres.hh | 37 ++++++++++++++++++------------ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/pdns/recursordist/rec-taskqueue.cc b/pdns/recursordist/rec-taskqueue.cc index a520e827c735..eac312eed205 100644 --- a/pdns/recursordist/rec-taskqueue.cc +++ b/pdns/recursordist/rec-taskqueue.cc @@ -120,13 +120,14 @@ static void resolveInternal(const struct timeval& now, bool logErrors, const pdn auto log = g_slog->withName("taskq")->withValues("name", Logging::Loggable(task.d_qname), "qtype", Logging::Loggable(QType(task.d_qtype).toString()), "netmask", Logging::Loggable(task.d_netmask.empty() ? "" : task.d_netmask.toString())); const string msg = "Exception while running a background ResolveTask"; SyncRes resolver(now); - vector ret; resolver.setRefreshAlmostExpired(task.d_refreshMode); resolver.setQuerySource(task.d_netmask); if (forceNoQM) { resolver.setQNameMinimization(false); } + bool exceptionOccurred = true; + vector ret; try { log->info(Logr::Debug, "resolving", "refresh", Logging::Loggable(task.d_refreshMode)); int res = resolver.beginResolve(task.d_qname, QType(task.d_qtype), QClass::IN, ret); @@ -186,7 +187,6 @@ static void tryDoT(const struct timeval& now, bool logErrors, const pdns::Resolv const string msg = "Exception while running a background tryDoT task"; SyncRes resolver(now); vector ret; - resolver.setRefreshAlmostExpired(false); bool exceptionOccurred = true; try { log->info(Logr::Debug, "trying DoT"); diff --git a/pdns/recursordist/syncres.cc b/pdns/recursordist/syncres.cc index 26ac43d384a8..8f7e1ed08959 100644 --- a/pdns/recursordist/syncres.cc +++ b/pdns/recursordist/syncres.cc @@ -462,7 +462,12 @@ static inline void accountAuthLatency(uint64_t usec, int family) } SyncRes::SyncRes(const struct timeval& now) : - d_authzonequeries(0), d_outqueries(0), d_tcpoutqueries(0), d_dotoutqueries(0), d_throttledqueries(0), d_timeouts(0), d_unreachables(0), d_bytesReceived(0), d_totUsec(0), d_fixednow(now), d_now(now), d_cacheonly(false), d_doDNSSEC(false), d_qNameMinimization(s_qnameminimization), d_lm(s_lm) + d_fixednow(now), + d_now(now), + d_doDNSSEC(g_dnssecmode != DNSSECMode::Off), + d_DNSSECValidationRequested(g_dnssecmode != DNSSECMode::Off && g_dnssecmode != DNSSECMode::ProcessNoValidate), + d_qNameMinimization(s_qnameminimization), + d_lm(s_lm) { d_validationContext.d_nsec3IterationsRemainingQuota = s_maxnsec3iterationsperq > 0 ? s_maxnsec3iterationsperq : std::numeric_limits::max(); } @@ -6297,8 +6302,6 @@ int SyncRes::getRootNS(struct timeval now, asyncresolve_t asyncCallback, unsigne SyncRes resolver(now); resolver.d_prefix = "[getRootNS]"; resolver.setUpdatingRootNS(); - resolver.setDoDNSSEC(g_dnssecmode != DNSSECMode::Off); - resolver.setDNSSECValidationRequested(g_dnssecmode != DNSSECMode::Off && g_dnssecmode != DNSSECMode::ProcessNoValidate); resolver.setAsyncCallback(std::move(asyncCallback)); resolver.setRefreshAlmostExpired(true); diff --git a/pdns/recursordist/syncres.hh b/pdns/recursordist/syncres.hh index 280cca85ce05..c66110fb0159 100644 --- a/pdns/recursordist/syncres.hh +++ b/pdns/recursordist/syncres.hh @@ -78,10 +78,17 @@ using NsSet = std::unordered_map, bool>>; extern std::unique_ptr g_negCache; -class SyncRes : public boost::noncopyable +class SyncRes { public: - enum LogMode + SyncRes(); + ~SyncRes() = default; + SyncRes(const SyncRes&) = delete; + SyncRes(SyncRes&&) = delete; + SyncRes& operator=(const SyncRes&) = delete; + SyncRes& operator=(SyncRes&&) = delete; + + enum LogMode : uint8_t { LogNone, Log, @@ -593,18 +600,18 @@ public: std::shared_ptr d_slog = g_slog->withName("syncres"); std::optional d_extendedError; - unsigned int d_authzonequeries; - unsigned int d_outqueries; - unsigned int d_tcpoutqueries; - unsigned int d_dotoutqueries; - unsigned int d_throttledqueries; - unsigned int d_timeouts; - unsigned int d_unreachables; - unsigned int d_bytesReceived; - unsigned int d_totUsec; + unsigned int d_authzonequeries{0}; + unsigned int d_outqueries{0}; + unsigned int d_tcpoutqueries{0}; + unsigned int d_dotoutqueries{0}; + unsigned int d_throttledqueries{0}; + unsigned int d_timeouts{0}; + unsigned int d_unreachables{0}; + unsigned int d_bytesReceived{0}; + unsigned int d_totUsec{0}; unsigned int d_maxdepth{0}; // Initialized only once, as opposed to d_now which gets updated after outgoing requests - struct timeval d_fixednow; + const struct timeval d_fixednow; private: ComboAddress d_requestor; @@ -749,15 +756,15 @@ private: /* When d_cacheonly is set to true, we will only check the cache. * This is set when the RD bit is unset in the incoming query */ - bool d_cacheonly; + bool d_cacheonly{false}; bool d_doDNSSEC; - bool d_DNSSECValidationRequested{false}; + bool d_DNSSECValidationRequested; bool d_requireAuthData{true}; bool d_updatingRootNS{false}; bool d_wantsRPZ{true}; bool d_wasOutOfBand{false}; bool d_wasVariable{false}; - bool d_qNameMinimization{false}; + bool d_qNameMinimization; bool d_qNameMinimizationFallbackMode{false}; bool d_queryReceivedOverTCP{false}; bool d_followCNAME{true}; From 37318bf92975de8834aa1ca45127babd383a3c12 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Thu, 26 Mar 2026 13:56:27 +0100 Subject: [PATCH 03/11] Introduce a "forcedRefresh" flag, which regards the entry expired if half the TTL has passed Signed-off-by: Otto Moerbeek --- pdns/recursordist/recursor_cache.cc | 17 ++++++++--------- pdns/recursordist/recursor_cache.hh | 12 +++++++++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pdns/recursordist/recursor_cache.cc b/pdns/recursordist/recursor_cache.cc index b521b26c4e3b..9766e0b74917 100644 --- a/pdns/recursordist/recursor_cache.cc +++ b/pdns/recursordist/recursor_cache.cc @@ -408,21 +408,21 @@ bool MemRecursorCache::entryMatches(MemRecursorCache::OrderedTagIterator_t& entr } // Fake a cache miss if more than refreshTTLPerc of the original TTL has passed -time_t MemRecursorCache::fakeTTD(MemRecursorCache::OrderedTagIterator_t& entry, const DNSName& qname, QType qtype, time_t ret, time_t now, uint32_t origTTL, bool refresh) +time_t MemRecursorCache::fakeTTD(MemRecursorCache::OrderedTagIterator_t& entry, const DNSName& qname, QType qtype, time_t ret, time_t now, uint32_t origTTL, MemRecursorCache::Flags flags) { time_t ttl = ret - now; // If we are checking an entry being served stale in refresh mode, // we always consider it stale so a real refresh attempt will be // kicked by SyncRes - if (refresh && entry->d_servedStale > 0) { + if (refresh(flags) && entry->d_servedStale > 0) { return -1; } - if (ttl > 0 && SyncRes::s_refresh_ttlperc > 0) { - const uint32_t deadline = origTTL * SyncRes::s_refresh_ttlperc / 100; + if (ttl > 0 && (forcedRefresh(flags) || SyncRes::s_refresh_ttlperc > 0)) { + const uint32_t deadline = forcedRefresh(flags) ? origTTL / 2 : origTTL * SyncRes::s_refresh_ttlperc / 100; // coverity[store_truncates_time_t] const bool almostExpired = static_cast(ttl) <= deadline; if (almostExpired && qname != g_rootdnsname) { - if (refresh) { + if (refresh(flags)) { return -1; } if (!entry->d_submitted) { @@ -438,7 +438,6 @@ time_t MemRecursorCache::fakeTTD(MemRecursorCache::OrderedTagIterator_t& entry, time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qtype, Flags flags, vector* res, const ComboAddress& who, const OptTag& routingTag, SigRecs* signatures, AuthRecs* authorityRecs, bool* variable, vState* state, bool* wasAuth, DNSName* fromAuthZone, Extra* extra) // NOLINT(readability-function-cognitive-complexity) { bool requireAuth = (flags & RequireAuth) != 0; - bool refresh = (flags & Refresh) != 0; bool serveStale = (flags & ServeStale) != 0; std::optional cachedState{std::nullopt}; @@ -492,7 +491,7 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qtype if (cachedState && ret > now) { ptrAssign(state, *cachedState); } - return fakeTTD(entry, qname, qtype, ret, now, origTTL, refresh); + return fakeTTD(entry, qname, qtype, ret, now, origTTL, flags); } return -1; } @@ -538,7 +537,7 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qtype if (cachedState && ttd > now) { ptrAssign(state, *cachedState); } - return fakeTTD(firstIndexIterator, qname, qtype, ttd, now, origTTL, refresh); + return fakeTTD(firstIndexIterator, qname, qtype, ttd, now, origTTL, flags); } return -1; } @@ -584,7 +583,7 @@ time_t MemRecursorCache::get(time_t now, const DNSName& qname, const QType qtype if (cachedState && ttd > now) { ptrAssign(state, *cachedState); } - return fakeTTD(firstIndexIterator, qname, qtype, ttd, now, origTTL, refresh); + return fakeTTD(firstIndexIterator, qname, qtype, ttd, now, origTTL, flags); } } return -1; diff --git a/pdns/recursordist/recursor_cache.hh b/pdns/recursordist/recursor_cache.hh index d2d12b174354..650cd2a4a203 100644 --- a/pdns/recursordist/recursor_cache.hh +++ b/pdns/recursordist/recursor_cache.hh @@ -74,7 +74,17 @@ public: static constexpr Flags RequireAuth = 1 << 0; static constexpr Flags Refresh = 1 << 1; static constexpr Flags ServeStale = 1 << 2; + static constexpr Flags ForcedRefresh = 1 << 3; + static bool refresh(Flags flags) + { + return (flags & Refresh) != 0; + } + + static bool forcedRefresh(Flags flags) + { + return (flags & Refresh) != 0; + } // The type used to pass auth record data to replace(); If the vector is non-empty, the cache will // store a shared pointer to the copied data. The shared pointer will be returned by get(). There // are optimizations: an empty vector will be stored as a nullptr, but get() will return a pointer @@ -381,7 +391,7 @@ private: return d_maps.at(qname.hash() % d_maps.size()); } - static time_t fakeTTD(OrderedTagIterator_t& entry, const DNSName& qname, QType qtype, time_t ret, time_t now, uint32_t origTTL, bool refresh); + static time_t fakeTTD(OrderedTagIterator_t& entry, const DNSName& qname, QType qtype, time_t ret, time_t now, uint32_t origTTL, Flags flags); static bool entryMatches(OrderedTagIterator_t& entry, QType qtype, bool requireAuth, const ComboAddress& who); static Entries getEntries(MapCombo::LockedContent& map, const DNSName& qname, QType qtype, const OptTag& rtag); From 26fe96211d8037b80b811de77edff4508e14c656 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Thu, 26 Mar 2026 15:07:14 +0100 Subject: [PATCH 04/11] Tie force refresh mode into syncres and tasks Signed-off-by: Otto Moerbeek --- pdns/recursordist/rec-taskqueue.cc | 11 +- pdns/recursordist/rec-taskqueue.hh | 2 +- pdns/recursordist/recursor_cache.cc | 2 +- pdns/recursordist/syncres.cc | 221 ++++++++++++++-------------- pdns/recursordist/syncres.hh | 12 +- pdns/recursordist/taskqueue.hh | 13 +- 6 files changed, 142 insertions(+), 119 deletions(-) diff --git a/pdns/recursordist/rec-taskqueue.cc b/pdns/recursordist/rec-taskqueue.cc index eac312eed205..f0783763242f 100644 --- a/pdns/recursordist/rec-taskqueue.cc +++ b/pdns/recursordist/rec-taskqueue.cc @@ -120,7 +120,8 @@ static void resolveInternal(const struct timeval& now, bool logErrors, const pdn auto log = g_slog->withName("taskq")->withValues("name", Logging::Loggable(task.d_qname), "qtype", Logging::Loggable(QType(task.d_qtype).toString()), "netmask", Logging::Loggable(task.d_netmask.empty() ? "" : task.d_netmask.toString())); const string msg = "Exception while running a background ResolveTask"; SyncRes resolver(now); - resolver.setRefreshAlmostExpired(task.d_refreshMode); + resolver.setRefreshAlmostExpired(task.d_refreshMode != pdns::ResolveTask::RefreshMode::None); + resolver.setForcedRefresh(task.d_refreshMode == pdns::ResolveTask::RefreshMode::Forced); resolver.setQuerySource(task.d_netmask); if (forceNoQM) { resolver.setQNameMinimization(false); @@ -248,14 +249,14 @@ bool runTaskOnce(bool logErrors) return true; } -void pushAlmostExpiredTask(const DNSName& qname, uint16_t qtype, time_t deadline, const Netmask& netmask) +void pushAlmostExpiredTask(const DNSName& qname, uint16_t qtype, time_t deadline, const Netmask& netmask, bool force) { if (SyncRes::isUnsupported(qtype)) { auto log = g_slog->withName("taskq")->withValues("name", Logging::Loggable(qname), "qtype", Logging::Loggable(QType(qtype).toString()), "netmask", Logging::Loggable(netmask.empty() ? "" : netmask.toString())); log->error(Logr::Error, "Cannot push task", "qtype unsupported"); return; } - pdns::ResolveTask task{qname, qtype, deadline, true, resolve, {}, {}, netmask}; + pdns::ResolveTask task{qname, qtype, deadline, force ? pdns::ResolveTask::ResolveTask::Forced : pdns::ResolveTask::RefreshMode::Refresh, resolve, {}, {}, netmask}; if (s_taskQueue.lock()->queue.push(std::move(task))) { ++s_almost_expired_tasks.pushed; } @@ -269,7 +270,7 @@ void pushResolveTask(const DNSName& qname, uint16_t qtype, time_t now, time_t de return; } auto func = forceQMOff ? resolveForceNoQM : resolve; - pdns::ResolveTask task{qname, qtype, deadline, false, func, {}, {}, {}}; + pdns::ResolveTask task{qname, qtype, deadline, pdns::ResolveTask::RefreshMode::None, func, {}, {}, {}}; auto lock = s_taskQueue.lock(); bool inserted = lock->rateLimitSet.insert(now, task); if (inserted) { @@ -287,7 +288,7 @@ bool pushTryDoTTask(const DNSName& qname, uint16_t qtype, const ComboAddress& ip return false; } - pdns::ResolveTask task{qname, qtype, deadline, false, tryDoT, ipAddress, nsname, {}}; + pdns::ResolveTask task{qname, qtype, deadline, pdns::ResolveTask::RefreshMode::None, tryDoT, ipAddress, nsname, {}}; bool pushed = s_taskQueue.lock()->queue.push(std::move(task)); if (pushed) { ++s_almost_expired_tasks.pushed; diff --git a/pdns/recursordist/rec-taskqueue.hh b/pdns/recursordist/rec-taskqueue.hh index e7bc855bd761..e1a30661765c 100644 --- a/pdns/recursordist/rec-taskqueue.hh +++ b/pdns/recursordist/rec-taskqueue.hh @@ -35,7 +35,7 @@ struct ResolveTask; } void runTasks(size_t max, bool logErrors); bool runTaskOnce(bool logErrors); -void pushAlmostExpiredTask(const DNSName& qname, uint16_t qtype, time_t deadline, const Netmask& netmask); +void pushAlmostExpiredTask(const DNSName& qname, uint16_t qtype, time_t deadline, const Netmask& netmask, bool force = false); void pushResolveTask(const DNSName& qname, uint16_t qtype, time_t now, time_t deadline, bool forceQMOff); bool pushTryDoTTask(const DNSName& qname, uint16_t qtype, const ComboAddress& ipAddress, time_t deadline, const DNSName& nsname); void taskQueueClear(); diff --git a/pdns/recursordist/recursor_cache.cc b/pdns/recursordist/recursor_cache.cc index 9766e0b74917..1d8cdac9a27d 100644 --- a/pdns/recursordist/recursor_cache.cc +++ b/pdns/recursordist/recursor_cache.cc @@ -421,7 +421,7 @@ time_t MemRecursorCache::fakeTTD(MemRecursorCache::OrderedTagIterator_t& entry, const uint32_t deadline = forcedRefresh(flags) ? origTTL / 2 : origTTL * SyncRes::s_refresh_ttlperc / 100; // coverity[store_truncates_time_t] const bool almostExpired = static_cast(ttl) <= deadline; - if (almostExpired && qname != g_rootdnsname) { + if (almostExpired /* && qname != g_rootdnsname */) { if (refresh(flags)) { return -1; } diff --git a/pdns/recursordist/syncres.cc b/pdns/recursordist/syncres.cc index 8f7e1ed08959..0c0f66f7aa60 100644 --- a/pdns/recursordist/syncres.cc +++ b/pdns/recursordist/syncres.cc @@ -1858,134 +1858,134 @@ int SyncRes::doResolveNoQNameMinimization(const DNSName& qname, const QType qtyp if (d_serveStale) { LOG(prefix << qname << ": Restart, with serve-stale enabled" << endl); } - // This is a difficult way of expressing "this is a normal query", i.e. not getRootNS. - if (!d_updatingRootNS || qtype.getCode() != QType::NS || !qname.isRoot()) { - DNSName authname(qname); - const auto iter = getBestAuthZone(&authname); - - if (d_cacheonly) { - if (iter != t_sstorage.domainmap->end()) { - if (iter->second.isAuth()) { - LOG(prefix << qname << ": Cache only lookup for '" << qname << "|" << qtype << "', in auth zone" << endl); - ret.clear(); - d_wasOutOfBand = doOOBResolve(qname, qtype, ret, depth, prefix, res); - if (fromCache != nullptr) { - *fromCache = d_wasOutOfBand; - } - return res; + + // Originally this was all skipped for root refresh cases, but we now have a generic solution + // for that via forcedRefresh + DNSName authname(qname); + const auto iter = getBestAuthZone(&authname); + + if (d_cacheonly) { + if (iter != t_sstorage.domainmap->end()) { + if (iter->second.isAuth()) { + LOG(prefix << qname << ": Cache only lookup for '" << qname << "|" << qtype << "', in auth zone" << endl); + ret.clear(); + d_wasOutOfBand = doOOBResolve(qname, qtype, ret, depth, prefix, res); + if (fromCache != nullptr) { + *fromCache = d_wasOutOfBand; } + return res; } } + } - bool wasForwardedOrAuthZone = false; - bool wasAuthZone = false; - bool wasForwardRecurse = false; + bool wasForwardedOrAuthZone = false; + bool wasAuthZone = false; + bool wasForwardRecurse = false; - if (iter != t_sstorage.domainmap->end()) { - wasForwardedOrAuthZone = true; + if (iter != t_sstorage.domainmap->end()) { + wasForwardedOrAuthZone = true; - if (iter->second.isAuth()) { - wasAuthZone = true; - } - else if (iter->second.shouldRecurse()) { - wasForwardRecurse = true; - } + if (iter->second.isAuth()) { + wasAuthZone = true; + } + else if (iter->second.shouldRecurse()) { + wasForwardRecurse = true; } + } - /* When we are looking for a DS, we want to the non-CNAME cache check first - because we can actually have a DS (from the parent zone) AND a CNAME (from - the child zone), and what we really want is the DS */ - if (qtype != QType::DS && doCNAMECacheCheck(qname, qtype, ret, depth, prefix, res, context, wasAuthZone, wasForwardRecurse, loop == 1)) { // will reroute us if needed - d_wasOutOfBand = wasAuthZone; - // Here we have an issue. If we were prevented from going out to the network (cache-only was set, possibly because we - // are in QM Step0) we might have a CNAME but not the corresponding target. - // It means that we will sometimes go to the next steps when we are in fact done, but that's fine since - // we will get the records from the cache, resulting in a small overhead. - // This might be a real problem if we had a RPZ hit, though, because we do not want the processing to continue, since - // RPZ rules will not be evaluated anymore (we already matched). - bool stoppedByPolicyHit = d_appliedPolicy.wasHit(); - if (stoppedByPolicyHit && d_appliedPolicy.d_kind == DNSFilterEngine::PolicyKind::Custom && d_appliedPolicy.d_custom) { - // if the custom RPZ record was a CNAME we still need a full chase - // tested by unit test test_following_cname_chain_with_rpz - if (!d_appliedPolicy.d_custom->empty() && d_appliedPolicy.d_custom->at(0)->getType() == QType::CNAME) { - stoppedByPolicyHit = false; - } - } - if (fromCache != nullptr && (!d_cacheonly || stoppedByPolicyHit)) { - *fromCache = true; + /* When we are looking for a DS, we want to the non-CNAME cache check first + because we can actually have a DS (from the parent zone) AND a CNAME (from + the child zone), and what we really want is the DS */ + if (qtype != QType::DS && doCNAMECacheCheck(qname, qtype, ret, depth, prefix, res, context, wasAuthZone, wasForwardRecurse, loop == 1)) { // will reroute us if needed + d_wasOutOfBand = wasAuthZone; + // Here we have an issue. If we were prevented from going out to the network (cache-only was set, possibly because we + // are in QM Step0) we might have a CNAME but not the corresponding target. + // It means that we will sometimes go to the next steps when we are in fact done, but that's fine since + // we will get the records from the cache, resulting in a small overhead. + // This might be a real problem if we had a RPZ hit, though, because we do not want the processing to continue, since + // RPZ rules will not be evaluated anymore (we already matched). + bool stoppedByPolicyHit = d_appliedPolicy.wasHit(); + if (stoppedByPolicyHit && d_appliedPolicy.d_kind == DNSFilterEngine::PolicyKind::Custom && d_appliedPolicy.d_custom) { + // if the custom RPZ record was a CNAME we still need a full chase + // tested by unit test test_following_cname_chain_with_rpz + if (!d_appliedPolicy.d_custom->empty() && d_appliedPolicy.d_custom->at(0)->getType() == QType::CNAME) { + stoppedByPolicyHit = false; } - /* Apply Post filtering policies */ - - if (d_wantsRPZ && !d_appliedPolicy.wasHit()) { - auto luaLocal = g_luaconfs.getLocal(); - if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { - mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); - bool done = false; - handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); - if (done && fromCache != nullptr) { - *fromCache = true; - } + } + if (fromCache != nullptr && (!d_cacheonly || stoppedByPolicyHit)) { + *fromCache = true; + } + /* Apply Post filtering policies */ + + if (d_wantsRPZ && !d_appliedPolicy.wasHit()) { + auto luaLocal = g_luaconfs.getLocal(); + if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { + mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); + bool done = false; + handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); + if (done && fromCache != nullptr) { + *fromCache = true; } } - // This handles the case mentioned above: if the full CNAME chain leading to the answer was - // constructed from the cache, indicate that. - if (fromCache != nullptr && !*fromCache && haveFinalAnswer(qname, qtype, res, ret)) { - *fromCache = true; - } - return res; } + // This handles the case mentioned above: if the full CNAME chain leading to the answer was + // constructed from the cache, indicate that. + if (fromCache != nullptr && !*fromCache && haveFinalAnswer(qname, qtype, res, ret)) { + *fromCache = true; + } + return res; + } - if (doCacheCheck(qname, authname, wasForwardedOrAuthZone, wasAuthZone, wasForwardRecurse, qtype, ret, depth, prefix, res, context)) { - // we done - d_wasOutOfBand = wasAuthZone; - if (fromCache != nullptr) { - *fromCache = true; - } + if (doCacheCheck(qname, authname, wasForwardedOrAuthZone, wasAuthZone, wasForwardRecurse, qtype, ret, depth, prefix, res, context)) { + // we done + d_wasOutOfBand = wasAuthZone; + if (fromCache != nullptr) { + *fromCache = true; + } - if (d_wantsRPZ && !d_appliedPolicy.wasHit()) { - auto luaLocal = g_luaconfs.getLocal(); - if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { - mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); - bool done = false; - handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); - } + if (d_wantsRPZ && !d_appliedPolicy.wasHit()) { + auto luaLocal = g_luaconfs.getLocal(); + if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { + mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); + bool done = false; + handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); } - - return res; } - /* if we have not found a cached DS (or denial of), now is the time to look for a CNAME */ - if (qtype == QType::DS && doCNAMECacheCheck(qname, qtype, ret, depth, prefix, res, context, wasAuthZone, wasForwardRecurse, loop == 1)) { // will reroute us if needed - d_wasOutOfBand = wasAuthZone; - // Here we have an issue. If we were prevented from going out to the network (cache-only was set, possibly because we - // are in QM Step0) we might have a CNAME but not the corresponding target. - // It means that we will sometimes go to the next steps when we are in fact done, but that's fine since - // we will get the records from the cache, resulting in a small overhead. - // This might be a real problem if we had a RPZ hit, though, because we do not want the processing to continue, since - // RPZ rules will not be evaluated anymore (we already matched). - const bool stoppedByPolicyHit = d_appliedPolicy.wasHit(); + return res; + } - if (fromCache != nullptr && (!d_cacheonly || stoppedByPolicyHit)) { - *fromCache = true; - } - /* Apply Post filtering policies */ - - if (d_wantsRPZ && !stoppedByPolicyHit) { - auto luaLocal = g_luaconfs.getLocal(); - if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { - mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); - bool done = false; - handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); - if (done && fromCache != nullptr) { - *fromCache = true; - } + /* if we have not found a cached DS (or denial of), now is the time to look for a CNAME */ + if (qtype == QType::DS && doCNAMECacheCheck(qname, qtype, ret, depth, prefix, res, context, wasAuthZone, wasForwardRecurse, loop == 1)) { // will reroute us if needed + d_wasOutOfBand = wasAuthZone; + // Here we have an issue. If we were prevented from going out to the network (cache-only was set, possibly because we + // are in QM Step0) we might have a CNAME but not the corresponding target. + // It means that we will sometimes go to the next steps when we are in fact done, but that's fine since + // we will get the records from the cache, resulting in a small overhead. + // This might be a real problem if we had a RPZ hit, though, because we do not want the processing to continue, since + // RPZ rules will not be evaluated anymore (we already matched). + const bool stoppedByPolicyHit = d_appliedPolicy.wasHit(); + + if (fromCache != nullptr && (!d_cacheonly || stoppedByPolicyHit)) { + *fromCache = true; + } + /* Apply Post filtering policies */ + + if (d_wantsRPZ && !stoppedByPolicyHit) { + auto luaLocal = g_luaconfs.getLocal(); + if (luaLocal->dfe.getPostPolicy(ret, d_discardedPolicies, d_appliedPolicy)) { + mergePolicyTags(d_policyTags, d_appliedPolicy.getTags()); + bool done = false; + handlePolicyHit(prefix, qname, qtype, ret, done, res, depth); + if (done && fromCache != nullptr) { + *fromCache = true; } } - if (fromCache != nullptr && !*fromCache && haveFinalAnswer(qname, qtype, res, ret)) { - *fromCache = true; - } - return res; } + if (fromCache != nullptr && !*fromCache && haveFinalAnswer(qname, qtype, res, ret)) { + *fromCache = true; + } + return res; } if (d_cacheonly) { @@ -2502,6 +2502,9 @@ bool SyncRes::doCNAMECacheCheck(const DNSName& qname, const QType qtype, vector< if (d_refresh) { flags |= MemRecursorCache::Refresh; } + if (d_forcedRefresh) { + flags |= MemRecursorCache::ForcedRefresh; + } if (d_serveStale) { flags |= MemRecursorCache::ServeStale; } @@ -2948,6 +2951,9 @@ bool SyncRes::doCacheCheck(const DNSName& qname, const DNSName& authname, bool w if (d_refresh) { flags |= MemRecursorCache::Refresh; } + if (d_forcedRefresh) { + flags |= MemRecursorCache::ForcedRefresh; + } MemRecursorCache::Extra extra; if (g_recCache->get(d_now.tv_sec, sqname, sqt, flags, &cset, d_cacheRemote, d_routingTag, d_doDNSSEC ? &signatures : nullptr, d_doDNSSEC ? &authorityRecs : nullptr, &d_wasVariable, &cachedState, &wasCachedAuth, nullptr, &extra) > 0) { @@ -6304,6 +6310,7 @@ int SyncRes::getRootNS(struct timeval now, asyncresolve_t asyncCallback, unsigne resolver.setUpdatingRootNS(); resolver.setAsyncCallback(std::move(asyncCallback)); resolver.setRefreshAlmostExpired(true); + resolver.setForcedRefresh(true); const string msg = "Failed to update . records"; vector ret; diff --git a/pdns/recursordist/syncres.hh b/pdns/recursordist/syncres.hh index c66110fb0159..af49496dd3a4 100644 --- a/pdns/recursordist/syncres.hh +++ b/pdns/recursordist/syncres.hh @@ -235,7 +235,7 @@ public: struct EDNSStatus { EDNSStatus(const ComboAddress& arg) : - address(arg) {} + address(arg) { } ComboAddress address; time_t ttd{0}; enum EDNSMode : uint8_t @@ -371,6 +371,13 @@ public: return old; } + bool setForcedRefresh(bool doit) + { + auto old = d_forcedRefresh; + d_forcedRefresh = doit; + return old; + } + bool setQNameMinimization(bool state = true) { auto old = d_qNameMinimization; @@ -769,6 +776,7 @@ private: bool d_queryReceivedOverTCP{false}; bool d_followCNAME{true}; bool d_refresh{false}; + bool d_forcedRefresh{false}; bool d_serveStale{false}; LogMode d_lm; @@ -930,7 +938,7 @@ class ImmediateServFailException { public: ImmediateServFailException(string reason_) : - reason(std::move(reason_)) {}; + reason(std::move(reason_)) { }; string reason; //! Print this to tell the user what went wrong }; diff --git a/pdns/recursordist/taskqueue.hh b/pdns/recursordist/taskqueue.hh index 6f174e05fec1..4376160e09f9 100644 --- a/pdns/recursordist/taskqueue.hh +++ b/pdns/recursordist/taskqueue.hh @@ -52,8 +52,15 @@ struct ResolveTask uint16_t d_qtype; // Deadline is not part of index and not used by operator<() time_t d_deadline; - // Whether to run this task in regular mode (false) or in the mode that refreshes almost expired tasks - bool d_refreshMode; + // Whether to run this task in normal mode (None) or in the mode that refreshes almost expired + // rrsets (Regular) or in Forced Mode + enum RefreshMode : uint8_t + { + None, + Refresh, + Forced + }; + RefreshMode d_refreshMode; // Use a function pointer as comparing std::functions is a nuisance using TaskFunction = void (*)(const struct timeval& now, bool logErrors, const ResolveTask& task); TaskFunction d_func; @@ -122,7 +129,7 @@ private: composite_key, member, - member, + member, member, member, member>>, From 0a498a6596294311f911dc7dfa3423ece5d5abf9 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Tue, 31 Mar 2026 16:34:53 +0200 Subject: [PATCH 05/11] Compute root refresh period based no TTLs of actual records Signed-off-by: Otto Moerbeek --- pdns/recursordist/rec-main.cc | 7 ++++--- pdns/recursordist/syncres.cc | 9 +++++++-- pdns/recursordist/syncres.hh | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index c95aeb005d80..eca2ae7b9934 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -2608,17 +2608,18 @@ static void houseKeepingWork(Logr::log_t log) pruneCookies(now.tv_sec - 3000); }); - // By default, refresh at 80% of max-cache-ttl with a minimum period of 10s + // By default, refresh at 80% of lowest TTL seen in the result with a minimum period of 10s const unsigned int minRootRefreshInterval = 10; static PeriodicTask rootUpdateTask{"rootUpdateTask", std::max(SyncRes::s_maxcachettl * 8 / 10, minRootRefreshInterval)}; rootUpdateTask.runIfDue(now, [now, &log, minRootRefreshInterval]() { int res = 0; + uint32_t minttl = SyncRes::s_maxcachettl; if (!g_regressionTestMode) { - res = SyncRes::getRootNS(now, nullptr, 0, log); + res = SyncRes::getRootNS(now, nullptr, 0, log, minttl); } if (res == 0) { // Success, go back to the default period - rootUpdateTask.setPeriod(std::max(SyncRes::s_maxcachettl * 8 / 10, minRootRefreshInterval)); + rootUpdateTask.setPeriod(std::max(minttl * 8 / 10, minRootRefreshInterval)); } else { // On failure, go to the middle of the remaining period (initially 80% / 8 = 10%) and shorten the interval on each diff --git a/pdns/recursordist/syncres.cc b/pdns/recursordist/syncres.cc index 0c0f66f7aa60..9e042e5873ce 100644 --- a/pdns/recursordist/syncres.cc +++ b/pdns/recursordist/syncres.cc @@ -2366,7 +2366,8 @@ void SyncRes::getBestNSFromCache(const DNSName& qname, const QType qtype, vector /* let's prevent an infinite loop */ if (!d_updatingRootNS) { auto log = g_slog->withName("housekeeping"); - getRootNS(d_now, d_asyncResolve, depth, log); + uint32_t dummy{}; + getRootNS(d_now, d_asyncResolve, depth, log, dummy); } } } while (subdomain.chopOff()); @@ -6300,7 +6301,7 @@ int directResolve(const DNSName& qname, const QType qtype, const QClass qclass, return res; } -int SyncRes::getRootNS(struct timeval now, asyncresolve_t asyncCallback, unsigned int depth, Logr::log_t log) +int SyncRes::getRootNS(struct timeval now, asyncresolve_t asyncCallback, unsigned int depth, Logr::log_t log, uint32_t& minttl) { if (::arg()["hint-file"] == "no-refresh") { return 0; @@ -6343,6 +6344,10 @@ int SyncRes::getRootNS(struct timeval now, asyncresolve_t asyncCallback, unsigne } if (res == 0) { + minttl = SyncRes::s_maxcachettl; + for (const auto& record : ret) { + minttl = std::min(minttl, record.d_ttl); + } log->info(Logr::Debug, "Refreshed . records"); } else { diff --git a/pdns/recursordist/syncres.hh b/pdns/recursordist/syncres.hh index af49496dd3a4..de3b3581eb4b 100644 --- a/pdns/recursordist/syncres.hh +++ b/pdns/recursordist/syncres.hh @@ -180,7 +180,7 @@ public: static size_t getNSSpeedTable(size_t maxSize, std::string& ret); static size_t putIntoNSSpeedTable(const std::string& ret); - static int getRootNS(struct timeval now, asyncresolve_t asyncCallback, unsigned int depth, Logr::log_t); + static int getRootNS(struct timeval now, asyncresolve_t asyncCallback, unsigned int depth, Logr::log_t, uint32_t& minttl); static void addDontQuery(const std::string& mask) { if (!s_dontQuery) { From 898b5688690a51627e33803f5dc96d5b2099fe7d Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Wed, 1 Apr 2026 10:30:01 +0200 Subject: [PATCH 06/11] Add keepwarm to recordcache config Signed-off-by: Otto Moerbeek --- pdns/recursordist/rec-rust-lib/generate.py | 4 ++++ .../rec-rust-lib/rust-bridge-in.rs | 9 +++++++++ .../rec-rust-lib/rust/src/bridge.rs | 20 +++++++++++++++++++ pdns/recursordist/rec-rust-lib/table.py | 13 ++++++++++++ 4 files changed, 46 insertions(+) diff --git a/pdns/recursordist/rec-rust-lib/generate.py b/pdns/recursordist/rec-rust-lib/generate.py index 4d83a4ecb51d..4bef23cfbb9f 100644 --- a/pdns/recursordist/rec-rust-lib/generate.py +++ b/pdns/recursordist/rec-rust-lib/generate.py @@ -118,6 +118,7 @@ class LType(Enum): ListZoneToCaches = auto() ListOutgoingTLSConfigurations = auto() ListOpenTelemetryTraceConditions = auto() + ListQNameAndQTypes = auto() String = auto() Uint64 = auto() @@ -140,6 +141,7 @@ class LType(Enum): LType.ListIncomingWSConfigs, LType.ListOutgoingTLSConfigurations, LType.ListOpenTelemetryTraceConditions, + LType.ListQNameAndQTypes, ) @@ -214,6 +216,8 @@ def get_newdoc_typename(typ): return "Sequence of `OutgoingTLSConfiguration`_" if typ == LType.ListOpenTelemetryTraceConditions: return "Sequence of `OpenTelemetryTraceCondition`_" + if typ == LType.ListQNameAndQTypes: + return "Sequence of `QNameAndQType`_" return "Unknown2" + str(typ) diff --git a/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs b/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs index 9c298b006f2e..c1e8557a5855 100644 --- a/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs +++ b/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs @@ -382,6 +382,15 @@ struct OpenTelemetryTraceCondition { traceid_only: bool, } +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +struct QNameAndQType { + #[serde(default, skip_serializing_if = "crate::is_default")] + qname: String, + #[serde(default = "crate::def_qnameandqtype_qtype", skip_serializing_if = "crate::def_value_equals_qnameqtype_qtype")] + qtype: String, +} + // Two structs used to generated YAML based on a vector of name to value mappings // Cannot use Enum as CXX has only very basic Enum support struct Value { diff --git a/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs b/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs index 7d64675e9646..e8853230866c 100644 --- a/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs +++ b/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs @@ -874,6 +874,18 @@ impl OpenTelemetryTraceCondition { } } +impl QNameAndQType { + pub fn validate(&self, field: &str) -> Result<(), ValidationError> { + if self.qname.is_empty() { + let msg = format!("{}: value may not be empty", field); + return Err(ValidationError { msg }); + } + validate_qtype(&(field.to_string() + ".qtype"), &self.qtype)?; + Ok(()) + } +} + + #[allow(clippy::ptr_arg)] //# Avoids creating a rust::Slice object on the C++ side. pub fn validate_auth_zones(field: &str, vec: &Vec) -> Result<(), ValidationError> { validate_vec(field, vec, |field, element| element.validate(field)) @@ -1393,6 +1405,14 @@ pub fn def_value_equals_pb_strategy(value: &String) -> bool { &def_pb_strategy() == value } +pub fn def_qnameandqtype_qtype() -> String { + String::from("A") +} + +pub fn def_value_equals_qnameqtype_qtype(value: &String) -> bool { + &def_qnameandqtype_qtype() == value +} + pub fn validate_dnssec(dnssec: &recsettings::Dnssec) -> Result<(), ValidationError> { let val = dnssec.validation.as_str(); match val { diff --git a/pdns/recursordist/rec-rust-lib/table.py b/pdns/recursordist/rec-rust-lib/table.py index a94f132b5003..3ef887079ffd 100644 --- a/pdns/recursordist/rec-rust-lib/table.py +++ b/pdns/recursordist/rec-rust-lib/table.py @@ -3689,4 +3689,17 @@ "versionadded": "5.4.0", "runtime": ["reload-lua-config", "reload-yaml"], }, + { + "name": "keepwarm", + "section": "recordcache", + "type": LType.ListQNameAndQTypes, + "default": "", + "help": "Sequence of QNameAndQType", + "doc": """ + List of names to keep warm in the cache + """, + "skip-old": "No equivalent old style setting", + "versionadded": "5.5.0", + "runtime": ["reload-lua-config", "reload-yaml"], + }, ] From 8cba2b1c7dc62804a80847308577eb543c828c00 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Wed, 1 Apr 2026 13:58:16 +0200 Subject: [PATCH 07/11] Do the actual pin in cache work. Signed-off-by: Otto Moerbeek --- pdns/recursordist/rec-lua-conf.hh | 1 + pdns/recursordist/rec-main.cc | 87 +++++++++++++++++++- pdns/recursordist/rec-rust-lib/cxxsupport.cc | 8 ++ pdns/recursordist/rec-taskqueue.cc | 6 +- pdns/recursordist/rec_channel_rec.cc | 2 + pdns/recursordist/syncres.hh | 4 +- 6 files changed, 100 insertions(+), 8 deletions(-) diff --git a/pdns/recursordist/rec-lua-conf.hh b/pdns/recursordist/rec-lua-conf.hh index 8da41a13aff8..d815ccef54b1 100644 --- a/pdns/recursordist/rec-lua-conf.hh +++ b/pdns/recursordist/rec-lua-conf.hh @@ -141,6 +141,7 @@ public: ProtobufExportConfig outgoingProtobufExportConfig; FrameStreamExportConfig frameStreamExportConfig; FrameStreamExportConfig nodFrameStreamExportConfig; + std::vector> keepWarm; std::shared_ptr d_slog; /* we need to increment this every time the configuration is reloaded, so we know if we need to reload the protobuf diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index eca2ae7b9934..7b55a6efb6e9 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -99,7 +99,6 @@ LockGuarded> g_initialAllowNotifyFrom; // new thre LockGuarded> g_initialAllowNotifyFor; // new threads need this to be setup LockGuarded> g_initialOpenTelemetryConditions; // new threads need this to be setup static time_t s_statisticsInterval; -static std::atomic s_counter; int g_argc; char** g_argv; static string s_structured_logger_backend; @@ -2452,6 +2451,75 @@ static void handleRCC(int fileDesc, FDMultiplexer::funcparam_t& /* var */) } } +static time_t keepCacheWarm(const timeval& now, LocalStateHolder& luaconfsLocal) +{ + auto log = g_slog->withName("cachewarmer"); + time_t wait = 60; + + for (const auto& [qname, qtype] : luaconfsLocal->keepWarm) { + SyncRes resolver(now); + resolver.setQNameMinimization(true); + resolver.setCacheOnly(true); + resolver.setDoDNSSEC(g_dnssecmode != DNSSECMode::Off); + resolver.setDNSSECValidationRequested(g_dnssecmode != DNSSECMode::Off && g_dnssecmode != DNSSECMode::ProcessNoValidate); + std::vector ret; + int res = -1; + const std::string msg = "Exception while resolving"; + try { + res = resolver.beginResolve(qname, qtype, QClass::IN, ret, 0); + } + catch (const PDNSException& e) { + log->error(Logr::Warning, e.reason, msg, "exception", Logging::Loggable("PDNSException")); + ret.clear(); + } + catch (const ImmediateServFailException& e) { + log->error(Logr::Warning, e.reason, msg, "exception", Logging::Loggable("ImmediateServFailException")); + ret.clear(); + } + catch (const PolicyHitException& e) { + log->info(Logr::Warning, msg, "exception", Logging::Loggable("PolicyHitException")); + ret.clear(); + } + catch (const std::exception& e) { + log->error(Logr::Warning, e.what(), msg, "exception", Logging::Loggable("std::exception")); + ret.clear(); + } + catch (...) { + log->info(Logr::Warning, msg); + ret.clear(); + } + + if (res == RCode::NoError && ret.size() > 0) { + uint32_t minttl = std::numeric_limits::max(); + for (const auto& record : ret) { + minttl = std::min(minttl, record.d_ttl); + } + if (minttl > 5) { + wait = std::min(wait, static_cast(minttl - 5)); + continue; + } + wait = std::min(wait, static_cast(1)); + } + + NegCache::NegCacheEntry negEntry; + bool inNegCache = g_negCache->get(qname, qtype, now, negEntry, false); + if (!inNegCache) { + log->info(Logr::Debug, "Absent or expiring and not in negache, pushing task", "qname", Logging::Loggable(qname), + "qtype", Logging::Loggable(qtype), + "res", Logging::Loggable(res), "size", Logging::Loggable(ret.size())); + // Work to be done + pushAlmostExpiredTask(qname, qtype, now.tv_sec + 60, ComboAddress("255.255.255.255"), true); + wait = std::min(wait, static_cast(1)); + } + else { + auto expiring = negEntry.d_ttd - now.tv_sec; + wait = std::min(wait, expiring); + } + } + log->info(Logr::Debug, "Wait", "interval", Logging::Loggable(wait)); + return wait; +} + class PeriodicTask { public: @@ -2530,6 +2598,12 @@ static void houseKeepingWork(Logr::log_t log) // Below are the thread specific tasks for the handler and the taskThread // Likley a few handler tasks could be moved to the taskThread if (info.isTaskThread()) { + static PeriodicTask keepWarmTask{"KeepWarmTask", 1}; + keepWarmTask.runIfDue(now, [now, &luaconfsLocal] { + auto actionRequired = keepCacheWarm(now, luaconfsLocal); + keepWarmTask.setPeriod(actionRequired); + }); + // TaskQueue is run always runTasks(10, g_logCommonErrors); @@ -2735,6 +2809,13 @@ static void recLoop() auto& threadInfo = RecThreadInfo::self(); + static std::atomic s_counter; + + // Use primes, it avoid not being scheduled in cases where the counter has a regular pattern. + // We want to call handler thread often, it gets scheduled about 2 times per second on an idle recursor + constexpr uint32_t handlerAndTaskInterval = 11; + constexpr uint32_t otherInterval = 499; + while (!RecursorControlChannel::stop) { try { while (g_multiTasker->schedule(g_now)) { @@ -2743,7 +2824,7 @@ static void recLoop() // Use primes, it avoid not being scheduled in cases where the counter has a regular pattern. // We want to call handler thread often, it gets scheduled about 2 times per second - if (((threadInfo.isHandler() || threadInfo.isTaskThread()) && s_counter % 11 == 0) || s_counter % 499 == 0) { + if (((threadInfo.isHandler() || threadInfo.isTaskThread()) && s_counter % handlerAndTaskInterval == 0) || s_counter % otherInterval == 0) { timeval start{}; Utility::gettimeofday(&start); g_multiTasker->makeThread(houseKeeping, nullptr); @@ -2784,7 +2865,7 @@ static void recLoop() } runLuaMaintenance(threadInfo, last_lua_maintenance, luaMaintenanceInterval); - auto timeoutUsec = g_multiTasker->nextWaiterDelayUsec(500000); + auto timeoutUsec = g_multiTasker->nextWaiterDelayUsec(1000000U / handlerAndTaskInterval / 2); t_fdm->run(&g_now, static_cast(timeoutUsec / 1000)); // 'run' updates g_now for us } diff --git a/pdns/recursordist/rec-rust-lib/cxxsupport.cc b/pdns/recursordist/rec-rust-lib/cxxsupport.cc index 30ff0c39934a..e80f456236e3 100644 --- a/pdns/recursordist/rec-rust-lib/cxxsupport.cc +++ b/pdns/recursordist/rec-rust-lib/cxxsupport.cc @@ -1345,6 +1345,13 @@ void fromRustToLuaConfig(const rust::Vec& keepwarm, std::vector>& lua) +{ + for (const auto& warm : keepwarm) { + lua.emplace_back(DNSName(std::string(warm.qname)), QType::chartocode(std::string(warm.qtype).data())); + } +} + void fromRustToOTTraceConditions(const rust::Vec& settings, OpenTelemetryTraceConditions& conditions) { for (const auto& setting : settings) { @@ -1397,6 +1404,7 @@ void pdns::settings::rec::fromBridgeStructToLuaConfig(const pdns::rust::settings fromRustToLuaConfig(settings.recursor.forwarding_catalog_zones, luaConfig.catalogzones); fromRustToLuaConfig(settings.incoming.proxymappings, proxyMapping); fromRustToOTTraceConditions(settings.logging.opentelemetry_trace_conditions, conditions); + fromRustToLuaConfig(settings.recordcache.keepwarm, luaConfig.keepWarm); } // Return true if an item that's (also) a Lua config item is set diff --git a/pdns/recursordist/rec-taskqueue.cc b/pdns/recursordist/rec-taskqueue.cc index f0783763242f..4c78977f90a0 100644 --- a/pdns/recursordist/rec-taskqueue.cc +++ b/pdns/recursordist/rec-taskqueue.cc @@ -130,7 +130,7 @@ static void resolveInternal(const struct timeval& now, bool logErrors, const pdn bool exceptionOccurred = true; vector ret; try { - log->info(Logr::Debug, "resolving", "refresh", Logging::Loggable(task.d_refreshMode)); + log->info(Logr::Debug, "resolving", "refreshMode", Logging::Loggable(task.d_refreshMode)); int res = resolver.beginResolve(task.d_qname, QType(task.d_qtype), QClass::IN, ret); exceptionOccurred = false; log->info(Logr::Debug, "done", "rcode", Logging::Loggable(res), "records", Logging::Loggable(ret.size())); @@ -155,7 +155,7 @@ static void resolveInternal(const struct timeval& now, bool logErrors, const pdn log->error(Logr::Warning, msg, "Unexpected exception"); } if (exceptionOccurred) { - if (task.d_refreshMode) { + if (task.d_refreshMode != 0) { ++s_almost_expired_tasks.exceptions; } else { @@ -163,7 +163,7 @@ static void resolveInternal(const struct timeval& now, bool logErrors, const pdn } } else { - if (task.d_refreshMode) { + if (task.d_refreshMode != 0) { ++s_almost_expired_tasks.run; } else { diff --git a/pdns/recursordist/rec_channel_rec.cc b/pdns/recursordist/rec_channel_rec.cc index d5f083dbdf7d..1b5f6dd57a0c 100644 --- a/pdns/recursordist/rec_channel_rec.cc +++ b/pdns/recursordist/rec_channel_rec.cc @@ -2121,6 +2121,7 @@ RecursorControlChannel::Answer luaconfig(bool broadcast) // We might have a lua config file, but also process dynamic YAML parts if applicable, currently those are: // - the OT trace conditions // - the outgoing TLS config + // - keepWarm entries try { if (yamlstat == pdns::settings::rec::YamlSettingsStatus::OK) { // YAML read above succeeded @@ -2128,6 +2129,7 @@ RecursorControlChannel::Answer luaconfig(bool broadcast) LuaConfigItems dummyLuaConfig; // we do not use the converted from YAML LuaConfigItems, but the "real thing" pdns::settings::rec::fromBridgeStructToLuaConfig(settings, dummyLuaConfig, dummyProxyMapping, conditions); TCPOutConnectionManager::setupOutgoingTLSConfigTables(settings); + lci.keepWarm = dummyLuaConfig.keepWarm; // XXX } if (!::arg()["lua-config-file"].empty()) { loadRecursorLuaConfig(::arg()["lua-config-file"], proxyMapping, lci); // will bump generation diff --git a/pdns/recursordist/syncres.hh b/pdns/recursordist/syncres.hh index de3b3581eb4b..77075abde9b5 100644 --- a/pdns/recursordist/syncres.hh +++ b/pdns/recursordist/syncres.hh @@ -235,7 +235,7 @@ public: struct EDNSStatus { EDNSStatus(const ComboAddress& arg) : - address(arg) { } + address(arg) {} ComboAddress address; time_t ttd{0}; enum EDNSMode : uint8_t @@ -938,7 +938,7 @@ class ImmediateServFailException { public: ImmediateServFailException(string reason_) : - reason(std::move(reason_)) { }; + reason(std::move(reason_)) {}; string reason; //! Print this to tell the user what went wrong }; From 1544454cf72662489b088a77c0068cb00bae9bca Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Wed, 8 Apr 2026 13:19:39 +0200 Subject: [PATCH 08/11] Base of a working mechanism using a multi-index Signed-off-by: Otto Moerbeek --- pdns/recursordist/rec-keepwarm.hh | 100 ++++++++++++++++++ pdns/recursordist/rec-main.cc | 140 ++++++++++++++++---------- pdns/recursordist/rec_channel_rec.cc | 2 + pdns/recursordist/test-syncres_cc1.cc | 60 +++++++++++ 4 files changed, 250 insertions(+), 52 deletions(-) create mode 100644 pdns/recursordist/rec-keepwarm.hh diff --git a/pdns/recursordist/rec-keepwarm.hh b/pdns/recursordist/rec-keepwarm.hh new file mode 100644 index 000000000000..03076e0b8e7f --- /dev/null +++ b/pdns/recursordist/rec-keepwarm.hh @@ -0,0 +1,100 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "dnsname.hh" +#include "qtype.hh" + +namespace rec +{ +using namespace ::boost::multi_index; + +struct KeepWarmEntry +{ + KeepWarmEntry(DNSName name, QType qtype, time_t ttd = 0) : d_qname(std::move(name)), d_ttd(ttd), d_qtype(qtype) {} + DNSName d_qname; + time_t d_ttd; + uint16_t d_qtype; +}; + +class KeepWarm +{ +public: + struct QNameQTypeTag + { + }; + + struct TTDTag + { + }; + + using Queue = multi_index_container< + KeepWarmEntry, + indexed_by, + composite_key, + member>>, + ordered_non_unique, member, std::less<>>>>; + + [[nodiscard]] const Queue& get() const + { + return d_queue; + } + void modifyTTD(const DNSName& qname, uint16_t qtype, uint32_t ttd) + { + auto item = d_queue.find(std::tie(qname, qtype)); + if (item != d_queue.end()) { + d_queue.modify(item, [ttd](rec::KeepWarmEntry& entry) { entry.d_ttd = ttd; }); + } + + } + void emplace(const DNSName& name, uint16_t qtype) + { + d_queue.emplace(name, qtype); + } + Queue::iterator erase(Queue::iterator iter) + { + return d_queue.erase(iter); + } + Queue::iterator begin() + { + return d_queue.begin(); + } + Queue::iterator end() + { + return d_queue.end(); + } + +private: + Queue d_queue; +}; +} diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index 7b55a6efb6e9..622aee7a42f7 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -40,6 +40,7 @@ #include "threadname.hh" #include "version.hh" #include "ws-recursor.hh" +#include "rec-keepwarm.hh" #ifdef NOD_ENABLED #include "nod.hh" @@ -2454,68 +2455,103 @@ static void handleRCC(int fileDesc, FDMultiplexer::funcparam_t& /* var */) static time_t keepCacheWarm(const timeval& now, LocalStateHolder& luaconfsLocal) { auto log = g_slog->withName("cachewarmer"); - time_t wait = 60; - - for (const auto& [qname, qtype] : luaconfsLocal->keepWarm) { - SyncRes resolver(now); - resolver.setQNameMinimization(true); - resolver.setCacheOnly(true); - resolver.setDoDNSSEC(g_dnssecmode != DNSSECMode::Off); - resolver.setDNSSECValidationRequested(g_dnssecmode != DNSSECMode::Off && g_dnssecmode != DNSSECMode::ProcessNoValidate); - std::vector ret; - int res = -1; - const std::string msg = "Exception while resolving"; - try { - res = resolver.beginResolve(qname, qtype, QClass::IN, ret, 0); - } - catch (const PDNSException& e) { - log->error(Logr::Warning, e.reason, msg, "exception", Logging::Loggable("PDNSException")); - ret.clear(); - } - catch (const ImmediateServFailException& e) { - log->error(Logr::Warning, e.reason, msg, "exception", Logging::Loggable("ImmediateServFailException")); - ret.clear(); - } - catch (const PolicyHitException& e) { - log->info(Logr::Warning, msg, "exception", Logging::Loggable("PolicyHitException")); - ret.clear(); + + static LockGuarded s_keepwarm; + static uint64_t lastgeneration = 0; + + auto lock = s_keepwarm.lock(); + + if (lastgeneration != luaconfsLocal->generation) { + lastgeneration = luaconfsLocal->generation; + for (const auto& [qname, qtype] : luaconfsLocal->keepWarm) { + lock->emplace(qname, qtype); } - catch (const std::exception& e) { - log->error(Logr::Warning, e.what(), msg, "exception", Logging::Loggable("std::exception")); - ret.clear(); + std::set> all; + std::copy(luaconfsLocal->keepWarm.begin(), luaconfsLocal->keepWarm.end(), std::inserter(all, all.end())); + for (auto iter = lock->begin(); iter != lock->end();) { + if (all.count({iter->d_qname, QType(iter->d_qtype)}) == 0) { + iter = lock->erase(iter); + } + else { + ++iter; + } } - catch (...) { - log->info(Logr::Warning, msg); - ret.clear(); + } + + std::vector toBeHandled; + + auto& sidx = lock->get().template get(); + auto siter = sidx.begin(); + + const int batchSize = 100; + const time_t specialTime = 1; + const time_t cooldown = 60; + const time_t almost = 5; + for (int i = 0; i < batchSize && siter != sidx.end(); i++, siter++) { + if (siter->d_ttd > now.tv_sec + almost) { + break; } + toBeHandled.emplace_back(*siter); + } - if (res == RCode::NoError && ret.size() > 0) { - uint32_t minttl = std::numeric_limits::max(); - for (const auto& record : ret) { - minttl = std::min(minttl, record.d_ttl); + + for (auto& element : toBeHandled) { + if (element.d_ttd == specialTime) { + SyncRes resolver(now); + resolver.setQNameMinimization(true); + resolver.setCacheOnly(true); + resolver.setDoDNSSEC(g_dnssecmode != DNSSECMode::Off); + resolver.setDNSSECValidationRequested(g_dnssecmode != DNSSECMode::Off && g_dnssecmode != DNSSECMode::ProcessNoValidate); + std::vector ret; + const std::string msg = "Exception while resolving"; + try { + resolver.beginResolve(element.d_qname, element.d_qtype, QClass::IN, ret, 0); } - if (minttl > 5) { - wait = std::min(wait, static_cast(minttl - 5)); - continue; + catch (const PDNSException& e) { + log->error(Logr::Warning, e.reason, msg, "exception", Logging::Loggable("PDNSException")); + ret.clear(); + } + catch (const ImmediateServFailException& e) { + log->error(Logr::Warning, e.reason, msg, "exception", Logging::Loggable("ImmediateServFailException")); + ret.clear(); + } + catch (const PolicyHitException& e) { + log->info(Logr::Warning, msg, "exception", Logging::Loggable("PolicyHitException")); + ret.clear(); + } + catch (const std::exception& e) { + log->error(Logr::Warning, e.what(), msg, "exception", Logging::Loggable("std::exception")); + ret.clear(); + } + catch (...) { + log->info(Logr::Warning, msg); + ret.clear(); } - wait = std::min(wait, static_cast(1)); - } - NegCache::NegCacheEntry negEntry; - bool inNegCache = g_negCache->get(qname, qtype, now, negEntry, false); - if (!inNegCache) { - log->info(Logr::Debug, "Absent or expiring and not in negache, pushing task", "qname", Logging::Loggable(qname), - "qtype", Logging::Loggable(qtype), - "res", Logging::Loggable(res), "size", Logging::Loggable(ret.size())); - // Work to be done - pushAlmostExpiredTask(qname, qtype, now.tv_sec + 60, ComboAddress("255.255.255.255"), true); - wait = std::min(wait, static_cast(1)); + uint32_t minttl = cooldown; // If no records found, either it did not resolve at all, or it did + // not resolve yet. In both cases, pace the work. + if (ret.size() > 0) { + minttl = std::numeric_limits::max(); + for (const auto& record : ret) { + minttl = std::min(minttl, record.d_ttl); + } + } + lock->modifyTTD(element.d_qname, element.d_qtype, now.tv_sec + minttl); } - else { - auto expiring = negEntry.d_ttd - now.tv_sec; - wait = std::min(wait, expiring); + else if (element.d_ttd == 0 || element.d_ttd <= now.tv_sec + almost) { + pushAlmostExpiredTask(element.d_qname, element.d_qtype, now.tv_sec + cooldown, ComboAddress("255.255.255.255"), true); + lock->modifyTTD(element.d_qname, element.d_qtype, specialTime); } } + + time_t wait = cooldown; + siter = sidx.begin(); + if (siter != sidx.end()) { + wait = siter->d_ttd - now.tv_sec - 1; + wait = std::max(static_cast(1), wait); + wait = std::min(static_cast(cooldown), wait); + } + log->info(Logr::Debug, "Wait", "interval", Logging::Loggable(wait)); return wait; } diff --git a/pdns/recursordist/rec_channel_rec.cc b/pdns/recursordist/rec_channel_rec.cc index 1b5f6dd57a0c..a05d0f8fe492 100644 --- a/pdns/recursordist/rec_channel_rec.cc +++ b/pdns/recursordist/rec_channel_rec.cc @@ -2130,6 +2130,8 @@ RecursorControlChannel::Answer luaconfig(bool broadcast) pdns::settings::rec::fromBridgeStructToLuaConfig(settings, dummyLuaConfig, dummyProxyMapping, conditions); TCPOutConnectionManager::setupOutgoingTLSConfigTables(settings); lci.keepWarm = dummyLuaConfig.keepWarm; // XXX + auto generation = g_luaconfs.getLocal()->generation; + lci.generation = generation + 1; } if (!::arg()["lua-config-file"].empty()) { loadRecursorLuaConfig(::arg()["lua-config-file"], proxyMapping, lci); // will bump generation diff --git a/pdns/recursordist/test-syncres_cc1.cc b/pdns/recursordist/test-syncres_cc1.cc index a30c3cb08b10..be9ac7ae9b98 100644 --- a/pdns/recursordist/test-syncres_cc1.cc +++ b/pdns/recursordist/test-syncres_cc1.cc @@ -1960,6 +1960,66 @@ BOOST_AUTO_TEST_CASE(test_cname_loop_forwarder) BOOST_REQUIRE_THROW(resolver->beginResolve(target, QType(QType::A), QClass::IN, ret), ImmediateServFailException); } +BOOST_AUTO_TEST_CASE(test_cname_loop2) +{ + std::unique_ptr sr; + initSR(sr, false, true); + sr->setQNameMinimization(); + + primeHints(); + + size_t count = 0; + const DNSName target1("cname1.powerdns.com."); + const DNSName target2("cname2.powerdns.com."); + + sr->setAsyncCallback([&](const ComboAddress& address, const DNSName& domain, int /* type */, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, std::optional& /* srcmask */, const ResolveContext& /* context */, LWResult* res, bool* /* chained */) { + count++; + + if (isRootServer(address)) { + + setLWResult(res, 0, false, false, true); + addRecordToLW(res, domain, QType::NS, "a.gtld-servers.net.", DNSResourceRecord::AUTHORITY, 172800); + addRecordToLW(res, "a.gtld-servers.net.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600); + return LWResult::Result::Success; + } + if (address == ComboAddress("192.0.2.1:53")) { + + //if (domain == target1) { + setLWResult(res, 0, true, false, false); + addRecordToLW(res, target1, QType::CNAME, target2.toString()); + addRecordToLW(res, target2, QType::CNAME, target1.toString()); + return LWResult::Result::Success; + //} + //if (domain == target2) { + //setLWResult(res, 0, true, false, false); + //addRecordToLW(res, target2, QType::CNAME, target1.toString()); + //addRecordToLW(res, target1, QType::CNAME, target2.toString()); + //return LWResult::Result::Success; + //} + } + + return LWResult::Result::Timeout; + }); + + vector ret; + int res = sr->beginResolve(target1, QType(QType::A), QClass::IN, ret); + BOOST_CHECK_EQUAL(res, RCode::ServFail); + BOOST_CHECK_EQUAL(ret.size(), 0U); + BOOST_CHECK_EQUAL(count, 8U); + + // And again to check cache + try { + sr->beginResolve(DNSName("test1.powerdns.com"), QType(QType::A), QClass::IN, ret); + BOOST_CHECK(false); + } + catch (const ImmediateServFailException& ex) { + BOOST_CHECK(true); + BOOST_CHECK(false); + } + BOOST_CHECK(false); +} + + BOOST_AUTO_TEST_CASE(test_cname_long_loop) { std::unique_ptr sr; From 8de07aa070e389b3d5008dbc650146f3b98be3e9 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Tue, 21 Apr 2026 16:21:40 +0200 Subject: [PATCH 09/11] Make number of task threads configurable Signed-off-by: Otto Moerbeek --- pdns/recursordist/rec-main.cc | 2 ++ pdns/recursordist/rec-main.hh | 8 +++++++- pdns/recursordist/rec-rust-lib/table.py | 15 +++++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index 622aee7a42f7..18ee539f1ebb 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -127,6 +127,7 @@ bool RecThreadInfo::s_weDistributeQueries; // if true, 1 or more threads listen unsigned int RecThreadInfo::s_numDistributorThreads; unsigned int RecThreadInfo::s_numUDPWorkerThreads; unsigned int RecThreadInfo::s_numTCPWorkerThreads; +unsigned int RecThreadInfo::s_numTaskThreads; thread_local unsigned int RecThreadInfo::t_id{RecThreadInfo::TID_NOT_INITED}; pdns::RateLimitedLog g_rateLimitedLogger; @@ -2254,6 +2255,7 @@ static int serviceMain(Logr::log_t log) RecThreadInfo::setNumDistributorThreads(::arg().asNum("distributor-threads")); RecThreadInfo::setNumUDPWorkerThreads(::arg().asNum("threads")); + RecThreadInfo::setNumTaskThreads(::arg().asNum("taskthreads")); if (RecThreadInfo::numUDPWorkers() < 1) { log->info(Logr::Warning, "Asked to run with 0 threads, raising to 1 instead"); RecThreadInfo::setNumUDPWorkerThreads(1); diff --git a/pdns/recursordist/rec-main.hh b/pdns/recursordist/rec-main.hh index 5d388f2bae57..1e18d55adcbb 100644 --- a/pdns/recursordist/rec-main.hh +++ b/pdns/recursordist/rec-main.hh @@ -439,7 +439,7 @@ public: static unsigned int numTaskThreads() { - return 1; + return s_numTaskThreads; } static unsigned int numUDPWorkers() @@ -482,6 +482,11 @@ public: s_numDistributorThreads = n; } + static void setNumTaskThreads(unsigned int n) + { + s_numTaskThreads = n; + } + static unsigned int numRecursorThreads() { return numHandlers() + numDistributors() + numUDPWorkers() + numTCPWorkers() + numTaskThreads(); @@ -589,6 +594,7 @@ private: static unsigned int s_numDistributorThreads; static unsigned int s_numUDPWorkerThreads; static unsigned int s_numTCPWorkerThreads; + static unsigned int s_numTaskThreads; }; struct ThreadMSG diff --git a/pdns/recursordist/rec-rust-lib/table.py b/pdns/recursordist/rec-rust-lib/table.py index 3ef887079ffd..44aa754ab2be 100644 --- a/pdns/recursordist/rec-rust-lib/table.py +++ b/pdns/recursordist/rec-rust-lib/table.py @@ -2963,9 +2963,9 @@ "section": "recursor", "type": LType.Uint64, "default": "2", - "help": "Launch this number of threads", + "help": "Launch this number of worker threads", "doc": """ -Spawn this number of threads on startup. +Spawn this number of worker threads on startup. """, }, { @@ -2979,6 +2979,17 @@ """, "versionadded": "5.0.0", }, + { + "name": "taskthreads", + "section": "recursor", + "type": LType.Uint64, + "default": "1", + "help": "Launch this number of async task threads", + "doc": """ +Spawn this number of asynchronous task threads on startup. + """, + "versionadded": "5.5.0", + }, { "name": "trace", "section": "logging", From c3c3dbdd5ba0f383322c31260c0675c8025bd492 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Wed, 13 May 2026 13:23:51 +0200 Subject: [PATCH 10/11] Rework logic a bit to make it more resilient against failing cases Signed-off-by: Otto Moerbeek --- pdns/recursordist/rec-keepwarm.hh | 10 +++++++--- pdns/recursordist/rec-main.cc | 27 ++++++++++++++++++--------- pdns/recursordist/syncres.cc | 2 +- pdns/recursordist/syncres.hh | 2 ++ 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/pdns/recursordist/rec-keepwarm.hh b/pdns/recursordist/rec-keepwarm.hh index 03076e0b8e7f..24e54e91cd3a 100644 --- a/pdns/recursordist/rec-keepwarm.hh +++ b/pdns/recursordist/rec-keepwarm.hh @@ -69,13 +69,13 @@ public: { return d_queue; } - void modifyTTD(const DNSName& qname, uint16_t qtype, uint32_t ttd) + void modifyTTD(KeepWarmEntry& entry, uint32_t ttd) { - auto item = d_queue.find(std::tie(qname, qtype)); + auto item = d_queue.find(std::tie(entry.d_qname, entry.d_qtype)); if (item != d_queue.end()) { d_queue.modify(item, [ttd](rec::KeepWarmEntry& entry) { entry.d_ttd = ttd; }); } - + entry.d_ttd = ttd; } void emplace(const DNSName& name, uint16_t qtype) { @@ -93,6 +93,10 @@ public: { return d_queue.end(); } + [[nodiscard]] size_t size() const + { + return d_queue.size(); + } private: Queue d_queue; diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index 18ee539f1ebb..84f7c2239ca7 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -2482,21 +2482,21 @@ static time_t keepCacheWarm(const timeval& now, LocalStateHolder std::vector toBeHandled; - auto& sidx = lock->get().template get(); + const auto& sidx = lock->get().template get(); auto siter = sidx.begin(); - const int batchSize = 100; + const auto batchSize = std::min(static_cast(1000), lock->size()); const time_t specialTime = 1; const time_t cooldown = 60; const time_t almost = 5; - for (int i = 0; i < batchSize && siter != sidx.end(); i++, siter++) { + for (size_t i = 0; i < batchSize && siter != sidx.end(); i++, siter++) { + if (siter->d_ttd > now.tv_sec + almost) { break; } toBeHandled.emplace_back(*siter); } - for (auto& element : toBeHandled) { if (element.d_ttd == specialTime) { SyncRes resolver(now); @@ -2505,9 +2505,10 @@ static time_t keepCacheWarm(const timeval& now, LocalStateHolder resolver.setDoDNSSEC(g_dnssecmode != DNSSECMode::Off); resolver.setDNSSECValidationRequested(g_dnssecmode != DNSSECMode::Off && g_dnssecmode != DNSSECMode::ProcessNoValidate); std::vector ret; + int res = -1; const std::string msg = "Exception while resolving"; try { - resolver.beginResolve(element.d_qname, element.d_qtype, QClass::IN, ret, 0); + res = resolver.beginResolve(element.d_qname, element.d_qtype, QClass::IN, ret, 0); } catch (const PDNSException& e) { log->error(Logr::Warning, e.reason, msg, "exception", Logging::Loggable("PDNSException")); @@ -2534,15 +2535,23 @@ static time_t keepCacheWarm(const timeval& now, LocalStateHolder // not resolve yet. In both cases, pace the work. if (ret.size() > 0) { minttl = std::numeric_limits::max(); + bool haveAnswerRecord = false; for (const auto& record : ret) { + if (record.d_place == DNSResourceRecord::ANSWER) { + haveAnswerRecord = true; + } minttl = std::min(minttl, record.d_ttl); } + if (haveAnswerRecord && !haveFinalAnswer(element.d_qname, element.d_qtype, res, ret)) { + // Common cause: a record in the CNAME chain expired, setting the minttl will trigger a task push below + minttl = 0; + } } - lock->modifyTTD(element.d_qname, element.d_qtype, now.tv_sec + minttl); + lock->modifyTTD(element, now.tv_sec + minttl); } - else if (element.d_ttd == 0 || element.d_ttd <= now.tv_sec + almost) { + if (element.d_ttd <= now.tv_sec + almost) { // include non-initialized (0) case pushAlmostExpiredTask(element.d_qname, element.d_qtype, now.tv_sec + cooldown, ComboAddress("255.255.255.255"), true); - lock->modifyTTD(element.d_qname, element.d_qtype, specialTime); + lock->modifyTTD(element, specialTime); } } @@ -2643,7 +2652,7 @@ static void houseKeepingWork(Logr::log_t log) }); // TaskQueue is run always - runTasks(10, g_logCommonErrors); + runTasks(100, g_logCommonErrors); static PeriodicTask ztcTask{"ZTC", 60}; static map ztcStates; diff --git a/pdns/recursordist/syncres.cc b/pdns/recursordist/syncres.cc index 9e042e5873ce..366f25d3d57b 100644 --- a/pdns/recursordist/syncres.cc +++ b/pdns/recursordist/syncres.cc @@ -1794,7 +1794,7 @@ unsigned int SyncRes::getAdjustedRecursionBound() const return bound; } -static bool haveFinalAnswer(const DNSName& qname, QType qtype, int res, const vector& ret) +bool haveFinalAnswer(const DNSName& qname, QType qtype, int res, const vector& ret) { if (res != RCode::NoError) { return false; diff --git a/pdns/recursordist/syncres.hh b/pdns/recursordist/syncres.hh index 77075abde9b5..655444caeaef 100644 --- a/pdns/recursordist/syncres.hh +++ b/pdns/recursordist/syncres.hh @@ -990,6 +990,7 @@ bool primeHints(time_t now = time(nullptr)); using timebuf_t = std::array; const char* isoDateTimeMillis(const struct timeval& tval, timebuf_t& buf); +bool haveFinalAnswer(const DNSName& qname, QType qtype, int res, const vector& ret); struct WipeCacheResult { @@ -1012,3 +1013,4 @@ struct ThreadTimes return *this; } }; + From 27d208a99a78a204d39e7bc4147165189b33f835 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Wed, 13 May 2026 15:19:33 +0200 Subject: [PATCH 11/11] Add conversion functions (just to be complete) Signed-off-by: Otto Moerbeek --- pdns/recursordist/rec-rust-lib/cxxsupport.cc | 17 +++++++++-------- .../recursordist/rec-rust-lib/rust-bridge-in.rs | 1 + .../rec-rust-lib/rust/src/bridge.rs | 15 ++++++++++++++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/pdns/recursordist/rec-rust-lib/cxxsupport.cc b/pdns/recursordist/rec-rust-lib/cxxsupport.cc index e80f456236e3..97fe2d5252a4 100644 --- a/pdns/recursordist/rec-rust-lib/cxxsupport.cc +++ b/pdns/recursordist/rec-rust-lib/cxxsupport.cc @@ -546,7 +546,7 @@ static void processLine(const std::string& arg, FieldMap& map, bool mainFile) ::rust::String section; ::rust::String fieldname; ::rust::String type_name; - pdns::rust::settings::rec::Value rustvalue = {false, 0, 0.0, "", {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + pdns::rust::settings::rec::Value rustvalue = {false, 0, 0.0, "", {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; if (pdns::settings::rec::oldKVToBridgeStruct(var, val, section, fieldname, type_name, rustvalue)) { auto overriding = !mainFile && !incremental && !simpleRustType(type_name); auto [existing, inserted] = map.emplace(std::pair{std::pair{section, fieldname}, pdns::rust::settings::rec::OldStyle{section, fieldname, var, std::move(type_name), rustvalue, overriding}}); @@ -663,22 +663,23 @@ std::string pdns::settings::rec::defaultsToYaml(bool postProcess) rustvalue.u64_val = 24; map.emplace(std::pair{std::pair{section, name}, pdns::rust::settings::rec::OldStyle{section, name, name, type, std::move(rustvalue), false}}); }; - def("dnssec", "trustanchors", "Vec"); def("dnssec", "negative_trustanchors", "Vec"); def("dnssec", "trustanchorfile", "String"); def("dnssec", "trustanchorfile_interval", "u64"); - def("logging", "protobuf_servers", "Vec"); - def("logging", "outgoing_protobuf_servers", "Vec"); + def("dnssec", "trustanchors", "Vec"); + def("incoming", "proxymappings", "Vec"); def("logging", "dnstap_framestream_servers", "Vec"); def("logging", "dnstap_nod_framestream_servers", "Vec"); - def("recursor", "rpzs", "Vec"); - def("recursor", "sortlists", "Vec"); + def("logging", "outgoing_protobuf_servers", "Vec"); + def("logging", "protobuf_servers", "Vec"); + def("recordcache", "keepwarm", "Vec"); def("recordcache", "zonetocaches", "Vec"); def("recursor", "allowed_additional_qtypes", "Vec"); - def("incoming", "proxymappings", "Vec"); def("recursor", "forwarding_catalog_zones", "Vec"); - def("webservice", "listen", "Vec"); + def("recursor", "rpzs", "Vec"); + def("recursor", "sortlists", "Vec"); def("recursor", "tls_configurations", "Vec"); + def("webservice", "listen", "Vec"); // End of should be generated XXX // Convert the map to a vector, as CXX does not have any dictionary like support. diff --git a/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs b/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs index c1e8557a5855..db2e00286c95 100644 --- a/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs +++ b/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs @@ -414,6 +414,7 @@ struct Value { vec_forwardingcatalogzone_val: Vec, vec_incomingwsconfig_val: Vec, vec_outgoingtlsconfiguration_val: Vec, + vec_qnameandqtype_val: Vec, } struct OldStyle { diff --git a/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs b/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs index e8853230866c..b558d576ea80 100644 --- a/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs +++ b/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs @@ -800,7 +800,6 @@ impl IncomingWSConfig { } impl OutgoingTLSConfiguration { - fn to_yaml_map(&self) -> serde_yaml::Value { let mut map = serde_yaml::Mapping::new(); inserts(&mut map, "name", &self.name); @@ -875,6 +874,13 @@ impl OpenTelemetryTraceCondition { } impl QNameAndQType { + fn to_yaml_map(&self) -> serde_yaml::Value { + let mut map = serde_yaml::Mapping::new(); + inserts(&mut map, "qname", &self.qname); + inserts(&mut map, "qtype", &self.qtype); + serde_yaml::Value::Mapping(map) + } + pub fn validate(&self, field: &str) -> Result<(), ValidationError> { if self.qname.is_empty() { let msg = format!("{}: value may not be empty", field); @@ -1223,6 +1229,13 @@ pub fn map_to_yaml_string(vec: &Vec) -> Result" => { + let mut seq = serde_yaml::Sequence::new(); + for element in &entry.value.vec_qnameandqtype_val { + seq.push(element.to_yaml_map()); + } + serde_yaml::Value::Sequence(seq) + } other => serde_yaml::Value::String( "map_to_yaml_string: Unknown type: ".to_owned() + other, ),