[{"data":1,"prerenderedAt":923},["ShallowReactive",2],{"post-fr-asyncio-production":3},{"id":4,"title":5,"body":6,"date":908,"description":909,"excerpt":910,"extension":911,"meta":912,"navigation":60,"path":913,"readTime":99,"seo":914,"slug":915,"stem":916,"tags":917,"__hash__":922},"fr_blog/fr/blog/asyncio-production.md","asyncio en production : les pièges que les tutos ne montrent pas",{"type":7,"value":8,"toc":897},"minimark",[9,13,22,31,38,149,158,165,224,228,238,306,316,320,327,357,364,367,413,419,423,426,524,534,538,541,621,640,644,647,768,775,779,782,840,843,871,874,878,893],[10,11,5],"h1",{"id":12},"asyncio-en-production-les-pièges-que-les-tutos-ne-montrent-pas",[14,15,16,17,21],"p",{},"Les tutoriels asyncio s'arrêtent presque toujours au même endroit : ",[18,19,20],"code",{},"await asyncio.gather(task1(), task2())",", quelques exemples de coroutines, et c'est tout. En production, la réalité est plus nuancée. Les exceptions silencieuses, les tâches qui ne se terminent jamais, les shutdowns qui bloquent — voici les problèmes réels et comment les résoudre.",[23,24,26,27,30],"h2",{"id":25},"le-problème-avec-asynciogather-et-les-exceptions","Le problème avec ",[18,28,29],{},"asyncio.gather"," et les exceptions",[14,32,33,34,37],{},"Le comportement par défaut de ",[18,35,36],{},"gather"," est contre-intuitif :",[39,40,45],"pre",{"className":41,"code":42,"language":43,"meta":44,"style":44},"language-python shiki shiki-themes github-dark github-light","import asyncio\n\nasync def task_ok():\n    await asyncio.sleep(1)\n    return \"ok\"\n\nasync def task_fail():\n    await asyncio.sleep(0.5)\n    raise ValueError(\"quelque chose a mal tourné\")\n\nasync def main():\n    results = await asyncio.gather(task_ok(), task_fail())\n    print(results)  # Ne sera jamais atteint\n\nasyncio.run(main())\n# ValueError: quelque chose a mal tourné\n# task_ok() a été annulé silencieusement\n","python","",[18,46,47,55,62,68,74,80,85,91,97,103,108,114,120,126,131,137,143],{"__ignoreMap":44},[48,49,52],"span",{"class":50,"line":51},"line",1,[48,53,54],{},"import asyncio\n",[48,56,58],{"class":50,"line":57},2,[48,59,61],{"emptyLinePlaceholder":60},true,"\n",[48,63,65],{"class":50,"line":64},3,[48,66,67],{},"async def task_ok():\n",[48,69,71],{"class":50,"line":70},4,[48,72,73],{},"    await asyncio.sleep(1)\n",[48,75,77],{"class":50,"line":76},5,[48,78,79],{},"    return \"ok\"\n",[48,81,83],{"class":50,"line":82},6,[48,84,61],{"emptyLinePlaceholder":60},[48,86,88],{"class":50,"line":87},7,[48,89,90],{},"async def task_fail():\n",[48,92,94],{"class":50,"line":93},8,[48,95,96],{},"    await asyncio.sleep(0.5)\n",[48,98,100],{"class":50,"line":99},9,[48,101,102],{},"    raise ValueError(\"quelque chose a mal tourné\")\n",[48,104,106],{"class":50,"line":105},10,[48,107,61],{"emptyLinePlaceholder":60},[48,109,111],{"class":50,"line":110},11,[48,112,113],{},"async def main():\n",[48,115,117],{"class":50,"line":116},12,[48,118,119],{},"    results = await asyncio.gather(task_ok(), task_fail())\n",[48,121,123],{"class":50,"line":122},13,[48,124,125],{},"    print(results)  # Ne sera jamais atteint\n",[48,127,129],{"class":50,"line":128},14,[48,130,61],{"emptyLinePlaceholder":60},[48,132,134],{"class":50,"line":133},15,[48,135,136],{},"asyncio.run(main())\n",[48,138,140],{"class":50,"line":139},16,[48,141,142],{},"# ValueError: quelque chose a mal tourné\n",[48,144,146],{"class":50,"line":145},17,[48,147,148],{},"# task_ok() a été annulé silencieusement\n",[14,150,151,153,154,157],{},[18,152,36],{}," lève la première exception et annule les autres tâches sans avertissement. Si ",[18,155,156],{},"task_ok()"," écrivait dans une base de données, le résultat est partiellement appliqué.",[14,159,160,161,164],{},"La solution : ",[18,162,163],{},"return_exceptions=True"," pour collecter toutes les exceptions sans interrompre les autres tâches :",[39,166,168],{"className":41,"code":167,"language":43,"meta":44,"style":44},"async def main():\n    results = await asyncio.gather(\n        task_ok(),\n        task_fail(),\n        return_exceptions=True\n    )\n    for result in results:\n        if isinstance(result, Exception):\n            logger.error(f\"Tâche échouée : {result}\")\n        else:\n            logger.info(f\"Résultat : {result}\")\n",[18,169,170,174,179,184,189,194,199,204,209,214,219],{"__ignoreMap":44},[48,171,172],{"class":50,"line":51},[48,173,113],{},[48,175,176],{"class":50,"line":57},[48,177,178],{},"    results = await asyncio.gather(\n",[48,180,181],{"class":50,"line":64},[48,182,183],{},"        task_ok(),\n",[48,185,186],{"class":50,"line":70},[48,187,188],{},"        task_fail(),\n",[48,190,191],{"class":50,"line":76},[48,192,193],{},"        return_exceptions=True\n",[48,195,196],{"class":50,"line":82},[48,197,198],{},"    )\n",[48,200,201],{"class":50,"line":87},[48,202,203],{},"    for result in results:\n",[48,205,206],{"class":50,"line":93},[48,207,208],{},"        if isinstance(result, Exception):\n",[48,210,211],{"class":50,"line":99},[48,212,213],{},"            logger.error(f\"Tâche échouée : {result}\")\n",[48,215,216],{"class":50,"line":105},[48,217,218],{},"        else:\n",[48,220,221],{"class":50,"line":110},[48,222,223],{},"            logger.info(f\"Résultat : {result}\")\n",[23,225,227],{"id":226},"taskgroup-la-meilleure-api-depuis-python-311","TaskGroup : la meilleure API depuis Python 3.11",[14,229,230,231,234,235,237],{},"Python 3.11 a introduit ",[18,232,233],{},"asyncio.TaskGroup",", qui corrige le comportement de ",[18,236,36],{}," de façon structurée :",[39,239,241],{"className":41,"code":240,"language":43,"meta":44,"style":44},"async def main():\n    results = []\n    try:\n        async with asyncio.TaskGroup() as tg:\n            t1 = tg.create_task(task_ok())\n            t2 = tg.create_task(task_fail())\n    except* ValueError as eg:\n        for exc in eg.exceptions:\n            logger.error(f\"Erreur : {exc}\")\n\n    # t1 et t2 sont garantis terminés ici\n    if not t1.cancelled():\n        results.append(t1.result())\n",[18,242,243,247,252,257,262,267,272,277,282,287,291,296,301],{"__ignoreMap":44},[48,244,245],{"class":50,"line":51},[48,246,113],{},[48,248,249],{"class":50,"line":57},[48,250,251],{},"    results = []\n",[48,253,254],{"class":50,"line":64},[48,255,256],{},"    try:\n",[48,258,259],{"class":50,"line":70},[48,260,261],{},"        async with asyncio.TaskGroup() as tg:\n",[48,263,264],{"class":50,"line":76},[48,265,266],{},"            t1 = tg.create_task(task_ok())\n",[48,268,269],{"class":50,"line":82},[48,270,271],{},"            t2 = tg.create_task(task_fail())\n",[48,273,274],{"class":50,"line":87},[48,275,276],{},"    except* ValueError as eg:\n",[48,278,279],{"class":50,"line":93},[48,280,281],{},"        for exc in eg.exceptions:\n",[48,283,284],{"class":50,"line":99},[48,285,286],{},"            logger.error(f\"Erreur : {exc}\")\n",[48,288,289],{"class":50,"line":105},[48,290,61],{"emptyLinePlaceholder":60},[48,292,293],{"class":50,"line":110},[48,294,295],{},"    # t1 et t2 sont garantis terminés ici\n",[48,297,298],{"class":50,"line":116},[48,299,300],{},"    if not t1.cancelled():\n",[48,302,303],{"class":50,"line":122},[48,304,305],{},"        results.append(t1.result())\n",[14,307,308,311,312,315],{},[18,309,310],{},"TaskGroup"," utilise la syntaxe ",[18,313,314],{},"except*"," (ExceptionGroup) — toutes les exceptions sont collectées, et le groupe attend que toutes les tâches soient terminées ou annulées avant de propager. Plus de tâches fantômes.",[23,317,319],{"id":318},"les-tâches-en-arrière-plan-dans-fastapi","Les tâches en arrière-plan dans FastAPI",[14,321,322,323,326],{},"Un pattern courant dans FastAPI : lancer une tâche en arrière-plan avec ",[18,324,325],{},"asyncio.create_task",". Le piège classique :",[39,328,330],{"className":41,"code":329,"language":43,"meta":44,"style":44},"# MAUVAIS — la tâche peut être garbage-collectée silencieusement\n@app.post(\"/process\")\nasync def process(data: dict):\n    asyncio.create_task(long_running_task(data))  # Référence perdue\n    return {\"status\": \"started\"}\n",[18,331,332,337,342,347,352],{"__ignoreMap":44},[48,333,334],{"class":50,"line":51},[48,335,336],{},"# MAUVAIS — la tâche peut être garbage-collectée silencieusement\n",[48,338,339],{"class":50,"line":57},[48,340,341],{},"@app.post(\"/process\")\n",[48,343,344],{"class":50,"line":64},[48,345,346],{},"async def process(data: dict):\n",[48,348,349],{"class":50,"line":70},[48,350,351],{},"    asyncio.create_task(long_running_task(data))  # Référence perdue\n",[48,353,354],{"class":50,"line":76},[48,355,356],{},"    return {\"status\": \"started\"}\n",[14,358,359,360,363],{},"asyncio ne maintient pas de référence forte aux tâches créées avec ",[18,361,362],{},"create_task",". Si le garbage collector passe au bon moment, la tâche est annulée silencieusement.",[14,365,366],{},"La bonne approche :",[39,368,370],{"className":41,"code":369,"language":43,"meta":44,"style":44},"# Dans le state de l'application\nbackground_tasks: set[asyncio.Task] = set()\n\n@app.post(\"/process\")\nasync def process(data: dict):\n    task = asyncio.create_task(long_running_task(data))\n    background_tasks.add(task)\n    task.add_done_callback(background_tasks.discard)  # Nettoyage automatique\n    return {\"status\": \"started\"}\n",[18,371,372,377,382,386,390,394,399,404,409],{"__ignoreMap":44},[48,373,374],{"class":50,"line":51},[48,375,376],{},"# Dans le state de l'application\n",[48,378,379],{"class":50,"line":57},[48,380,381],{},"background_tasks: set[asyncio.Task] = set()\n",[48,383,384],{"class":50,"line":64},[48,385,61],{"emptyLinePlaceholder":60},[48,387,388],{"class":50,"line":70},[48,389,341],{},[48,391,392],{"class":50,"line":76},[48,393,346],{},[48,395,396],{"class":50,"line":82},[48,397,398],{},"    task = asyncio.create_task(long_running_task(data))\n",[48,400,401],{"class":50,"line":87},[48,402,403],{},"    background_tasks.add(task)\n",[48,405,406],{"class":50,"line":93},[48,407,408],{},"    task.add_done_callback(background_tasks.discard)  # Nettoyage automatique\n",[48,410,411],{"class":50,"line":99},[48,412,356],{},[14,414,415,418],{},[18,416,417],{},"add_done_callback"," retire la tâche du set quand elle se termine — pas de fuite mémoire, pas de tâche annulée silencieusement.",[23,420,422],{"id":421},"timeout-sur-les-opérations-réseau","Timeout sur les opérations réseau",[14,424,425],{},"Sans timeout explicite, une opération réseau peut bloquer une coroutine indéfiniment :",[39,427,429],{"className":41,"code":428,"language":43,"meta":44,"style":44},"import asyncio\nfrom httpx import AsyncClient\n\n# MAUVAIS — peut bloquer pour toujours\nasync def fetch_data(url: str) -> dict:\n    async with AsyncClient() as client:\n        response = await client.get(url)\n        return response.json()\n\n# BON — timeout explicite\nasync def fetch_data(url: str, timeout: float = 10.0) -> dict:\n    try:\n        async with asyncio.timeout(timeout):  # Python 3.11+\n            async with AsyncClient() as client:\n                response = await client.get(url)\n                return response.json()\n    except asyncio.TimeoutError:\n        logger.error(f\"Timeout après {timeout}s sur {url}\")\n        raise\n",[18,430,431,435,440,444,449,454,459,464,469,473,478,483,487,492,497,502,507,512,518],{"__ignoreMap":44},[48,432,433],{"class":50,"line":51},[48,434,54],{},[48,436,437],{"class":50,"line":57},[48,438,439],{},"from httpx import AsyncClient\n",[48,441,442],{"class":50,"line":64},[48,443,61],{"emptyLinePlaceholder":60},[48,445,446],{"class":50,"line":70},[48,447,448],{},"# MAUVAIS — peut bloquer pour toujours\n",[48,450,451],{"class":50,"line":76},[48,452,453],{},"async def fetch_data(url: str) -> dict:\n",[48,455,456],{"class":50,"line":82},[48,457,458],{},"    async with AsyncClient() as client:\n",[48,460,461],{"class":50,"line":87},[48,462,463],{},"        response = await client.get(url)\n",[48,465,466],{"class":50,"line":93},[48,467,468],{},"        return response.json()\n",[48,470,471],{"class":50,"line":99},[48,472,61],{"emptyLinePlaceholder":60},[48,474,475],{"class":50,"line":105},[48,476,477],{},"# BON — timeout explicite\n",[48,479,480],{"class":50,"line":110},[48,481,482],{},"async def fetch_data(url: str, timeout: float = 10.0) -> dict:\n",[48,484,485],{"class":50,"line":116},[48,486,256],{},[48,488,489],{"class":50,"line":122},[48,490,491],{},"        async with asyncio.timeout(timeout):  # Python 3.11+\n",[48,493,494],{"class":50,"line":128},[48,495,496],{},"            async with AsyncClient() as client:\n",[48,498,499],{"class":50,"line":133},[48,500,501],{},"                response = await client.get(url)\n",[48,503,504],{"class":50,"line":139},[48,505,506],{},"                return response.json()\n",[48,508,509],{"class":50,"line":145},[48,510,511],{},"    except asyncio.TimeoutError:\n",[48,513,515],{"class":50,"line":514},18,[48,516,517],{},"        logger.error(f\"Timeout après {timeout}s sur {url}\")\n",[48,519,521],{"class":50,"line":520},19,[48,522,523],{},"        raise\n",[14,525,526,529,530,533],{},[18,527,528],{},"asyncio.timeout()"," (Python 3.11+) est plus propre que ",[18,531,532],{},"asyncio.wait_for()"," pour les blocs de code, car il s'intègre dans un context manager et annule proprement toutes les opérations imbriquées.",[23,535,537],{"id":536},"shutdown-propre-dans-fastapi","Shutdown propre dans FastAPI",[14,539,540],{},"Un problème sous-estimé : quand FastAPI reçoit SIGTERM (arrêt d'un pod Kubernetes, redéploiement), les tâches en cours doivent se terminer proprement.",[39,542,544],{"className":41,"code":543,"language":43,"meta":44,"style":44},"from contextlib import asynccontextmanager\nimport asyncio\n\nbackground_tasks: set[asyncio.Task] = set()\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    # Startup\n    yield\n    # Shutdown — attendre les tâches en cours\n    if background_tasks:\n        logger.info(f\"Attente de {len(background_tasks)} tâches en cours...\")\n        await asyncio.gather(*background_tasks, return_exceptions=True)\n        logger.info(\"Toutes les tâches terminées.\")\n\napp = FastAPI(lifespan=lifespan)\n",[18,545,546,551,555,559,563,567,572,577,582,587,592,597,602,607,612,616],{"__ignoreMap":44},[48,547,548],{"class":50,"line":51},[48,549,550],{},"from contextlib import asynccontextmanager\n",[48,552,553],{"class":50,"line":57},[48,554,54],{},[48,556,557],{"class":50,"line":64},[48,558,61],{"emptyLinePlaceholder":60},[48,560,561],{"class":50,"line":70},[48,562,381],{},[48,564,565],{"class":50,"line":76},[48,566,61],{"emptyLinePlaceholder":60},[48,568,569],{"class":50,"line":82},[48,570,571],{},"@asynccontextmanager\n",[48,573,574],{"class":50,"line":87},[48,575,576],{},"async def lifespan(app: FastAPI):\n",[48,578,579],{"class":50,"line":93},[48,580,581],{},"    # Startup\n",[48,583,584],{"class":50,"line":99},[48,585,586],{},"    yield\n",[48,588,589],{"class":50,"line":105},[48,590,591],{},"    # Shutdown — attendre les tâches en cours\n",[48,593,594],{"class":50,"line":110},[48,595,596],{},"    if background_tasks:\n",[48,598,599],{"class":50,"line":116},[48,600,601],{},"        logger.info(f\"Attente de {len(background_tasks)} tâches en cours...\")\n",[48,603,604],{"class":50,"line":122},[48,605,606],{},"        await asyncio.gather(*background_tasks, return_exceptions=True)\n",[48,608,609],{"class":50,"line":128},[48,610,611],{},"        logger.info(\"Toutes les tâches terminées.\")\n",[48,613,614],{"class":50,"line":133},[48,615,61],{"emptyLinePlaceholder":60},[48,617,618],{"class":50,"line":139},[48,619,620],{},"app = FastAPI(lifespan=lifespan)\n",[14,622,623,624,627,628,631,632,635,636,639],{},"Le ",[18,625,626],{},"lifespan"," context manager remplace les dépréciés ",[18,629,630],{},"@app.on_event(\"startup\")"," et ",[18,633,634],{},"@app.on_event(\"shutdown\")",". Le ",[18,637,638],{},"yield"," sépare les phases de démarrage et d'arrêt.",[23,641,643],{"id":642},"éviter-le-blocage-de-la-boucle-dévénements","Éviter le blocage de la boucle d'événements",[14,645,646],{},"asyncio est monothreadé. Un appel synchrone bloquant dans une coroutine bloque toute l'application :",[39,648,650],{"className":41,"code":649,"language":43,"meta":44,"style":44},"import asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\nexecutor = ThreadPoolExecutor(max_workers=4)\n\n# MAUVAIS — bloque la boucle d'événements\nasync def process_file(path: str) -> str:\n    with open(path) as f:\n        return f.read()  # I/O synchrone dans une coroutine\n\n# BON — délègue au thread pool\nasync def process_file(path: str) -> str:\n    loop = asyncio.get_event_loop()\n    return await loop.run_in_executor(\n        executor,\n        lambda: open(path).read()\n    )\n\n# Encore mieux — aiofiles pour l'I/O fichier\nimport aiofiles\n\nasync def process_file(path: str) -> str:\n    async with aiofiles.open(path) as f:\n        return await f.read()\n",[18,651,652,656,661,665,670,674,679,684,689,694,698,703,707,712,717,722,727,731,735,740,746,751,756,762],{"__ignoreMap":44},[48,653,654],{"class":50,"line":51},[48,655,54],{},[48,657,658],{"class":50,"line":57},[48,659,660],{},"from concurrent.futures import ThreadPoolExecutor\n",[48,662,663],{"class":50,"line":64},[48,664,61],{"emptyLinePlaceholder":60},[48,666,667],{"class":50,"line":70},[48,668,669],{},"executor = ThreadPoolExecutor(max_workers=4)\n",[48,671,672],{"class":50,"line":76},[48,673,61],{"emptyLinePlaceholder":60},[48,675,676],{"class":50,"line":82},[48,677,678],{},"# MAUVAIS — bloque la boucle d'événements\n",[48,680,681],{"class":50,"line":87},[48,682,683],{},"async def process_file(path: str) -> str:\n",[48,685,686],{"class":50,"line":93},[48,687,688],{},"    with open(path) as f:\n",[48,690,691],{"class":50,"line":99},[48,692,693],{},"        return f.read()  # I/O synchrone dans une coroutine\n",[48,695,696],{"class":50,"line":105},[48,697,61],{"emptyLinePlaceholder":60},[48,699,700],{"class":50,"line":110},[48,701,702],{},"# BON — délègue au thread pool\n",[48,704,705],{"class":50,"line":116},[48,706,683],{},[48,708,709],{"class":50,"line":122},[48,710,711],{},"    loop = asyncio.get_event_loop()\n",[48,713,714],{"class":50,"line":128},[48,715,716],{},"    return await loop.run_in_executor(\n",[48,718,719],{"class":50,"line":133},[48,720,721],{},"        executor,\n",[48,723,724],{"class":50,"line":139},[48,725,726],{},"        lambda: open(path).read()\n",[48,728,729],{"class":50,"line":145},[48,730,198],{},[48,732,733],{"class":50,"line":514},[48,734,61],{"emptyLinePlaceholder":60},[48,736,737],{"class":50,"line":520},[48,738,739],{},"# Encore mieux — aiofiles pour l'I/O fichier\n",[48,741,743],{"class":50,"line":742},20,[48,744,745],{},"import aiofiles\n",[48,747,749],{"class":50,"line":748},21,[48,750,61],{"emptyLinePlaceholder":60},[48,752,754],{"class":50,"line":753},22,[48,755,683],{},[48,757,759],{"class":50,"line":758},23,[48,760,761],{},"    async with aiofiles.open(path) as f:\n",[48,763,765],{"class":50,"line":764},24,[48,766,767],{},"        return await f.read()\n",[14,769,770,771,774],{},"La règle : toute opération qui prend plus de quelques millisecondes et n'est pas nativement async doit passer par ",[18,772,773],{},"run_in_executor"," ou une bibliothèque async dédiée.",[23,776,778],{"id":777},"déboguer-les-coroutines-qui-ne-sexécutent-jamais","Déboguer les coroutines qui ne s'exécutent jamais",[14,780,781],{},"Un piège fréquent pour les débutants asyncio :",[39,783,785],{"className":41,"code":784,"language":43,"meta":44,"style":44},"async def my_coroutine():\n    print(\"exécuté\")\n\n# MAUVAIS — crée un objet coroutine sans l'exécuter\nmy_coroutine()\n# RuntimeWarning: coroutine 'my_coroutine' was never awaited\n\n# BON\nawait my_coroutine()\n# ou\nasyncio.run(my_coroutine())\n",[18,786,787,792,797,801,806,811,816,820,825,830,835],{"__ignoreMap":44},[48,788,789],{"class":50,"line":51},[48,790,791],{},"async def my_coroutine():\n",[48,793,794],{"class":50,"line":57},[48,795,796],{},"    print(\"exécuté\")\n",[48,798,799],{"class":50,"line":64},[48,800,61],{"emptyLinePlaceholder":60},[48,802,803],{"class":50,"line":70},[48,804,805],{},"# MAUVAIS — crée un objet coroutine sans l'exécuter\n",[48,807,808],{"class":50,"line":76},[48,809,810],{},"my_coroutine()\n",[48,812,813],{"class":50,"line":82},[48,814,815],{},"# RuntimeWarning: coroutine 'my_coroutine' was never awaited\n",[48,817,818],{"class":50,"line":87},[48,819,61],{"emptyLinePlaceholder":60},[48,821,822],{"class":50,"line":93},[48,823,824],{},"# BON\n",[48,826,827],{"class":50,"line":99},[48,828,829],{},"await my_coroutine()\n",[48,831,832],{"class":50,"line":105},[48,833,834],{},"# ou\n",[48,836,837],{"class":50,"line":110},[48,838,839],{},"asyncio.run(my_coroutine())\n",[14,841,842],{},"Activer le mode debug d'asyncio détecte ces erreurs et d'autres anomalies :",[39,844,846],{"className":41,"code":845,"language":43,"meta":44,"style":44},"import asyncio\nimport logging\n\nlogging.basicConfig(level=logging.DEBUG)\nasyncio.run(main(), debug=True)\n",[18,847,848,852,857,861,866],{"__ignoreMap":44},[48,849,850],{"class":50,"line":51},[48,851,54],{},[48,853,854],{"class":50,"line":57},[48,855,856],{},"import logging\n",[48,858,859],{"class":50,"line":64},[48,860,61],{"emptyLinePlaceholder":60},[48,862,863],{"class":50,"line":70},[48,864,865],{},"logging.basicConfig(level=logging.DEBUG)\n",[48,867,868],{"class":50,"line":76},[48,869,870],{},"asyncio.run(main(), debug=True)\n",[14,872,873],{},"En mode debug, asyncio logue les coroutines non attendues, les tâches qui prennent plus de 100ms (signe d'un blocage de la boucle), et les ressources non fermées proprement.",[23,875,877],{"id":876},"ce-quil-faut-retenir","Ce qu'il faut retenir",[14,879,880,881,883,884,886,887,889,890,892],{},"asyncio est puissant mais ses comportements par défaut sont parfois surprenants. Les points clés : utiliser ",[18,882,310],{}," plutôt que ",[18,885,36],{}," sur Python 3.11+, toujours maintenir une référence aux tâches créées avec ",[18,888,362],{},", ajouter des timeouts explicites sur toutes les opérations réseau, et gérer le shutdown proprement dans le ",[18,891,626],{}," FastAPI. Ces quatre pratiques couvrent l'essentiel des problèmes rencontrés en production.",[894,895,896],"style",{},"html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":44,"searchDepth":57,"depth":57,"links":898},[899,901,902,903,904,905,906,907],{"id":25,"depth":57,"text":900},"Le problème avec asyncio.gather et les exceptions",{"id":226,"depth":57,"text":227},{"id":318,"depth":57,"text":319},{"id":421,"depth":57,"text":422},{"id":536,"depth":57,"text":537},{"id":642,"depth":57,"text":643},{"id":777,"depth":57,"text":778},{"id":876,"depth":57,"text":877},"2024-08-19","Les tutoriels asyncio s'arrêtent presque toujours au même endroit : await asyncio.gather(task1(), task2()), quelques exemples de coroutines, et c'est tout. En production, la réalité est plus nuancée. Les exceptions silencieuses, les tâches qui ne se terminent jamais, les shutdowns qui bloquent — voici les problèmes réels et comment les résoudre.",null,"md",{},"/fr/blog/asyncio-production",{"title":5,"description":909},"asyncio-production","fr/blog/asyncio-production",[918,919,920,921],"Python","asyncio","FastAPI","Concurrence","M31f847IhLbwQP0zo0OPXKVidEbo6iae12mgDACq0kw",1774645636550]