diff --git a/docs/lua-records/functions.rst b/docs/lua-records/functions.rst index 45a63a1c258b..af02b2eaf45a 100644 --- a/docs/lua-records/functions.rst +++ b/docs/lua-records/functions.rst @@ -67,6 +67,7 @@ Record creation functions - ``timeout``: Maximum time in seconds that you allow the check to take (default 2) - ``interval``: Time interval between two checks, in seconds. Defaults to :ref:`setting-lua-health-checks-interval` if not specified. - ``minimumFailures``: The number of unsuccessful checks in a row required to mark the address as down. Defaults to 1 if not specified, i.e. report as down on the first unsuccessful check. + - ``failOnIncompleteCheck``: if set to "true", return SERVFAIL instead of applying ``backupSelector``, if none of the addresses has completed their background health check yet. .. function:: ifurlup(url, addresses[, options]) @@ -93,6 +94,7 @@ Record creation functions - ``useragent``: Set the HTTP "User-Agent" header in the requests. By default it is set to "PowerDNS Authoritative Server" - ``byteslimit``: Limit the maximum download size to ``byteslimit`` bytes (default 0 meaning no limit). - ``minimumFailures``: The number of unsuccessful checks in a row required to mark the address as down. Defaults to 1 if not specified, i.e. report as down on the first unsuccessful check. + - ``failOnIncompleteCheck``: if set to "true", return SERVFAIL instead of applying ``backupSelector``, if none of the addresses has completed their background health check yet. An example of a list of address sets: diff --git a/pdns/lua-record.cc b/pdns/lua-record.cc index f450cc0a0c59..6268d6ec2eb4 100644 --- a/pdns/lua-record.cc +++ b/pdns/lua-record.cc @@ -320,6 +320,13 @@ class IsUpOracle } }; +// The return value of this function can be one of three sets of values: +// - positive integer: the target is up, the return value is its weight. +// (1 if weights are not used) +// - zero: the target is down. +// - negative integer: the check for this target has not completed yet. +// (this value is only reported if the failOnIncompleteCheck option is +// set, otherwise zero will be returned) //NOLINTNEXTLINE(readability-identifier-length) int IsUpOracle::isUp(const CheckDesc& cd) { @@ -349,6 +356,14 @@ int IsUpOracle::isUp(const CheckDesc& cd) (*statuses)[cd] = std::make_unique(now); } } + // If explicitly asked to fail on incomplete checks, report this (as + // a negative value). + static const std::string foic{"failOnIncompleteCheck"}; + if (cd.opts.count(foic) != 0) { + if (cd.opts.at(foic) == "true") { + return -1; + } + } return 0; } @@ -882,7 +897,7 @@ static std::string pickConsistentWeightedHashed(const ComboAddress& bestwho, con return {}; } -static vector genericIfUp(const boost::variant& ips, boost::optional options, const std::function& upcheckf, uint16_t port = 0) +static vector genericIfUp(const boost::variant& ips, boost::optional options, const std::function& upcheckf, uint16_t port = 0) { vector > candidates; opts_t opts; @@ -891,12 +906,17 @@ static vector genericIfUp(const boost::variant& candidates = convMultiComboAddressList(ips, port); + bool incompleteCheck{true}; for(const auto& unit : candidates) { vector available; for(const auto& c : unit) { - if (upcheckf(c, opts)) { + int status = upcheckf(c, opts); + if (status > 0) { available.push_back(c); } + if (status >= 0) { + incompleteCheck = false; + } } if(!available.empty()) { vector res = useSelector(getOptionValue(options, "selector", "random"), s_lua_record_ctx->bestwho, available); @@ -904,7 +924,12 @@ static vector genericIfUp(const boost::variant& } } - // All units down, apply backupSelector on all candidates + // All units down or have not completed their checks yet. + if (incompleteCheck) { + throw std::runtime_error("if{url,port}up health check has not completed yet"); + } + + // Apply backupSelector on all candidates vector ret{}; for(const auto& unit : candidates) { ret.insert(ret.end(), unit.begin(), unit.end()); @@ -1194,7 +1219,7 @@ static void setupLuaRecords(LuaContext& lua) // NOLINT(readability-function-cogn port = std::max(port, 0); port = std::min(port, static_cast(std::numeric_limits::max())); - auto checker = [](const ComboAddress& addr, const opts_t& opts) { + auto checker = [](const ComboAddress& addr, const opts_t& opts) -> int { return g_up.isUp(addr, opts); }; return genericIfUp(ips, options, checker, port); @@ -1210,6 +1235,7 @@ static void setupLuaRecords(LuaContext& lua) // NOLINT(readability-function-cogn ca_unspec.sin4.sin_family=AF_UNSPEC; // ipurls: { { ["192.0.2.1"] = "https://example.com", ["192.0.2.2"] = "https://example.com/404" } } + bool incompleteCheck{true}; for (const auto& [count, unitmap] : ipurls) { // unitmap: 1 = { ["192.0.2.1"] = "https://example.com", ["192.0.2.2"] = "https://example.com/404" } vector available; @@ -1218,9 +1244,13 @@ static void setupLuaRecords(LuaContext& lua) // NOLINT(readability-function-cogn // unit: ["192.0.2.1"] = "https://example.com" ComboAddress ip(ipStr); candidates.push_back(ip); - if (g_up.isUp(ca_unspec, url, opts)) { + int status = g_up.isUp(ca_unspec, url, opts); + if (status > 0) { available.push_back(ip); } + if (status >= 0) { + incompleteCheck = false; + } } if(!available.empty()) { vector res = useSelector(getOptionValue(options, "selector", "random"), s_lua_record_ctx->bestwho, available); @@ -1228,7 +1258,12 @@ static void setupLuaRecords(LuaContext& lua) // NOLINT(readability-function-cogn } } - // All units down, apply backupSelector on all candidates + // All units down or have not completed their checks yet. + if (incompleteCheck) { + throw std::runtime_error("ifexturlup health check has not completed yet"); + } + + // Apply backupSelector on all candidates vector res = useSelector(getOptionValue(options, "backupSelector", "random"), s_lua_record_ctx->bestwho, candidates); return convComboAddressListToString(res); }); @@ -1237,7 +1272,7 @@ static void setupLuaRecords(LuaContext& lua) // NOLINT(readability-function-cogn const boost::variant& ips, boost::optional options) { - auto checker = [&url](const ComboAddress& addr, const opts_t& opts) { + auto checker = [&url](const ComboAddress& addr, const opts_t& opts) -> int { return g_up.isUp(addr, url, opts); }; return genericIfUp(ips, options, checker); @@ -1287,7 +1322,7 @@ static void setupLuaRecords(LuaContext& lua) // NOLINT(readability-function-cogn lua.writeFunction("pickrandomsample", [](int n, const iplist_t& ips) { vector items = convStringList(ips); - return pickRandomSample(n, items); + return pickRandomSample(n, items); }); lua.writeFunction("pickhashed", [](const iplist_t& ips) { @@ -1461,7 +1496,7 @@ static void setupLuaRecords(LuaContext& lua) // NOLINT(readability-function-cogn lua.writeFunction("all", [](const vector< pair >& ips) { vector result; - result.reserve(ips.size()); + result.reserve(ips.size()); for(const auto& ip : ips) { result.emplace_back(ip.second); diff --git a/regression-tests.auth-py/test_LuaRecords.py b/regression-tests.auth-py/test_LuaRecords.py index 3448f22eefa3..1e2dd47b06f3 100644 --- a/regression-tests.auth-py/test_LuaRecords.py +++ b/regression-tests.auth-py/test_LuaRecords.py @@ -112,6 +112,11 @@ class BaseLuaTest(AuthTest): "return ifurlup('http://www.lua.org:8080/', " "USAips, settings) ") +usa-failincomplete IN LUA A ( ";settings={{stringmatch='Programming in Lua', failOnIncompleteCheck='true'}} " + "USAips={{'192.168.42.105'}}" + "return ifurlup('http://www.lua.org:8080/', " + "USAips, settings) ") + mix.ifurlup IN LUA A ("ifurlup('http://www.other.org:8080/ping.json', " "{{ '192.168.42.101', '{prefix}.101' }}, " "{{ stringmatch='pong' }}) ") @@ -1338,6 +1343,30 @@ def testIfurlupInterval(self): self.assertAnyRRsetInAnswer(res, reachable_rrs) self.assertNoneRRsetInAnswer(res, unreachable_rrs) + def testIfurlupFailOnIncompleteCheck(self): + """ + Simple ifurlup() test with failOnIncompleteCheck option set. + """ + ips = ['192.168.42.105'] + all_rrs = [] + for ip in ips: + rr = dns.rrset.from_text('usa-failincomplete.example.org.', 0, dns.rdataclass.IN, 'A', ip) + all_rrs.append(rr) + + query = dns.message.make_query('usa-failincomplete.example.org', 'A') + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.SERVFAIL) + + # The above request being sent at time T, the following events occur: + # T+00: SERVFAIL returned as no data available yet + # T+00: checker thread starts + # T+02: 192.168.42.105 found down and marked as such + + time.sleep(3) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, all_rrs) + if __name__ == '__main__': unittest.main() exit(0)