angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

AIOHTTP : Détection du timeout DNS avec des serveurs de noms personnalisés

En utilisant 'Client tracing', nous pouvons générer les variables dns_cache_miss et host_resolved pour déterminer si une exception a été levée dans le résolveur.

27 juillet 2022 Mise à jour 27 juillet 2022
Dans Async
post main image
https://www.pexels.com/nl-nl/@fotios-photos

Lorsque vous utilisez AIOHTTP pour extraire des données d'une page Web sur Internet, vous utilisez probablement un délai d'attente pour limiter le temps d'attente maximum.

Si vous utilisez un nom de domaine, l'adresse IP doit être résolue. Si vous n'utilisez pas un résolveur séparé, vous dépendez du système d'exploitation sous-jacent. Toute erreur se propage à votre application.

Je n'ai pas voulu de cette dépendance et j'ai spécifié moi-même les serveurs de noms, en utilisant les AsyncResolver et TCPConnector.

Supposons maintenant qu'un délai d'attente se produise. Comment savoir si le délai d'attente est causé par le résolveur ou par la connexion au serveur distant ?

Le problème

La requête AIOHTTP se compose de deux parties :

  • Résoudre le DNS
  • Réception des données
    |----------------- request ----------------->|

    |---- resolve DNS --->|---- receive data --->|

    |                     |                      |
----+---------------------+----------------------+---> t
  start

Avec AIOHTTP , nous pouvons spécifier une durée maximale pour la requête. Lorsque ce délai expire, une exception TimeoutError est levée.

Mais cette exception concerne l'ensemble de la demande. Il n'y a pas d'exception distincte pour un dépassement de délai du résolveur. Encore une fois, comment savoir si le délai d'attente est causé par le résolveur DNS ou par le serveur distant ?

Le traçage du client à la rescousse

Heureusement, nous pouvons suivre le flux d'exécution d'une requête en attachant des coroutines d'écoute aux signaux fournis par l'instance TraceConfig , qui peut être utilisée comme paramètre pour le constructeur ClientSession .

Si nous regardons la AIOHTTP 'Tracing Reference', voir les liens ci-dessous, et zoomons sur 'Connection acquiring' et 'DNS resolving', nous voyons que nous avons besoin des coroutines suivantes :

  • on_request_start
  • on_dns_cache_miss
  • on_dns_resolvehost_end

Lorsqu'un timeout se produit et que 'on_dns_cache_miss' a été appelé et que 'on_dns_resolvehost_end' n'a pas été appelé, nous pouvons supposer que le timeout est causé par le resolver.

Pour faire fonctionner les coroutines, nous créons un objet TraceConfig et attachons les coroutines. Tout ce que nous faisons dans ces coroutines est de mesurer le temps depuis le début de la requête et de le stocker dans notre dictionnaire 'trace_result', transmis comme contexte, avec les valeurs initiales None :

trace_results = {
	'on_dns_cache_hit': None,
	'on_dns_cache_miss': None,
	'on_dns_resolvehost_end': None,
}

Le code

Lorsqu'une exception est levée, nous vérifions d'abord si l'erreur est une TimeoutError. Si c'est le cas, nous vérifions si l'exception s'est produite dans le résolveur en utilisant 'cache_miss' et 'host_resolved'. Choisissez le résolveur qui fonctionne avec les serveurs de noms de quad9.net, ou utilisez simplement une adresse IP.

import asyncio
import aiohttp
from aiohttp.resolver import AsyncResolver
import socket
import sys
import traceback


class Runner:

    def __init__(self):
        pass

    async def on_request_start(self, session, trace_config_ctx, params):
        trace_config_ctx.start = asyncio.get_event_loop().time()

    async def on_dns_cache_miss(self, session, trace_config_ctx, params):
        elapsed = asyncio.get_event_loop().time() - trace_config_ctx.start
        trace_config_ctx.trace_request_ctx['on_dns_cache_miss'] = elapsed

    async def on_dns_resolvehost_end(self, session, trace_config_ctx, params):
        elapsed = asyncio.get_event_loop().time() - trace_config_ctx.start
        trace_config_ctx.trace_request_ctx['on_dns_resolvehost_end'] = elapsed

    async def get_trace_config(self):
        trace_config = aiohttp.TraceConfig()
        trace_config.on_request_start.append(self.on_request_start)
        trace_config.on_dns_cache_miss.append(self.on_dns_cache_miss)
        trace_config.on_dns_resolvehost_end.append(self.on_dns_resolvehost_end)
        trace_results = {
            'on_dns_cache_hit': None,
            'on_dns_cache_miss': None,
            'on_dns_resolvehost_end': None,
        }
        return trace_config, trace_results

    async def run(self, url):
        # quad9.net dns server
        resolver = AsyncResolver(nameservers=['9.9.9.9', '149.112.112.112'])
        # ip address of www.example.com, using this causes a resolver timeout
        resolver = AsyncResolver(nameservers=['93.184.216.34'])

        connector = aiohttp.TCPConnector(
            family=socket.AF_INET,
            resolver=resolver,
        )

        trace_config, trace_results = await self.get_trace_config()

        error = None
        e_str = None
        try:
            async with aiohttp.ClientSession(
                connector=connector,
                timeout=aiohttp.ClientTimeout(total=.5),
                trace_configs=[trace_config],
            ) as session:
                async with session.get(
                    url,
                    trace_request_ctx=trace_results,
                ) as client_response:
                    html = await client_response.text()

        except Exception as e:
            print(traceback.format_exc())
            error = type(e).__name__
            e_str = str(e)
            print('url = {}'.format(url))
            print('error = {}'.format(type(e).__name__))
            print('e_str = {}'.format(e_str))
            print('e.args = {}'.format(e.args))

        finally:
            print('url = {}'.format(url))
            for k, v in trace_results.items():
                print('trace_results: {} = {}'.format(k, v))

        dns_cache_miss = True if trace_results['on_dns_cache_miss'] else False
        host_resolved = True if trace_results['on_dns_resolvehost_end'] else False

        if error == 'TimeoutError':
            if dns_cache_miss and not host_resolved:
                error = 'DNSTimeoutError'

        print('error = {}, e_str = {}'.format(error, e_str))

if __name__=='__main__':
    # 'fast' website
    url = 'http://www.example.com'
    # 'slow' website
    url = 'http://www.imdb.com'
    runner = Runner()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(runner.run(url))

Autres erreurs de résolution

Il existe d'autres erreurs de résolveur et AIOHTTP ne nous aide pas ici non plus, par exemple :

  • Could not contact DNS servers
  • ConnectionRefusedError

Le premier a certainement à voir avec le résolveur, mais le ConnectionRefusedError, peut provenir des deux actions dans la requête.

Résumé

Je veux savoir si une exception soulevée provient du résolveur ou d'une autre partie de la requête. Si c'est le résolveur, alors je peux marquer ce résolveur (temporaire) comme invalide et en utiliser un autre.

J'espérais que les exceptions AIOHHTP me donneraient toutes les informations, mais cela ne semble pas être le cas. Peut-être qu'un jour cela sera implémenté, mais pour le moment je dois faire le sale boulot moi-même. À part cela, AIOHTTP est un très bon paquetage !

Liens / crédits

AIOHTTP - Client exceptions
https://docs.aiohttp.org/en/stable/client_reference.html?highlight=exceptions#client-exceptions

AIOHTTP - Tracing Reference
https://docs.aiohttp.org/en/stable/tracing_reference.html

Monitoring network calls in Python using TIG stack
https://calendar.perfplanet.com/2020/monitoring-network-calls-in-python-using-tig-stack

En savoir plus...

Aiohttp Async

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.