Session et essais répétés : réessayer sans insister
Réutiliser une connexion et des en-têtes avec requests.Session(), réessayer un appel raté avec une boucle for et time.sleep, doubler la pause à chaque essai, et savoir pourquoi on s'arrête après quelques essais.
Mis à jour le
À recouper avec le cours. Cette leçon a été écrite à partir du plan du chapitre, avant relecture avec les supports. En cas de doute, le cours fait foi.
Pourquoi on voit ça
Un réseau a des ratés : un paquet perdu, un serveur qui redémarre, une seconde de surcharge. Si l’outil alerte toute l’équipe au premier Timeout, il crie au loup. La parade : réessayer deux ou trois fois avant de déclarer un serveur down.
Mais réessayer a un coût. Il faut le faire efficacement (une Session), poliment (une pause qui grandit) et pas éternellement (un nombre d’essais fixé).
requests.Session()
requests.get ouvre une connexion vers le serveur, envoie la requête, puis referme la connexion. Pour un appel isolé, c’est très bien. Pour des dizaines d’appels au même serveur, on refait ce travail à chaque fois.
Une session est un objet qui reste ouvert entre les appels. Elle apporte deux choses :
- elle réutilise la connexion vers un même serveur, au lieu d’en ouvrir une nouvelle à chaque appel ;
- elle porte des en-têtes communs, écrits une seule fois et envoyés avec chaque requête.
import requests
s = requests.Session()
s.headers.update({"User-Agent": "fleetcheck"})
r1 = s.get("http://web-1.local/health", timeout=3)
r2 = s.get("http://web-1.local/health", timeout=3)
Ce qui change et ce qui ne change pas :
requests.get(...) |
s.get(...) |
|
|---|---|---|
| Connexion | une nouvelle à chaque appel | ouverte une fois, réutilisée |
| En-têtes | à repasser à chaque appel | rangés dans s.headers |
timeout= |
à écrire | à écrire aussi |
| Réponse et exceptions | les mêmes | les mêmes |
Une analogie : requests.get, c’est raccrocher le téléphone après chaque question. La session garde la ligne ouverte.
Un en-tête est une information qui accompagne la requête, sous la forme nom: valeur. Ici, User-Agent dit qui appelle. La leçon suivante s’en sert pour envoyer un token.
Réessayer avec une boucle for
L’idée : faire l’appel dans une boucle. Si ça marche, on sort tout de suite. Si ça rate, on passe au tour suivant.
import requests
s = requests.Session()
for essai in range(1, 4):
try:
r = s.get("http://web-1.local/health", timeout=3)
print("réussi à l'essai", essai)
break
except requests.RequestException:
print("essai", essai, "raté")
Si le serveur ne répond qu’à la deuxième tentative :
essai 1 raté
réussi à l'essai 2
Trois points à lire dans ce code :
range(1, 4)donne1,2,3: trois essais au maximum, pas quatre.breakquitte la boucle dès qu’un essai réussit. Les essais restants n’ont pas lieu.- Si
s.getlève une exception, les lignes qui le suivent dans letrysont sautées :breakn’est pas atteint, la boucle continue.
Attention : on ne réessaie que ce qu’on attrape. Avec except requests.Timeout, une ConnectionError n’est pas attrapée : elle arrête le programme au premier essai.
time.sleep : attendre entre deux essais
Réessayer immédiatement ne sert pas à grand-chose : en quelques millisecondes, le serveur n’a pas eu le temps de se remettre. time.sleep(n) met le programme en pause pendant n secondes.
import time
print("avant")
time.sleep(2)
print("après")
après s’affiche deux secondes après avant. Pendant ce temps, le programme ne fait rien.
La pause doublée
Plutôt qu’une pause fixe, on double la pause à chaque essai raté : 1 s, puis 2 s, puis 4 s.
pause = 1
for essai in range(3):
print(pause)
pause = pause * 2
1
2
4
Pourquoi doubler ? Un raté très court est absorbé dès la première seconde. Si le problème dure, on laisse de plus en plus de temps au serveur pour se relever, au lieu de le harceler au moment où il est en difficulté.
La fonction complète
import time
import requests
def fetch(s, url, essais=3):
pause = 1
for essai in range(1, essais + 1):
try:
return s.get(url, timeout=3)
except requests.RequestException:
print("essai", essai, "raté")
if essai < essais:
time.sleep(pause)
pause = pause * 2
return None
Si l’appel réussit, return rend la réponse et quitte la fonction : c’est lui qui joue le rôle du break. Si tous les essais ratent, la boucle se termine et la fonction renvoie None.
Déroulé pour un serveur qui ne répond jamais, avec essais=3 :
| Essai | Résultat | Ensuite |
|---|---|---|
| 1 | raté | pause de 1 s |
| 2 | raté | pause de 2 s |
| 3 | raté | pas de pause : on abandonne, None |
Il n’y a pas de pause après le dernier essai : il n’y a plus rien à attendre. Avec trois essais, on attend donc 1 + 2 = 3 secondes. La pause de 4 s n’arriverait qu’avant un quatrième essai.
Pourquoi ne pas réessayer sans fin
Une boucle while True qui réessaie jusqu’au succès semble tentante. C’est une mauvaise idée, pour trois raisons :
- Un serveur vraiment tombé ne reviendra pas tout seul. La boucle ne se termine jamais.
- L’outil est gelé. Coincé sur ce serveur, il ne vérifie plus les autres et n’envoie jamais l’alerte. C’est exactement le problème du
timeout=oublié. - On aggrave la panne. Un serveur en difficulté qu’on bombarde de requêtes a encore moins de chances de se relever.
Les essais répétés servent à absorber un raté passager. Une panne qui dure doit être déclarée : après quelques essais, le serveur est classé down. D’où la boucle for, qui a un nombre de tours fixé d’avance.
À retenir
s = requests.Session(): la connexion est réutilisée, ets.headers.update({...})range des en-têtes envoyés avec chaque requête. Letimeout=reste à écrire à chaque appel.- Essais répétés : une boucle
foravec un nombre de tours fixé, et une sortie (breakoureturn) dès que ça marche. time.sleep(n)met le programme en pausensecondes.- Pause doublée : 1 s, 2 s, 4 s. On laisse de plus en plus de temps au serveur.
- Jamais d’essais sans fin : un serveur vraiment tombé gèlerait l’outil. Après quelques essais, il est
down.
Entraînement
12 questions, dont 4 à compléter. Tu réponds, on passe à la suivante. La note arrive à la fin.
Ta note
Cartes · 7
Réviser les cartes ici
Pourquoi