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.
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
Récent
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
- Transfert de données sécurisé grâce au cryptage à Public Key et à pyNaCl
- rqlite : une alternative à haute disponibilité et dist distribuée SQLite
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow