]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
Retry on test timeout (#1146)
authorBob Halley <halley@dnspython.org>
Sat, 12 Oct 2024 16:29:13 +0000 (09:29 -0700)
committerGitHub <noreply@github.com>
Sat, 12 Oct 2024 16:29:13 +0000 (09:29 -0700)
wrap all the tests that can time out

tests/test_async.py
tests/test_ddr.py
tests/test_doh.py
tests/test_query.py
tests/test_resolver.py
tests/util.py

index b6e7a451e56a081f1690d575ce0ef4c4b6a5e63c..9a19609cf84186779ba1ce3d25f5faf931bf263e 100644 (file)
@@ -187,6 +187,7 @@ class AsyncTests(unittest.TestCase):
     def async_run(self, afunc):
         return asyncio.run(afunc())
 
+    @tests.util.retry_on_timeout
     def testResolve(self):
         async def run():
             answer = await dns.asyncresolver.resolve("dns.google.", "A")
@@ -196,6 +197,7 @@ class AsyncTests(unittest.TestCase):
         self.assertTrue("8.8.8.8" in seen)
         self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testResolveAddress(self):
         async def run():
             return await dns.asyncresolver.resolve_address("8.8.8.8")
@@ -204,6 +206,7 @@ class AsyncTests(unittest.TestCase):
         dnsgoogle = dns.name.from_text("dns.google.")
         self.assertEqual(answer[0].target, dnsgoogle)
 
+    @tests.util.retry_on_timeout
     def testResolveName(self):
         async def run1():
             return await dns.asyncresolver.resolve_name("dns.google.")
@@ -250,6 +253,7 @@ class AsyncTests(unittest.TestCase):
             with self.assertRaises(dns.resolver.NoAnswer):
                 self.async_run(run5)
 
+    @tests.util.retry_on_timeout
     def testCanonicalNameNoCNAME(self):
         cname = dns.name.from_text("www.google.com")
 
@@ -258,6 +262,7 @@ class AsyncTests(unittest.TestCase):
 
         self.assertEqual(self.async_run(run), cname)
 
+    @tests.util.retry_on_timeout
     def testCanonicalNameCNAME(self):
         name = dns.name.from_text("www.dnspython.org")
         cname = dns.name.from_text("dmfrjf4ips8xa.cloudfront.net")
@@ -270,6 +275,7 @@ class AsyncTests(unittest.TestCase):
     @unittest.skipIf(
         _systemd_resolved_present or _is_docker, "systemd-resolved or docker in use"
     )
+    @tests.util.retry_on_timeout
     def testCanonicalNameDangling(self):
         name = dns.name.from_text("dangling-cname.dnspython.org")
         cname = dns.name.from_text("dangling-target.dnspython.org")
@@ -279,6 +285,7 @@ class AsyncTests(unittest.TestCase):
 
         self.assertEqual(self.async_run(run), cname)
 
+    @tests.util.retry_on_timeout
     def testZoneForName1(self):
         async def run():
             name = dns.name.from_text("www.dnspython.org.")
@@ -288,6 +295,7 @@ class AsyncTests(unittest.TestCase):
         zname = self.async_run(run)
         self.assertEqual(zname, ezname)
 
+    @tests.util.retry_on_timeout
     def testZoneForName2(self):
         async def run():
             name = dns.name.from_text("a.b.www.dnspython.org.")
@@ -297,6 +305,7 @@ class AsyncTests(unittest.TestCase):
         zname = self.async_run(run)
         self.assertEqual(zname, ezname)
 
+    @tests.util.retry_on_timeout
     def testZoneForName3(self):
         async def run():
             name = dns.name.from_text("dnspython.org.")
@@ -317,6 +326,7 @@ class AsyncTests(unittest.TestCase):
 
         self.assertRaises(dns.resolver.NotAbsolute, bad)
 
+    @tests.util.retry_on_timeout
     def testQueryUDP(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -334,6 +344,7 @@ class AsyncTests(unittest.TestCase):
             self.assertTrue("8.8.8.8" in seen)
             self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testQueryUDPWithSocket(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -358,6 +369,7 @@ class AsyncTests(unittest.TestCase):
             self.assertTrue("8.8.8.8" in seen)
             self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testQueryTCP(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -375,6 +387,7 @@ class AsyncTests(unittest.TestCase):
             self.assertTrue("8.8.8.8" in seen)
             self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testQueryTCPWithSocket(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -403,6 +416,7 @@ class AsyncTests(unittest.TestCase):
             self.assertTrue("8.8.4.4" in seen)
 
     @unittest.skipIf(not _ssl_available, "SSL not available")
+    @tests.util.retry_on_timeout
     def testQueryTLS(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -421,6 +435,7 @@ class AsyncTests(unittest.TestCase):
             self.assertTrue("8.8.4.4" in seen)
 
     @unittest.skipIf(not _ssl_available, "SSL not available")
+    @tests.util.retry_on_timeout
     def testQueryTLSWithContext(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -443,6 +458,7 @@ class AsyncTests(unittest.TestCase):
             self.assertTrue("8.8.4.4" in seen)
 
     @unittest.skipIf(not _ssl_available, "SSL not available")
+    @tests.util.retry_on_timeout
     def testQueryTLSWithSocket(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -474,6 +490,7 @@ class AsyncTests(unittest.TestCase):
             self.assertTrue("8.8.8.8" in seen)
             self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testQueryUDPFallback(self):
         for address in query_addresses:
             qname = dns.name.from_text(".")
@@ -485,6 +502,7 @@ class AsyncTests(unittest.TestCase):
             (_, tcp) = self.async_run(run)
             self.assertTrue(tcp)
 
+    @tests.util.retry_on_timeout
     def testQueryUDPFallbackNoFallback(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -496,6 +514,7 @@ class AsyncTests(unittest.TestCase):
             (_, tcp) = self.async_run(run)
             self.assertFalse(tcp)
 
+    @tests.util.retry_on_timeout
     def testUDPReceiveQuery(self):
         async def run():
             async with await self.backend.make_socket(
@@ -536,6 +555,7 @@ class AsyncTests(unittest.TestCase):
         self.assertRaises(dns.exception.Timeout, run)
 
     @unittest.skipIf(not dns.query._have_httpx, "httpx not available")
+    @tests.util.retry_on_timeout
     def testDOHGetRequest(self):
         async def run():
             nameserver_url = random.choice(KNOWN_ANYCAST_DOH_RESOLVER_URLS)
@@ -548,6 +568,7 @@ class AsyncTests(unittest.TestCase):
         self.async_run(run)
 
     @unittest.skipIf(not dns.query._have_httpx, "httpx not available")
+    @tests.util.retry_on_timeout
     def testDOHPostRequest(self):
         async def run():
             nameserver_url = random.choice(KNOWN_ANYCAST_DOH_RESOLVER_URLS)
@@ -560,6 +581,7 @@ class AsyncTests(unittest.TestCase):
         self.async_run(run)
 
     @unittest.skipIf(not dns.quic.have_quic, "aioquic not available")
+    @tests.util.retry_on_timeout
     def testDoH3GetRequest(self):
         async def run():
             nameserver_url = random.choice(KNOWN_ANYCAST_DOH3_RESOLVER_URLS)
@@ -577,6 +599,7 @@ class AsyncTests(unittest.TestCase):
         self.async_run(run)
 
     @unittest.skipIf(not dns.quic.have_quic, "aioquic not available")
+    @tests.util.retry_on_timeout
     def TestDoH3PostRequest(self):
         async def run():
             nameserver_url = random.choice(KNOWN_ANYCAST_DOH3_RESOLVER_URLS)
@@ -594,6 +617,7 @@ class AsyncTests(unittest.TestCase):
         self.async_run(run)
 
     @unittest.skipIf(not dns.quic.have_quic, "aioquic not available")
+    @tests.util.retry_on_timeout
     def TestDoH3QueryIP(self):
         async def run():
             nameserver_ip = "8.8.8.8"
@@ -610,6 +634,7 @@ class AsyncTests(unittest.TestCase):
         self.async_run(run)
 
     @unittest.skipIf(not dns.query._have_httpx, "httpx not available")
+    @tests.util.retry_on_timeout
     def testResolverDOH(self):
         async def run():
             res = dns.asyncresolver.Resolver(configure=False)
@@ -622,6 +647,7 @@ class AsyncTests(unittest.TestCase):
         self.async_run(run)
 
     @unittest.skipIf(not tests.util.have_ipv4(), "IPv4 not reachable")
+    @tests.util.retry_on_timeout
     def testResolveAtAddress(self):
         async def run():
             answer = await dns.asyncresolver.resolve_at("8.8.8.8", "dns.google.", "A")
@@ -632,6 +658,7 @@ class AsyncTests(unittest.TestCase):
         self.async_run(run)
 
     @unittest.skipIf(not tests.util.have_ipv4(), "IPv4 not reachable")
+    @tests.util.retry_on_timeout
     def testResolveAtName(self):
         async def run():
             answer = await dns.asyncresolver.resolve_at(
@@ -661,6 +688,7 @@ class AsyncioOnlyTests(unittest.TestCase):
     def async_run(self, afunc):
         return asyncio.run(afunc())
 
+    @tests.util.retry_on_timeout
     def testUseAfterTimeout(self):
         # Test #843 fix.
         async def run():
index ce38d0e94b40e414b9ec9a1241bc082a8e84a9ac..c7892d02512b45924777c35cbd7da010d38ce53d 100644 (file)
@@ -1,21 +1,20 @@
 # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
 
 import asyncio
-import time
 
 import pytest
 
 import dns.asyncbackend
 import dns.asyncresolver
-import dns.resolver
 import dns.nameserver
-
+import dns.resolver
 import tests.util
 
 
 @pytest.mark.skipif(
     not tests.util.is_internet_reachable(), reason="Internet not reachable"
 )
+@tests.util.retry_on_timeout
 def test_basic_ddr_sync():
     for nameserver in ["1.1.1.1", "8.8.8.8"]:
         res = dns.resolver.Resolver(configure=False)
@@ -29,6 +28,7 @@ def test_basic_ddr_sync():
 @pytest.mark.skipif(
     not tests.util.is_internet_reachable(), reason="Internet not reachable"
 )
+@tests.util.retry_on_timeout
 def test_basic_ddr_async():
     async def run():
         dns.asyncbackend._default_backend = None
index f5d413085e935398183aa9841ea5ccc1376ce482..2743dee4a94632682c668d8bece7e86360d90ca0 100644 (file)
@@ -18,6 +18,8 @@ import random
 import socket
 import unittest
 
+import dns.exception
+
 try:
     import ssl
 
@@ -88,6 +90,7 @@ class DNSOverHTTPSTestCaseHttpx(unittest.TestCase):
     def tearDown(self):
         self.session.close()
 
+    @tests.util.retry_on_timeout
     def test_get_request(self):
         nameserver_url = random.choice(KNOWN_ANYCAST_DOH_RESOLVER_URLS)
         q = dns.message.make_query("example.com.", dns.rdatatype.A)
@@ -101,6 +104,7 @@ class DNSOverHTTPSTestCaseHttpx(unittest.TestCase):
         )
         self.assertTrue(q.is_response(r))
 
+    @tests.util.retry_on_timeout
     def test_post_request(self):
         nameserver_url = random.choice(KNOWN_ANYCAST_DOH_RESOLVER_URLS)
         q = dns.message.make_query("example.com.", dns.rdatatype.A)
@@ -114,6 +118,7 @@ class DNSOverHTTPSTestCaseHttpx(unittest.TestCase):
         )
         self.assertTrue(q.is_response(r))
 
+    @tests.util.retry_on_timeout
     def test_build_url_from_ip(self):
         self.assertTrue(resolver_v4_addresses or resolver_v6_addresses)
         if resolver_v4_addresses:
@@ -159,12 +164,14 @@ class DNSOverHTTPSTestCaseHttpx(unittest.TestCase):
     #         )
     #         self.assertTrue(q.is_response(r))
 
+    @tests.util.retry_on_timeout
     def test_new_session(self):
         nameserver_url = random.choice(KNOWN_ANYCAST_DOH_RESOLVER_URLS)
         q = dns.message.make_query("example.com.", dns.rdatatype.A)
         r = dns.query.https(q, nameserver_url, timeout=4)
         self.assertTrue(q.is_response(r))
 
+    @tests.util.retry_on_timeout
     def test_resolver(self):
         res = dns.resolver.Resolver(configure=False)
         res.nameservers = ["https://dns.google/dns-query"]
@@ -173,6 +180,7 @@ class DNSOverHTTPSTestCaseHttpx(unittest.TestCase):
         self.assertTrue("8.8.8.8" in seen)
         self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def test_padded_get(self):
         nameserver_url = random.choice(KNOWN_PAD_AWARE_DOH_RESOLVER_URLS)
         q = dns.message.make_query("example.com.", dns.rdatatype.A, use_edns=0, pad=128)
@@ -194,6 +202,7 @@ class DNSOverHTTPSTestCaseHttpx(unittest.TestCase):
     "Aioquic cannot be imported; no DNS over HTTP3 (DOH3)",
 )
 class DNSOverHTTP3TestCase(unittest.TestCase):
+    @tests.util.retry_on_timeout
     def testDoH3GetRequest(self):
         nameserver_url = random.choice(KNOWN_ANYCAST_DOH3_RESOLVER_URLS)
         q = dns.message.make_query("dns.google.", dns.rdatatype.A)
@@ -207,6 +216,7 @@ class DNSOverHTTP3TestCase(unittest.TestCase):
         )
         self.assertTrue(q.is_response(r))
 
+    @tests.util.retry_on_timeout
     def testDoH3PostRequest(self):
         nameserver_url = random.choice(KNOWN_ANYCAST_DOH3_RESOLVER_URLS)
         q = dns.message.make_query("dns.google.", dns.rdatatype.A)
@@ -220,6 +230,7 @@ class DNSOverHTTP3TestCase(unittest.TestCase):
         )
         self.assertTrue(q.is_response(r))
 
+    @tests.util.retry_on_timeout
     def test_build_url_from_ip(self):
         self.assertTrue(resolver_v4_addresses or resolver_v6_addresses)
         if resolver_v4_addresses:
index dee6d3b20b00797cf1ceab2f1f3a62e1482819ac..e6b388a0e2b07292f60841f481476e55bd9b0828 100644 (file)
@@ -66,6 +66,7 @@ keyring = dns.tsigkeyring.from_text({"name": "tDz6cfXXGtNivRpQ98hr6A=="})
 
 @unittest.skipIf(not tests.util.is_internet_reachable(), "Internet not reachable")
 class QueryTests(unittest.TestCase):
+    @tests.util.retry_on_timeout
     def testQueryUDP(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -79,6 +80,7 @@ class QueryTests(unittest.TestCase):
             self.assertTrue("8.8.8.8" in seen)
             self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testQueryUDPWithSocket(self):
         for address in query_addresses:
             with socket.socket(
@@ -96,6 +98,7 @@ class QueryTests(unittest.TestCase):
                 self.assertTrue("8.8.8.8" in seen)
                 self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testQueryTCP(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -109,6 +112,7 @@ class QueryTests(unittest.TestCase):
             self.assertTrue("8.8.8.8" in seen)
             self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testQueryTCPWithSocket(self):
         for address in query_addresses:
             with socket.socket(
@@ -130,6 +134,7 @@ class QueryTests(unittest.TestCase):
                 self.assertTrue("8.8.4.4" in seen)
 
     @unittest.skipUnless(have_ssl, "No SSL support")
+    @tests.util.retry_on_timeout
     def testQueryTLS(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -144,6 +149,7 @@ class QueryTests(unittest.TestCase):
             self.assertTrue("8.8.4.4" in seen)
 
     @unittest.skipUnless(have_ssl, "No SSL support")
+    @tests.util.retry_on_timeout
     def testQueryTLSWithContext(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -160,6 +166,7 @@ class QueryTests(unittest.TestCase):
             self.assertTrue("8.8.4.4" in seen)
 
     @unittest.skipUnless(have_ssl, "No SSL support")
+    @tests.util.retry_on_timeout
     def testQueryTLSWithSocket(self):
         for address in query_addresses:
             with socket.socket(
@@ -186,6 +193,7 @@ class QueryTests(unittest.TestCase):
                     self.assertTrue("8.8.4.4" in seen)
 
     @unittest.skipUnless(have_ssl, "No SSL support")
+    @tests.util.retry_on_timeout
     def testQueryTLSwithPadding(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -206,6 +214,7 @@ class QueryTests(unittest.TestCase):
                     has_pad = True
             self.assertTrue(has_pad)
 
+    @tests.util.retry_on_timeout
     def testQueryUDPFallback(self):
         for address in query_addresses:
             qname = dns.name.from_text(".")
@@ -213,6 +222,7 @@ class QueryTests(unittest.TestCase):
             (_, tcp) = dns.query.udp_with_fallback(q, address, timeout=4)
             self.assertTrue(tcp)
 
+    @tests.util.retry_on_timeout
     def testQueryUDPFallbackWithSocket(self):
         for address in query_addresses:
             af = dns.inet.af_for_address(address)
@@ -230,6 +240,7 @@ class QueryTests(unittest.TestCase):
                     )
                     self.assertTrue(tcp)
 
+    @tests.util.retry_on_timeout
     def testQueryUDPFallbackNoFallback(self):
         for address in query_addresses:
             qname = dns.name.from_text("dns.google.")
@@ -237,6 +248,7 @@ class QueryTests(unittest.TestCase):
             (_, tcp) = dns.query.udp_with_fallback(q, address, timeout=2)
             self.assertFalse(tcp)
 
+    @tests.util.retry_on_timeout
     def testUDPReceiveQuery(self):
         with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as listener:
             listener.bind(("127.0.0.1", 0))
index 8694335cc56bb20086cba5df7c0605ed32390fef..3e37648b9020a775c18063b906882a18fda65333 100644 (file)
@@ -630,18 +630,21 @@ keyname = dns.name.from_text("keyname")
 
 @unittest.skipIf(not tests.util.is_internet_reachable(), "Internet not reachable")
 class LiveResolverTests(unittest.TestCase):
+    @tests.util.retry_on_timeout
     def testZoneForName1(self):
         name = dns.name.from_text("www.dnspython.org.")
         ezname = dns.name.from_text("dnspython.org.")
         zname = dns.resolver.zone_for_name(name)
         self.assertEqual(zname, ezname)
 
+    @tests.util.retry_on_timeout
     def testZoneForName2(self):
         name = dns.name.from_text("a.b.www.dnspython.org.")
         ezname = dns.name.from_text("dnspython.org.")
         zname = dns.resolver.zone_for_name(name)
         self.assertEqual(zname, ezname)
 
+    @tests.util.retry_on_timeout
     def testZoneForName3(self):
         ezname = dns.name.from_text("dnspython.org.")
         zname = dns.resolver.zone_for_name("dnspython.org.")
@@ -654,23 +657,27 @@ class LiveResolverTests(unittest.TestCase):
 
         self.assertRaises(dns.resolver.NotAbsolute, bad)
 
+    @tests.util.retry_on_timeout
     def testResolve(self):
         answer = dns.resolver.resolve("dns.google.", "A")
         seen = set([rdata.address for rdata in answer])
         self.assertTrue("8.8.8.8" in seen)
         self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testResolveTCP(self):
         answer = dns.resolver.resolve("dns.google.", "A", tcp=True)
         seen = set([rdata.address for rdata in answer])
         self.assertTrue("8.8.8.8" in seen)
         self.assertTrue("8.8.4.4" in seen)
 
+    @tests.util.retry_on_timeout
     def testResolveAddress(self):
         answer = dns.resolver.resolve_address("8.8.8.8")
         dnsgoogle = dns.name.from_text("dns.google.")
         self.assertEqual(answer[0].target, dnsgoogle)
 
+    @tests.util.retry_on_timeout
     def testResolveName(self):
         answers = dns.resolver.resolve_name("dns.google.")
         seen = set(answers.addresses())
@@ -700,6 +707,7 @@ class LiveResolverTests(unittest.TestCase):
             with self.assertRaises(dns.resolver.NoAnswer):
                 dns.resolver.resolve_name(dns.reversename.from_address("8.8.8.8"))
 
+    @tests.util.retry_on_timeout
     @patch.object(dns.message.Message, "use_edns")
     def testResolveEdnsOptions(self, message_use_edns_mock):
         resolver = dns.resolver.Resolver()
@@ -708,12 +716,14 @@ class LiveResolverTests(unittest.TestCase):
         resolver.resolve("dns.google.", "A")
         assert {"options": options} in message_use_edns_mock.call_args
 
+    @tests.util.retry_on_timeout
     def testResolveNodataException(self):
         def bad():
             dns.resolver.resolve("dnspython.org.", "SRV")
 
         self.assertRaises(dns.resolver.NoAnswer, bad)
 
+    @tests.util.retry_on_timeout
     def testResolveNodataAnswer(self):
         qname = dns.name.from_text("dnspython.org")
         qclass = dns.rdataclass.from_text("IN")
@@ -726,6 +736,7 @@ class LiveResolverTests(unittest.TestCase):
             ),
         )
 
+    @tests.util.retry_on_timeout
     def testResolveNXDOMAIN(self):
         qname = dns.name.from_text("nxdomain.dnspython.org")
         qclass = dns.rdataclass.from_text("IN")
@@ -742,6 +753,7 @@ class LiveResolverTests(unittest.TestCase):
             self.assertGreaterEqual(len(nx.responses()), 1)
 
     @unittest.skipIf(not tests.util.have_ipv4(), "IPv4 not reachable")
+    @tests.util.retry_on_timeout
     def testResolveCacheHit(self):
         res = dns.resolver.Resolver(configure=False)
         res.nameservers = ["8.8.8.8"]
@@ -754,6 +766,7 @@ class LiveResolverTests(unittest.TestCase):
         self.assertIs(answer2, answer1)
 
     @unittest.skipIf(not tests.util.have_ipv4(), "IPv4 not reachable")
+    @tests.util.retry_on_timeout
     def testTLSNameserver(self):
         res = dns.resolver.Resolver(configure=False)
         res.nameservers = [dns.nameserver.DoTNameserver("8.8.8.8", 853)]
index 9f0d3f464cd95b451db77e425d03e3999d3bd0b6..f029fb02b114df65dd3d0288e488e012ed01268f 100644 (file)
 # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 import enum
+import functools
 import inspect
 import os
 
+import dns.exception
 import dns.message
 import dns.name
 import dns.query
@@ -131,3 +133,21 @@ def is_docker() -> bool:
         return os.path.isfile("/.dockerenv")
     except Exception:
         return False
+
+
+def retry_on_timeout(f):
+    @functools.wraps(f)
+    def wrapper(*args, **kwargs):
+        bad = True
+        for i in range(3):
+            try:
+                print("TRY", i, "for", f.__name__)
+                f(*args, **kwargs)
+                bad = False
+                break
+            except dns.exception.Timeout:
+                pass
+        if bad:
+            raise dns.exception.Timeout
+
+    return wrapper