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

AIOHTTP: Detección del tiempo de espera del DNS con servidores de nombres personalizados

Usando 'Client tracing' podemos generar las variables dns_cache_miss y host_resolved para determinar si se ha producido una excepción en el resolver.

27 julio 2022 Actualizado 27 julio 2022
En Async
post main image
https://www.pexels.com/nl-nl/@fotios-photos

Cuando se utiliza AIOHTTP para obtener datos de una página web en Internet, probablemente se utiliza un tiempo de espera para limitar el tiempo máximo de espera.

Si está utilizando un nombre de dominio, la dirección IP debe ser resuelta. Sin usar un resolvedor separado usted depende del sistema operativo subyacente. Cualquier error se propaga a su aplicación.

Yo no quería esta dependencia y especifiqué los servidores de nombres yo mismo, usando el AsyncResolver y TCPConnector.

Ahora supongamos que se produce un timeout. ¿Cómo podemos saber si el tiempo de espera es causado por el resolver o por la conexión al servidor remoto?

El problema

La petición AIOHTTP consta de dos partes:

  • Resolver el DNS
  • Recibir datos
    |----------------- request ----------------->|

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

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

Con AIOHTTP podemos especificar un tiempo máximo para la petición. Cuando este tiempo expira, se lanza una excepción TimeoutError .

Pero esto es para toda la petición. No hay una excepción separada para el tiempo de espera del resolver. De nuevo, ¿cómo sabemos si el tiempo de espera es causado por el resolver DNS o por el servidor remoto?

El rastreo del cliente al rescate

Afortunadamente, podemos seguir el flujo de ejecución de una petición adjuntando coroutines de escucha a las señales proporcionadas por la instancia TraceConfig , que puede utilizarse como parámetro para el constructor ClientSession .

Si nos fijamos en el AIOHTTP 'Tracing Reference', ver enlaces más abajo, y ampliamos en 'Connection acquiring' y 'DNS resolving', entonces vemos que necesitamos las siguientes coroutines:

  • on_request_start
  • on_dns_cache_miss
  • on_dns_resolvehost_end

Si se produce un timeout y se llama a 'on_dns_cache_miss' y no se llama a 'on_dns_resolvehost_end', podemos suponer que el timeout está causado por el resolver.

Para poner en marcha las coroutines, creamos un objeto TraceConfig y adjuntamos las coroutines. Todo lo que hacemos en estas coroutines es medir el tiempo desde el inicio de la petición y almacenarlo en nuestro diccionario 'trace_result', pasado como contexto, con valores iniciales None:

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

El código

Cuando se produce una excepción, primero comprobamos si el error es un TimeoutError. Si este es el caso, comprobamos si la excepción se ha producido en el resolver utilizando 'cache_miss' y 'host_resolved'. Elija el resolver que funciona con los servidores de nombres de quad9.net, o simplemente utilice alguna dirección 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))

Más errores de resolución

Hay más errores de resolución y AIOHTTP no nos ayuda aquí también, por ejemplo:

  • Could not contact DNS servers
  • ConnectionRefusedError

El primero tiene seguramente que ver con el resolver, pero el ConnectionRefusedError, puede originarse por ambas acciones en la petición.

Resumen

Quiero saber si una excepción planteada proviene del resolver o de otra parte de la petición. Si es el resolver, entonces puedo marcar este resolver (temporal) inválido y usar otro.

Esperaba que las excepciones AIOHHTP me dieran toda la información, pero parece que no es así. Quizás algún día se implemente, pero por el momento debo hacer el trabajo sucio yo mismo. Por lo demás, AIOHTTP es un paquete muy bonito.

Enlaces / créditos

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.