Aller au contenu
English

Gestion des connexions

  • Leçon 9
  • 55 min
  • Module 3 : Accéder aux données depuis une API

Ce que vous saurez faire

N’ouvrez plus une nouvelle connexion à la base de données pour chaque requête. Partagez quelques connexions ouvertes dans un pool de connexions (pool), et fixez ses limites. Assurez-vous qu’une requête échouée ne garde jamais une connexion et ne laisse jamais un changement incomplet.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 55 minutes

Ce que vous devez savoir

  • Le dépôt et les points de terminaison des billets de soutien, présentés dans Requêtes sûres.
  • Les transactions, leur validation et leur annulation, ainsi que SELECT … FOR UPDATE, présentés dans Transactions.
  • La chaîne de connexion dans DATABASE_URL, présentée dans De SQLite à PostgreSQL.

Ce qu’il vous faut sur votre ordinateur

  • Le projet après Requêtes sûres : python -m pytest affiche 67 passed (67 tests réussis), et .env contient la ligne DB_POOL=false.
  • PostgreSQL en marche, avec les petites données chargées. Plus tard dans la leçon, vous chargerez les grandes données, créées dans Inventaire des modes d’accès. Si data/large/ manque, le chargeur les recrée (environ 15 secondes).

Après cette leçon, votre projet contient trois fichiers de plus et correspond à l’instantané end-of-m03, sauf les fichiers de migration de la prochaine leçon. Chaque pool occupé et chaque fuite de cette leçon sont des cas construits. Le pool et la base de données sont réels; les requêtes lentes et le code incorrect sont intentionnels. Les tests ont été faits sur macOS (Apple silicon); les commandes Windows n’ont pas été testées. Les durées ont été mesurées sur un ordinateur Apple M4 sans autre activité. Vos durées seront différentes, mais la tendance restera la même.

À l’entrée d’un supermarché se trouve une rangée de chariots. Vous en prenez un, faites vos achats et le rapportez. La personne suivante prend le même chariot. Personne ne construit un nouveau chariot pour chaque visite, et personne ne le jette à la fin.

Trois problèmes peuvent survenir. Si 30 personnes arrivent en même temps et que le magasin a 10 chariots, 20 personnes attendent à l’entrée. Si certaines personnes emportent leur chariot chez elles, la rangée se vide peu à peu. D’autres attendent alors même si le magasin est tranquille. Et si le magasin n’avait aucun chariot, il faudrait en fabriquer un à l’entrée pour chaque personne. Chaque visite commencerait lentement.

Une connexion à la base de données, c’est le chariot.

Le problème : une nouvelle connexion pour chaque requête

Section intitulée « Le problème : une nouvelle connexion pour chaque requête »

Jusqu’ici, history.py ouvrait une nouvelle connexion pour chaque requête et la fermait à la fin. Depuis la dernière leçon, les points de terminaison des billets font pareil, puisque .env contient DB_POOL=false :

ticket_api/history.py (end of Module 2)
return psycopg.connect(self.url, connect_timeout=3, row_factory=dict_row)

Une connexion est plus qu’une ligne ouverte. Avant la première requête SQL, le client et le serveur ouvrent une connexion réseau et vérifient le mot de passe. Ensuite, PostgreSQL démarre un nouveau processus serveur réservé à cette connexion, avec sa propre mémoire. À la fin, tout est supprimé. L’auteur a mesuré ce travail avec scripts.pool_demo connect :

== connect
open a new connection: median 3.25 ms | borrow one from the pool: median 0.003 ms (50 times each)

Ouvrir une connexion a pris 3,25 ms. Emprunter une connexion ouverte a pris 0,003 ms : environ mille fois moins. Est-ce que 3 ms, c’est beaucoup? Mesurez avant de décider. L’auteur a exécuté la même charge de travail, avec et sans pool : 200 requêtes HTTP pour la liste des billets sur les grandes données, 4 à la fois.

200 requests, 4 at a time, pool on (max 10)
median 438.7 ms | p95 500.8 ms | slowest 574.7 ms | 9.0 requests/s
errors: 0 {200: 200} | database queries per request: 41
200 requests, 4 at a time, pool off
median 405.1 ms | p95 477.8 ms | slowest 516.0 ms | 9.7 requests/s
errors: 0 {200: 200} | database queries per request: 41

Aucune différence. L’exécution sans pool était même un peu plus rapide, ce qui relève du bruit de mesure. Deux exécutions d’une requête de 400 ms diffèrent de plus de 3 ms. Chaque requête HTTP envoie 41 requêtes SQL lentes, et 3 ms représente moins de 1 % du temps. Le pool ne peut pas aider un point de terminaison qui consacre son temps aux requêtes SQL.

Le module Améliorer la performance des requêtes réduit cette même liste à une requête SQL et à 2,8 ms. Les 3 ms d’une nouvelle connexion dépassent alors la durée de la requête HTTP entière. Avec 4 requêtes à la fois, la médiane est passée de 2,8 ms avec le pool à 8,1 ms sans pool. L’API traitait environ 482 plutôt que 1 334 requêtes par seconde. Un pool compte donc surtout quand les requêtes SQL sont rapides. Mesurez votre propre point de terminaison avant d’attribuer le problème aux connexions.

Il existe une deuxième raison, liée aux limites. Un serveur PostgreSQL accepte un nombre fixe de connexions. SHOW max_connections; affiche 100 dans la base de données du cours. Chaque connexion est un processus serveur avec sa propre mémoire. Sans pool, le nombre de connexions correspond au nombre de requêtes en cours. Dans le test de l’auteur avec 64 clients simultanés et sans pool, toutes les requêtes ont réussi. La limite n’a jamais été atteinte. FastAPI exécute un point de terminaison normal (def) dans un pool de fils d’exécution. Par défaut, ce pool en exécute au plus 40 à la fois, donc au plus 40 connexions étaient ouvertes. Ajoutez un deuxième processus d’application ou d’autres processus, et le total peut dépasser 100. Les nouvelles connexions échouent alors avec too many clients (trop de clients). Ce cas n’a pas été exécuté dans le cours. Il survient quand plusieurs processus ouvrent chacun leurs propres connexions.

Un pool de connexions garde quelques connexions ouvertes et les prête. Une requête en emprunte une, l’utilise et la rend. La connexion reste ouverte pour la prochaine requête. Le pool fixe aussi les règles de la rangée de chariots :

  • Taille minimale : le nombre de connexions qui restent ouvertes quand l’API est tranquille (DB_POOL_MIN, 2).
  • Taille maximale : le nombre maximal de connexions ouvertes, même quand l’API est occupée (DB_POOL_MAX, 10). Cela protège la limite de la base de données.
  • Limite d’attente : la durée pendant laquelle une requête attend une connexion libre avant d’abandonner (DB_POOL_TIMEOUT, 5 secondes). L’API répond alors 503 database_busy (base de données occupée).
  • Durées de vie : une connexion est remplacée après 30 minutes (max_lifetime). Une connexion supplémentaire inactive se ferme après 5 minutes (max_idle). Les anciennes connexions ne durent pas indéfiniment, et une API tranquille en garde peu.

Un petit exemple montre les règles avec tous les nombres. Un pool contient 3 connexions. Six requêtes arrivent à 0,05 seconde d’intervalle, et chacune a besoin de sa connexion pendant 2 secondes. Une requête attend au plus 1 seconde. Voici la sortie de scripts.pool_demo exhaust :

== exhaust
pool of 3, each request holds its connection 2.0 s, a request waits at most 1.0 s
request 1: asked at 0.00 s, connection at 0.00 s, done at 2.01 s: ok
request 2: asked at 0.05 s, connection at 0.05 s, done at 2.06 s: ok
request 3: asked at 0.11 s, connection at 0.11 s, done at 2.11 s: ok
request 4: asked at 0.17 s, connection at -, done at 1.17 s: busy (503)
request 5: asked at 0.23 s, connection at -, done at 1.23 s: busy (503)
request 6: asked at 0.29 s, connection at -, done at 1.30 s: busy (503)

Les requêtes 1 à 3 obtiennent une connexion immédiatement. Les requêtes 4 à 6 attendent. La première connexion revient à 2,01 s, mais elles ont abandonné vers 1,2 s, après leur seconde d’attente. Chacune répond 503. Essayez les autres tailles et limites d’attente :

Explorer

Un pool de connexions : attentes, réponses 503 et fuite

Réponses
3 sur 6
Occupé (503)
3
Attente la plus longue pour une connexion
0,00 s

Un pool de 3, 6 requêtes à 0,05 s d’intervalle. Chacune garde sa connexion 2 s, et une requête attend au plus 1 s. Les requêtes 1 à 3 obtiennent une connexion. Les 3 autres attendent 1 s, n’obtiennent rien et répondent 503 database_busy. Une connexion revient seulement vers 2 s.

  • Attend une connexion
  • Garde une connexion
  • Abandonne : 503
  1. Requête 1ok à 2,01 s
  2. Requête 2ok à 2,07 s
  3. Requête 3ok à 2,12 s
  4. Requête 4503 à 1,17 s
  5. Requête 5503 à 1,23 s
  6. Requête 6503 à 1,29 s

Un cas construit, avec un vrai pool et une vraie base de données PostgreSQL (scripts.pool_demo). Les temps viennent des réglages (2 s de travail, la limite d’attente), donc ils sont presque les mêmes sur chaque ordinateur.

Exécutions enregistrées de scripts.pool_demo (un vrai pool sur une vraie base de données; le travail lent et la fuite sont construits) et durées mesurées du point de terminaison avec et sans pool.
Description textuelle de l’explorateur

L’explorateur comporte trois vues. La première, « Six requêtes à la fois », montre six requêtes en lignes sur un axe de 0 à 4,5 secondes. Une barre hachurée représente le temps d’attente d’une connexion. Une barre pleine représente les 2 secondes pendant lesquelles la requête la garde. Un repère 503 indique le moment où elle abandonne. La vue commence avec un pool de 3 et une limite d’attente de 1 s. Les requêtes 1 à 3 gardent une connexion jusqu’à environ 2,1 s. Les requêtes 4 à 6 répondent 503 vers 1,2 s.

Avec une limite d’attente de 3 s et un pool de 3, les six obtiennent une réponse. Les requêtes 4 à 6 attendent environ 1,9 s qu’une connexion revienne, puis la gardent pendant 2 s. La dernière finit à 4,15 s. Avec un pool de 1 et une limite de 1 s, seule la requête 1 réussit. Avec un pool de 1 et une limite de 3 s, la requête 2 attend 2,0 s et réussit. Les requêtes 3 à 6 répondent 503 vers 3,2 s. Avec un pool de 2 et une limite de 3 s, quatre réussissent et deux répondent 503. Avec un pool de 6, les six obtiennent une connexion immédiatement.

La deuxième vue, « Une fuite », parcourt du code qui oublie de rendre les connexions (voir l’erreur courante ci-dessous). Après trois recherches échouées, 0 connexion sur 3 est libre. Le quatrième appel échoue avec PoolTimeout, puis une requête normale attend 1,01 s et répond 503.

La troisième vue, « Pool activé ou non », montre la durée médiane par requête du point de terminaison rapide de la liste du module Améliorer la performance des requêtes. Avec 1 requête à la fois : 1,8 ms avec le pool, 5,4 ms sans pool. Avec 4 : 2,8 et 8,1 ms. Avec 16 : 11,6 et 29,3 ms.

Avec 64 : 46,7 et 116 ms. À partir de 4 requêtes simultanées, le débit était d’environ 1 300 requêtes par seconde avec le pool. Sans pool, il était d’environ 500. Aucune requête n’a échoué.

Ce que cela nous apprend. Une limite d’attente plus longue transforme les réponses 503 en réponses lentes. Un pool plus grand transforme les attentes en travail parallèle. Les deux ont un coût. Une longue limite d’attente fait patienter les clients devant une base de données trop lente. Un grand pool peut faire dépasser la limite de la base de données. Les bons nombres viennent des mesures : combien de temps une requête garde sa connexion, et combien arrivent en même temps.

Le fichier ticket_api/db.py d’Amira crée un seul pool pour l’API. L’API l’ouvre au démarrage et le ferme à l’arrêt, dans main.py. Aucune connexion ne reste donc ouverte sur le serveur après l’arrêt de l’API. Chaque point de terminaison emprunte une connexion dans un bloc with :

ticket_api/tickets.py (one endpoint)
def add_message(ticket_id: TicketId, message: MessageIn, db: DB) -> Message:
with db.connection() as conn:
row = repository.add_message(conn, ticket_id, message.author, message.body)
return Message(**row)

Le bloc with définit tout le contrat. À sa fin, la connexion retourne au pool. Avant cela, le bloc valide la transaction s’il s’est terminé normalement, et l’annule s’il s’est terminé avec une erreur. Le code à l’intérieur ne peut oublier aucune de ces étapes. Voici la partie de db.py qui fait ce travail :

ticket_api/db.py (Database.connection, the pool part)
try:
with self.pool.connection() as conn: # commit at the end, roll back on an error, return
yield conn
except PoolTimeout as error:
...
raise DatabaseBusy(f"no connection within {self.timeout} seconds") from error
except psycopg.OperationalError as error:
raise DatabaseUnavailable("the database does not answer") from error

Deux erreurs deviennent des réponses claires dans errors.py. DatabaseBusy correspond à 503 database_busy, avec l’en-tête Retry-After: 2 (réessayer après 2 secondes). Le client sait donc qu’il peut réessayer bientôt. DatabaseUnavailable correspond à 503 database_unavailable (base de données indisponible). Avec DB_POOL=false, la même fonction connection() ouvre et ferme plutôt une nouvelle connexion. Le projet garde ce mode uniquement pour mesurer la différence.

Voici le parcours complet d’une requête, avec le pool et la transaction :

Une séquence avec quatre participants : client, point de terminaison de l'API, pool de connexions et PostgreSQL. Le client envoie POST /v1/tickets/T-30002/messages. Le point de terminaison emprunte une connexion avec db.connection(). Si une connexion est libre, le pool la fournit en 0,003 ms. Si elles sont toutes utilisées, le point de terminaison attend au plus DB_POOL_TIMEOUT, soit 5 secondes. Si aucune ne revient, le client reçoit 503 database_busy avec Retry-After : 2. Avec une connexion, le point de terminaison envoie BEGIN, SELECT … FOR UPDATE pour verrouiller le billet, INSERT INTO messages et UPDATE tickets pour les compteurs. Si toutes les instructions ont réussi : COMMIT, les deux changements sont conservés, la connexion retourne au pool, et le client reçoit 201 Created (créé). Si une instruction a échoué : ROLLBACK, aucun changement n'est conservé, la connexion revient, et le client reçoit une erreur. La connexion reste ouverte pour la prochaine requête.
Une requête pour ajouter un message : emprunter une connexion, effectuer une transaction, rendre la connexion dans les deux cas.
Description textuelle du diagramme

Le diagramme contient deux boîtes marquées alt (alternative), une pour chaque choix. La première concerne le pool. Soit une connexion est libre immédiatement, soit la requête attend jusqu’à 5 secondes et répond 503 sans toucher la base de données. La deuxième concerne la transaction. Soit toutes les instructions ont réussi et COMMIT (valider) conserve les deux changements, soit une a échoué et ROLLBACK (annuler) supprime les deux. Dans les deux cas, la connexion retourne au pool. C’est le sens de « une requête échouée ne garde jamais une connexion et ne laisse jamais un changement incomplet ».

Une connexion empruntée est un chariot en usage. Le pool ne peut la prêter à personne d’autre. Une transaction sur cette connexion peut aussi garder des verrous que d’autres requêtes attendent. Les règles concernent donc le temps :

  1. Commencez et terminez la transaction au même endroit. Dans le projet, le bloc with db.connection() du point de terminaison constitue la transaction pour les lectures. repository.add_message ouvre with conn.transaction() pour ses trois instructions. Une fonction du dépôt ne valide jamais une transaction elle-même.
  2. Ne faites aucun travail lent à l’intérieur. N’appelez pas un modèle d’IA, n’envoyez pas de courriel et ne téléversez pas de fichier pendant que vous gardez une connexion. Lisez ce qu’il vous faut, rendez la connexion, faites le travail lent, puis empruntez-en une autre pour enregistrer le résultat. Le module Fichiers, vecteurs et cycle de vie suit cette règle pour le stockage des fichiers.
  3. Ne prenez jamais une connexion manuellement. pool.getconn() exige un pool.putconn() correspondant dans tous les cas, y compris les erreurs. Le bloc with le fait pour vous. L’erreur courante montre ce qui arrive sans ce bloc.

La vérification de fin du module teste le deuxième parcours du diagramme, celui de l’échec. scripts.pool_demo rollback emprunte une connexion, démarre une transaction et ajoute un message à T-30002. Il en ajoute ensuite un deuxième avec l’auteur robot. La contrainte CHECK (vérification) nommée messages_author_known rejette le deuxième message :

== rollback
error: new row for relation "messages" violates check constraint "messages_author_known"
messages on T-30002: 1 before, 1 after | pool: 2 of 2 connections free

T-30002 avait 1 message avant et 1 après. La première insertion n’a donc pas été conservée : aucune mise à jour partielle. Les deux connexions du pool sont de nouveau libres, donc la transaction échouée n’a causé aucune fuite. Le test tests/test_pool.py vérifie exactement ces deux faits. Il les vérifie aussi par l’API : cinq requêtes échouées de suite sur un pool de 2, puis la requête suivante réussit encore.

  1. Dans ~/projects/ticket-api, avec l’environnement virtuel actif, copiez les trois fichiers de cette leçon depuis l’instantané end-of-m03 :

    Fenêtre de terminal
    S=../nextia-learning-labs/C20/snapshots/end-of-m03
    for f in scripts/pool_demo.py scripts/bench.py tests/test_pool.py; do cp "$S/$f" "$f"; done
  2. Supprimez la ligne DB_POOL=false de .env. Le pool est maintenant activé, ce qui est le réglage par défaut.

  3. Exécutez les quatre scénarios sur les petites données, un à la fois : python -m scripts.pool_demo connect, puis exhaust, leak et rollback. Comparez-les aux sorties de cette leçon.

  4. Démarrez l’API avec fastapi dev. Dans un deuxième terminal, consultez ses connexions dans PostgreSQL :

    Fenêtre de terminal
    docker compose exec db psql -U tickets -d tickets -c "SELECT pid, state, backend_start::time(0) AS started FROM pg_stat_activity WHERE datname = 'tickets' AND pid <> pg_backend_pid();"

    Envoyez ensuite quelques requêtes HTTP à http://127.0.0.1:8000/v1/tickets et exécutez de nouveau la requête SQL. Arrêtez l’API avec Ctrl+C, puis exécutez la requête SQL une troisième fois.

  5. Exécutez les tests : python -m pytest. Attendez-vous à voir 74 passed (74 tests réussis).

  6. Exécutez seulement la vérification de fin du module, avec le nom de chaque test : python -m pytest tests/test_pool.py -v.

  7. Chargez les grandes données pour le banc d’essai : python -m scripts.load --size large --reset. Cela prend quelques secondes sur un ordinateur tranquille, et jusqu’à une minute sur un ordinateur occupé. Cela prend plus longtemps s’il faut recréer les données.

  8. Mesurez le point de terminaison de la liste avec le pool, puis sans pool. Chaque exécution prend environ 25 secondes. Fermez d’abord les autres programmes occupés.

    Fenêtre de terminal
    python -m scripts.bench
    python -m scripts.bench --pool off

    Gardez les grandes données : la prochaine leçon les utilise.

Afficher le résultat attendu

L’étape 3 affiche les quatre sorties de cette leçon. Les durées de connect et les secondes exactes diffèrent un peu sur votre ordinateur. Étape 4 : pendant que l’API fonctionne, la requête SQL montre deux connexions idle (inactives), soit le minimum du pool. Après vos requêtes HTTP, elle montre les deux mêmes numéros pid (identifiants de processus) : le pool les a réutilisées. Après Ctrl+C, elle affiche (0 rows) (0 ligne), puisque le pool les a fermées à l’arrêt.

pid | state | started
-------+-------+----------
18468 | idle | 04:34:51
18469 | idle | 04:34:51
(2 rows)

L’étape 6 présente sept tests, dont test_a_failed_transaction_leaves_no_partial_update_and_no_connection et test_a_failed_api_request_returns_its_connection. Elle se termine par 7 passed (7 tests réussis). Étape 8 : les deux exécutions affichent errors: 0 {200: 200} | database queries per request: 41 (aucune erreur, 200 réponses 200 et 41 requêtes SQL par requête HTTP). Leurs médianes, de quelques centaines de millisecondes, sont proches l’une de l’autre. L’exécution la plus rapide change d’une fois à l’autre. Deux lignes WARNING ticket_api: API_KEY is not set (avertissement : API_KEY n’est pas définie) peuvent apparaître au début. Elles indiquent seulement que /v1/classify n’a pas de clé sur votre ordinateur.

Changez un réglage. Le banc d’essai propose les options --pool-max, --pool-timeout, --concurrency et --requests. Faites d’abord une prédiction : que se passe-t-il sur les grandes données avec un pool de 1 connexion, une attente de 1 seconde et 16 requêtes simultanées? Chaque requête prend quelques centaines de millisecondes. Exécutez ensuite le test avec 48 requêtes pour qu’il se termine vite :

Fenêtre de terminal
python -m scripts.bench --requests 48 --concurrency 16 --pool-max 1 --pool-timeout 1
Afficher la réponse

La plupart des requêtes répondent 503. Une seule requête à la fois peut garder l’unique connexion. La 16e requête dans la file devrait attendre 15 autres requêtes, soit plusieurs secondes. Mais elle ne peut attendre que 1 seconde. Voici l’exécution de l’auteur sur un ordinateur occupé :

errors: 31 {200: 17, 503: 31} | database queries per request: 41

Sur 48 requêtes, 31 ont répondu 503. Le journal affichait WARNING ticket_api: database busy: no connection within 1.0 seconds (base de données occupée : aucune connexion obtenue en 1,0 seconde) pour chacune. Votre nombre sera différent, mais la plupart des requêtes échouent. Avec le pool par défaut de 10 et la même commande sans les deux options du pool, aucune requête de l’auteur n’a échoué. La durée médiane a toutefois augmenté, car 16 requêtes SQL lentes simultanées partagent la même base de données. Un pool plus grand ne peut pas rendre une requête SQL lente rapide. C’est le rôle du module Améliorer la performance des requêtes.

Erreur courante

Une connexion prise manuellement et rendue seulement en cas de succès

Symptôme (simulé) : l’API fonctionne pendant un certain temps. Puis chaque requête attend et répond 503 database_busy (base de données occupée), même si l’ordinateur est peu sollicité. scripts.pool_demo leak le montre avec un pool de 3 connexions :

== leak
call 1: LookupError; connections free: 2 of 3
call 2: LookupError; connections free: 1 of 3
call 3: LookupError; connections free: 0 of 3
call 4: PoolTimeout
a normal request: busy (503) after 1.01 s

Cause : une recherche prend une connexion manuellement et la rend seulement lorsqu’elle trouve le billet de soutien :

scripts/pool_demo.py (the bug, on purpose)
conn = db.pool.getconn()
row = conn.execute("SELECT subject FROM tickets WHERE ticket_id = %s", (ticket_id,)).fetchone()
if row is None:
raise LookupError(ticket_id) # returns without db.pool.putconn(conn)
db.pool.putconn(conn)

Chaque recherche d’un billet absent garde une connexion. Après trois recherches, le pool est vide. Ailleurs, du code correct subit alors les effets du bogue.

Diagnostic : examinez d’abord le pool. Ses chiffres (db.stats(), ou l’avertissement pool timeout (délai d’attente du pool dépassé) dans le journal) montrent 0 connexion disponible et des requêtes en attente. Examinez ensuite la base de données. Pendant que le code défectueux garde les connexions, SELECT state, query FROM pg_stat_activity WHERE datname = 'tickets'; les affiche dans l’état idle in transaction (inactif dans une transaction), avec la dernière requête du code défectueux. Lors de la vérification de l’auteur, trois lignes présentaient cet état. Une connexion dans cet état a une transaction ouverte que personne ne terminera.

Correction :

  1. Empruntez les connexions seulement avec with db.connection() as conn:. Le bloc rend la connexion dans tous les cas.
  2. Redémarrez l’API pour récupérer les connexions immédiatement. La fuite revient tant que le code n’est pas corrigé.
  3. Ajoutez un test dont le nombre d’échecs dépasse la taille du pool, comme le fait test_a_failed_api_request_returns_its_connection : cinq échecs avec un pool de 2 connexions, puis une requête normale.

Pour un service d’IA : un point de terminaison d’IA attend souvent plusieurs secondes la réponse d’un modèle. S’il garde une connexion pendant cette attente, chaque appel lent agit comme une courte fuite. Lisez les données nécessaires à la requête, rendez la connexion, appelez le modèle, puis empruntez une connexion pour enregistrer l’exécution d’IA.

Vérifiez votre compréhension

Sur le point de terminaison de liste de ce module, le pool n'a rien changé (438,7 contre 405,1 ms). Amira devrait-elle le retirer? Expliquez avec les chiffres.

Afficher la réponse

Non. Sur ce point de terminaison, chaque requête HTTP passe environ 400 ms à exécuter 41 requêtes SQL. Les 3,25 ms d’une nouvelle connexion représentent moins de 1 % de ce temps et se perdent dans les variations. Le module 4 ramène le temps du même point de terminaison à 2,8 ms. Une nouvelle connexion par requête HTTP fait alors passer ce temps à 8,1 ms. Le débit tombe d’environ 1 300 à environ 500 requêtes par seconde. Le pool plafonne aussi le nombre de connexions, ce qui protège la limite de 100 de la base de données. Mesurez chaque changement; gardez le pool pour ces deux raisons.

Vérifiez votre compréhension

Prédisez le résultat : un pool de 10 connexions, une limite d'attente de 5 secondes. Chaque requête garde sa connexion pendant 50 ms. 100 requêtes arrivent au même moment. Certaines répondent-elles 503?

Afficher la réponse

Non, dans ce modèle simple. Dix requêtes s’exécutent à la fois, et chaque lot de dix prend 50 ms. La centième requête attend neuf lots, soit environ 0,45 seconde, bien moins que 5 secondes. Toutes les requêtes réussissent, et la plus lente a environ une demi-seconde de retard. Une requête reçoit 503 seulement si son attente d’une connexion libre dépasse la limite. Cela arrive lorsque les requêtes gardent les connexions longtemps, ou lorsqu’un trop grand nombre arrivent pour être traitées à temps.

Vérifiez votre compréhension

Une requête qui échoue ne doit laisser « aucune mise à jour partielle ni aucune fuite de connexion ». Quelle ligne du projet garantit chacun de ces résultats?

Afficher la réponse

Aucune mise à jour partielle : with conn.transaction(): dans repository.add_message. Lorsqu’une instruction du bloc échoue, psycopg annule toute la transaction. La première insertion ne reste donc pas. Aucune fuite de connexion : with db.connection() as conn: dans le point de terminaison. La fin du bloc rend la connexion au pool dans tous les cas, même après une erreur. tests/test_pool.py vérifie les deux : le nombre de messages reste identique, et toutes les connexions du pool sont libres.

Récapitulatif

  • Ouvrir une connexion prenait 3,25 ms; en emprunter une à un pool de connexions (pool) prenait 0,003 ms. Un pool garde quelques connexions ouvertes et les prête.
  • Mesurez d’abord. Sur le point de terminaison de liste lent (41 requêtes SQL), le pool n’a rien changé.
  • Sans pool, le point de terminaison rapide du module 4 prenait 8,1 ms au lieu de 2,8 ms. Son débit est passé d’environ 1 300 à environ 500 requêtes par seconde.
  • Le pool fixe des limites : 2 à 10 connexions, une attente maximale de 5 secondes, puis 503 database_busy avec Retry-After: 2 (réessayer après 2 secondes); le pool remplace une connexion après 30 minutes. La base de données a sa propre limite de 100 connexions.
  • with db.connection() rend la connexion dans tous les cas; with conn.transaction() conserve tout un changement ou rien du tout. Gardez les transactions courtes, sans travail lent à l’intérieur.
  • Une fuite vide le pool : trois recherches échouées ont laissé 0 connexion disponible sur 3. Une requête correcte a ensuite répondu 503.
  • Vérification finale : tests/test_pool.py réussit; votre projet affiche 74 passed (74 tests réussis).

Leçon suivante : Migrations