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.

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
Recientes
- Recoger y bloquear IP addresses con ipset y Python
- Cómo cancelar tareas con Python Asynchronous IO (AsyncIO)
- Ejecutar un comando Docker dentro de un contenedor Cron Docker
- Creación de un Captcha con Flask, WTForms, SQLAlchemy, SQLite
- Multiprocessing, bloqueo de archivos, SQLite y pruebas
- Envío de mensajes a Slack mediante chat_postMessage
Más vistos
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando PyInstaller y Cython para crear un ejecutable de Python
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados