Longues requĂŞtes avec aiohttp
Dans la note précédente j’écris avoir laissé aiohttp de côté, avoir plutôt tapé un mini-procotole maison parce que c’était très simple et évitait l’introduction d’une dépendance supplémentaire. Mais quand même, j’étais curieux de regarder aiohttp alors j’ai pris prétexte d’un autre développement pour regarder.
L’idée de base est simple, mon intégration continue fonctionne mais je fais parfois preuve d’impatience et je regarde les logs pour voir où ça en est. En pratique ça veut dire ssh
vers le serveur et journalctl -u git-eobuilder.service -f
. De là donc avoir une URL que je pourrais consulter pour avoir les logs, et tant qu’à faire qu’il n’y ait pas à recharger la page pour voir les logs continuer à arriver.
Vraiment tout bête et affaire sans doute réglée en cinq minutes avec un CGI façon :
#! /bin/sh echo "Content-type: text/plain; charset=utf-8" echo "" journalctl -u git-eobuilder.service -f
(cela sans tester, il y a peut-être une astuce ou l’autre côté paramétrage nginx/fastcgi pour éviter le buffering)
Mais ce n’est pas cette vieille recette qui va me faire regarder aiohttp, c’est parti donc pour autre chose et l’hello world bidon ressemble à :
from aiohttp import web async def handle(request): return web.Response(text='hello world') app = web.Application() app.add_routes([web.get('/', handle)]) web.run_app(app)
Et déjà ici j’aurais pu voir ce qui allait un peu coincer : j’ai l’impression qu’aiohttp ne sait pas très bien se positionner, fait client et serveur, et côté serveur intègre des fonctionnalités relevant davantage d’un framework complet, comme le système de routage vu ici. Bon, c’est sans doute juste moi trompé sur le nom, je m’attendais à quelque chose de basique et minimal et je tombe sur autre chose, mais ça a des conséquences pratiques sur la documentation, que je trouve pour la peine mal agencée, m’obligeant à visiter le code source pour des bouts qui me semblent élémentaires. Dont le cas qui m’intéresse, produire une réponse longue, sans coupure, il y a juste mention à un moment :
A request handler must be a coroutine that accepts a Request
instance as its only argument and returns a StreamResponse
derived (e.g. Response
) instance
qui révèle l’existance de cette classe StreamResponse qui doit être la réponse qu’il me faut mais zéro exemple d’utilisation. Reste donc la description technique détaillée de la classe et le code source.
Pareil pour un autre point que j’imagine basique, démarrer sur autre chose que le port par défaut, l’info ne se trouve pas, sauf pareil dans la référence (de run_app).
En passant je me trouve donc à regarder le code et confession, je trouve ça moche et je suis loin d’être prêt à accueillir du typage explicite en Python.
Passons, réponse simple pour démarrer sur une interface et port particulier :
web.run_app(app, host='127.0.0.1', port=8123)
et exemple pour StreamResponse :
async def handle(request): response = web.StreamResponse( headers={'content-type': 'text/plain; charset=utf-8'}) response.enable_chunked_encoding() await response.prepare(request) process = await asyncio.create_subprocess_exec( '/bin/journalctl', '-f', stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) while process.returncode is None: data = await process.stdout.readline() await response.write(data) return response
c’est-à -dire au final pas compliqué, retourner l’objet comme il vient et laisser la boucle asynchrone avant l’alimenter. (et ça repointe ce que je trouve très chouette avec asyncio, ça peut vraiment donner du code qui se lit linéairement)
Sauf que si jamais la requête HTTP est interrompue par le client, ça va être absorbé silencieusement et le processus démarré va être laissé intact et trainer, donc il y a à encadrer ça :
async def handle(request): response = web.StreamResponse( headers={'content-type': 'text/plain; charset=utf-8'}) response.enable_chunked_encoding() await response.prepare(request) process = await asyncio.create_subprocess_exec( '/bin/journalctl', '-f', stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) try: while process.returncode is None: data = await process.stdout.readline() await response.write(data) except asyncio.CancelledError: if process.returncode is None: process.terminate() raise return response
Autre chose, j’aurais aimé faire ça depuis une classe pour porter un peu de contexte et taper ce code dans une méthode __call__, façon :
class Handler: async def __call__(self, request): response = web.StreamResponse( headers={'content-type': 'text/plain; charset=utf-8'}) (...)
ça fonctionne mais cette construction est mal détectée et aiohttp affiche
DeprecationWarning: Bare functions are deprecated, use async ones
ce qui n’est pas joli.
De tout cela pour terminer j’ai quand même tapé mon code dans un dépôt (tailerd) et dans l’élan ça gère davantage, genre ça se configure via un fichier :
[config] command1 = /bin/journald -u whatever -f command2 = /usr/bin/tail -f /var/log/whatever.log
Pour permettre qu’un accès à /command1/ retourne le résultat de la première commande, et /command2/ de la seconde; ou dans mon cas :
[config] logs = /bin/journald -u git-eobuilder.service -f
pour avoir une URL /logs/ qui contient la part de logs qui m’intéresse.
Mais je ne pense pas le projet particulièrement intéressant quand un CGI de quatre lignes fait le job.
Conclusion, aiohttp, bof, allez quand mĂŞme, it was ok, 2 Ă©toiles.