trio – Sam & Max http://sametmax.com Du code, du cul Thu, 05 Sep 2019 08:22:03 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Super article invité sur Trio que l’auteur a oublié de titrer http://sametmax.com/super-article-invite-sur-trio-que-lauteur-a-oublie-de-titrer/ http://sametmax.com/super-article-invite-sur-trio-que-lauteur-a-oublie-de-titrer/#comments Thu, 14 Jun 2018 07:39:52 +0000 http://sametmax.com/?p=24605 Ceci est un post invité de touilleMan posté sous licence creative common 3.0 unported.

C’est bon vous avez cédé à la hype ?

Après un n-ème talk sur asyncio vous avez été convaincu que tout vos sites webs doivent être recodé dans cette techno ? Oui, surtout celui de la mairie de Gaudriole-sur-Gironde avec ses 50 visiteurs/jour, Django ça scalera pas et vous aurez sûrement besoin de websockets à l’avenir.

Et puis là pan ! En commençant à utiliser asyncio on se rend compte que ça va pas être aussi marrant que ce que vous a vendu l’enfoiré de hipster dans son talk avec son exemple de crawler web en 20 lignes :

  • la doc de asyncio fait 50 putain de pages, même les plus grands déclarent ne rien y comprendre
  • pdb est aux fraises, un step-over sur un await vous envoi à perpet’ dans l’event loop
  • il faut passer l’event loop en tant qu’argument à chaque fonction, évidemment vous n’allez pas le faire et utiliser get_event_loop() à la place. Et ça va merder sévère à un moment (typiquement quand vous ajouterez des tests non triviaux), et vous allez devoir corriger tout votre code.
  • régulièrement une stacktrace d’une coroutine ayant crashé dégueule de stdout sans que le programme ne bronche, autant de je m’en foutisme on se croirait revenu en PHP !
  • parlons-en de la stacktrace ! Impossible de savoir d’où vient la coroutine, encore une fois on nous renvoi dans les méandres de l’event loop.
  • Et pour initialiser/finaliser proprement votre système alors là c’est la fête totale

Je ne parle même pas des soucis ceinture-noir-2ème-dan du genre high-water mark qui vous tomberons dessus une fois l’appli en prod.

Lourd est le parpaing de la réalité sur la tartelette aux fraises de nos illusions…

1 – Pourquoi c’est (de) la merde ?

Pour faire simple asyncio a été pensé à la base comme une tentative de standardisation de l’écosystème asynchrone Python où chaque framework (Twisted et Tornado principalement) était incompatible avec les autres et devait re-créer son écosystème de zéro.

C’était la bonne chose à faire à l’époque, ça a eu beaucoup de succès (Twisted et Tornado sont maintenant compatible asyncio), ça a donné une killer-feature pour faire taire les rageux au sujet de Python 3 et ça a créé une émulsion formidable concernant la programmation asynchrone en Python.
Mais dans le même temps ça a obligé cette nouvelle lib à hériter des choix historiques des anciennes libs : les callbacks.

Pour faire simple un framework asynchrone c’est deux choses :

  • une grosse boucle infinie (la fameuse « event loop ») qui a configuré les appels d’IO au kernel en mode non-bloquant et qui poll ceux-ci en continu
  • un mécanisme pour garder trace de quel bout de code exécuter quand une IO donnée aura été terminée

Concernant le 2ème point, cela veut dire que si on a une fonction synchrone comme ceci :

def listen_and_answer(sock):
    print('start')
    data = sock.read()
    print('working with %s' % data)
    sock.write('ok')
    print('done')

Il faut trouver un moyen pour la découper en une série de morceaux de codes et d’IO.

Il y la façon « javascript », où on découpe à la main comme un compilo déroulerai une boucle :

def listen_and_answer(sock):
    print('start')

    def on_listen(data):
        print('working with %s' % data)

        def on_write(ret):
            print('done')

        sock.write('ok', on_write)

    sock.read(on_listen)

Et là j’ai fait la version simple sans chercher à gérer les exceptions et autres joyeusetés. Autant dire que quand un vieux dev Twisted vous dit le regard vide et la voix chevrotante qu’il a connu l’enfer, ne prenez pas ses déclarations à la légère.

Sinon la façon async/await si chère à asyncio :

async def listen_and_answer(sock):
    print('start')
    data = await sock.read()
    print('working with %s' % data)
    await sock.write('ok')
    print('done')

C’est clair, c’est propre, la gestion des exceptions est totalement naturelle, bref c’est du Python dans toute sa splendeur.
Sauf que non, tout ça n’est qu’un putain d’écran de fumée : pour être compatible avec Twisted&co sous le capot asyncio fonctionne avec des callbacks.

Vous vous souvenez de cette sensation de détresse mêlée d’hilarité devant une stacktrace d’un projet Javascript lambda d’où vous ne reconnaissez que la première ligne ? C’est ça les callbacks, et c’est ça que vous avez dans asyncio.

Concrètement le soucis vient du fait qu’une callback n’est rien d’autre qu’une fonction passée telle qu’elle sans aucune information quant à d’où elle vient. De fait impossible pour l’event loop asynchrone de reconstruire une callstack complète à partir de cela.
Heureusement async/await permettent à python de conserver ces informations de fonction appelante ce qui limite un peu le problème avec asyncio.
Toutefois en remontant suffisamment haut on finira toujours avec une callback quelque part. Et vous savez qui a l’habitude de remonter aussi haut que nécessaire ? Les exceptions.

import asyncio
import random

async def succeed(client_writer):
    print('Lucky guy...')
    # Googlez "ayncio high water mark" pour comprender pourquoi c'est
    # une idée à la con de ne pas avoir cette methode asynchrone
    client_writer.write(b'Lucky guy...')

async def fail(client_writer):
    raise RuntimeError('Tough shit...')

async def handle_request_russian_roulette_style(client_reader, client_writer):
    handlers = (
        succeed,
        succeed,
        succeed,
        fail,
    )
    await handlers[random.randint(0, 3)](client_writer)
    client_writer.close()

async def start_server():
    server = await asyncio.start_server(
        handle_request_russian_roulette_style,
        host='localhost', port=8080)
    await server.wait_closed()

asyncio.get_event_loop().run_until_complete(start_server())

Maintenant si on lance tout ça et qu’on envoie des curl localhost:8080 on va finir avec:

$ python3 russian_roulette_server.py
Lucky guy...
Lucky guy...
Task exception was never retrieved
future:  exception=RuntimeError('Tough shit...',)>
Traceback (most recent call last):
  File "ex.py", line 18, in handle_request_russian_roulette_style
    await handlers[random.randint(0, 3)](client_writer)
  File "ex.py", line 9, in fail
    raise RuntimeError('Tough shit...')
RuntimeError: Tough shit...
Lucky guy...
Task exception was never retrieved
future:  exception=RuntimeError('Tough shit...',)>
Traceback (most recent call last):
  File "ex.py", line 18, in handle_request_russian_roulette_style
    await handlers[random.randint(0, 3)](client_writer)
  File "ex.py", line 9, in fail
    raise RuntimeError('Tough shit...')
RuntimeError: Tough shit...

Le problème saute aux yeux: asyncio.start_server gère sa tambouille avec des callbacks et se retrouve bien embêté quand notre code remonte une exception. Du coup il fait au mieux en affichant la stacktrace et en faisant comme si de rien n’était. C’est peut-être le comportement qu’on attend d’un serveur web (encore que… si aviez configuré logging pour envoyer dans un fichier vous êtes bien baïzay) mais il existe des tonnes de usecases pour lesquels ça pose problème (et de toute façon on n’a vu que la partie émergée de l’iceberg d’emmerdes qu’est la programmation asynchrone).

Bref, si vous voulez en savoir plus, allez lire ce post, d’ailleurs allez lire tous les posts du blog, ce mec est un génie.

2 – Trio, une façon de faire de l’asynchrone

Ce mec en question, c’est Nathaniel J. Smith et il a eu la très cool idée de créer sa propre lib asynchrone pour Python: Trio

L’objectif est simple: rendre la programmation asynchrone (presque) aussi simple que celle synchrone en s’appuyant sur les nouvelles fonctionnalités offertes par les dernières versions de Python ainsi qu’un paradigme de concurrence innovant. Cette phrase est digne d’un marketeux, vous avez le droit de me cracher à la gueule.

Concrètement ce que ça donne:

# pip install trio asks beautifulsoup4
import trio
import asks
import bs4
import re


# Asks est un grosso modo requests en asynchrone, vu qu'il supporte trio et curio
# (une autre lib asynchrone dans le même style), il faut donc lui dire lequel utiliser
asks.init('trio')


async def recursive_find(url, on_found, depth=0):
    # On fait notre requête HTTP en asynchrone
    rep = await asks.get(url)
    print(f'depth {depth}, try {url}...')

    # On retrouve le corps de l'article grace à beautiful soup
    soup = bs4.BeautifulSoup(rep.text, 'html.parser')
    body = soup.find('div', attrs={"id": 'mw-content-text'})

    # On cherche notre point Godwin
    if re.search(r'(?i)hitler|nazi|adolf', body.text):
        on_found(url, depth)

    else:
        async with trio.open_nursery() as nursery:
            # On retrouve tous les liens de l'article et relance le recherche
            # de manière récursive
            for tag in body.find_all('a'):
                if tag.has_attr('href') and tag.attrs['href'].startswith('/wiki/'):
                    child_link = 'https://en.wikipedia.org' + tag.attrs['href']
                    # On créé une nouvelle coroutine par lien à crawler
                    nursery.start_soon(recursive_find, child_link, on_found, depth+1)


async def godwin_find(url):
    results = []

    with trio.move_on_after(10) as cancel_scope:
        def on_found(found_url, depth):
            results.append((found_url, depth))
            cancel_scope.cancel()

        await recursive_find(url, on_found)

    if results:
        found_url, depth = results[0]
        print(f'Found Godwin point in {found_url} (depth: {depth})')
    else:
        print('No point for this article')


trio.run(godwin_find, 'https://en.wikipedia.org/wiki/My_Little_Pony')

L’idée de ce code est, partant d’un article wikipedia, de crawler ses liens récursivement jusqu’à ce qu’on trouve un article contenant des mots clés.

Au niveau des trucs intéressants:

async with trio.open_nursery() as nursery:
    for tag in body.find_all('a'):
        if tag.has_attr('href') and tag.attrs['href'].startswith('/wiki/'):
            child_link = 'https://en.wikipedia.org' + tag.attrs['href']
            nursery.start_soon(recursive_find, child_link, on_found, depth+1)

En trio, une coroutine doit forcément être connectée à une nurserie. Cela permet deux choses:

  • Rattacher la coroutine à sa coroutine parente, de cette façon (et vu que trio est implémenté intégralement en utilisant async/await au lieu de callbacks), on a donc une stacktrace claire et une exception sera toujours propagée jusqu’à la racine du programme si il le faut.
  • Borner la durée de vie de la coroutine. La nurserie est un context manager asynchrone, une fois qu’on arrive à la fin du async with, la nursery bloque tant que toutes les coroutines qu’elle gère n’ont pas terminé. Si une coroutine raise une exception, la nursery va pouvoir cancel les autres coroutines avant de re-raise l’exception en question (cf. le point précédent)

Quel intérêt à borner la durée de vie des coroutines ? Si on avait voulu écrire un truc équivalent en asyncio on aurait sans doute utilisé asyncio.gather:

coroutines = [recursive_find(link) for link in links]
await asyncio.gather(coroutines)

Maintenant on fait tourner ce code avec une connection internet un peu faiblarde (au hasard sur la box Orange de Sam ces temps ci…) les ennuis auraient commencé dès qu’une requête http aurait timeout.
L’exception de timeout aurait été récupérée par asyncio.gather qui l’aurait relancé sans pour autant fermer les autres coroutines qui auraient continué à crawler wikipedia en créant des centaines de coroutines (oui recursive_find est un peu bourrin).
De fait si on se place dans le cas d’un code tournant longtemps (typiquement on a un serveur web qui a lancé notre code dans le cadre du traitement d’une requête entrante) on va avoir bien du mal à retrouver l’état ayant mené à ce bordel.

Du coup en trio la seule solution pour avoir une coroutine qui survit à son parent c’est de lui passer une nursery en paramètre:

async def work(sleep_time, nursery):
    await trio.sleep(sleep_time)
    print('work done !')
    # Je vous ai dit qu'une nurserie contient automatiquement un cancel scope ?
    nursery.cancel_scope.cancel()

async def work_generator(nursery):
    print('bootstrapping...')
    await trio.sleep(1)
    for sleep_time in range(10):
        nursery.start_soon(work, sleep_time, nursery)

async def stop_a_first_work_done():
    async with trio.open_nursery() as nursery:
        await work_generator(nursery)
        print('Waiting for a work to finish...')

Un autre truc cool:

with trio.move_on_after(10) as cancel_scope:
    def on_found(found_url, depth):
        results.append((found_url, depth))
        cancel_scope.cancel()

    await recursive_find(url, on_found)

Vu qu’en trio on se retrouve avec un arbre de coroutines, il est très facile d’appliquer des conditions sur un sous-ensemble de l’arbre. C’est le rôle des cancel scope.
Comme pour les nursery, les cancel scope sont des contexts managers (mais synchrone ceux-ci). On peut les configurer avec un timeout, une deadline, ou bien tout simplement les annuler manuellement via cancel_scope.cancel().

Dans notre exemple, on définit un scope dont on sortira obligatoirement au bout de 10s. Pour éviter d’attendre pour rien, on annule le scope explicitement dans la closure appelée quand un résultat est trouvé.
Vu que les nurseries définies à chaque appel de recursive_find se trouvent englobées par notre cancel scope, elles seront automatiquement détruites (et toutes les coroutines qu’elles gèrent avec).

Pour faire la même chose avec asyncio bonne chance:

  • soit on passe un argument de timeout à notre appel pour récupérer la requête HTTP, mais dans ce cas pour peu que chaque requête soit individuellement plus courte que le timeout on ne s’arrêtera jamais
  • soit on gère à la mano le temps à coup de time.monotonic() en passant le temps restant autorisé aux coroutines filles. Bonjour la gueule du code.

En plus comme en parlait un mec (décidemment !), la gestion du timeout dans une socket tcp est foireuse, il suffit de recevoir un paquet (et une requête entière peut contenir beaucoup de paquets !) pour que le timeout soit remis à zéro. Donc encore une fois pas de garanties fortes quant à quand le code s’arrêtera.

3 – Eeeeet c’est tout !

Au final la doc de l’api de trio pourrait tenir sur l’étiquette de mon slip: pas de promise, de futurs, de tasks, de pattern Protocol/Transport legacy. On se retrouve juste avec la sainte trinité (j’imagine que c’est de là que vient le nom) async/await, nursery, cancel scope.

Et évidemment maintenant, l’enfoiré de hipster qui vous vend une techno à coup de whao effect avec un crawler asynchrone de 20 lignes c’est moi…

Remarquez si vous préférez la version longue je vous conseil cet excellent article de Nathaniel (je vous ai dit que ce mec était un génie ?).

4 – L’écosystème

C’est là où on se rend compte que asyncio est malgré ses lacunes une super idée: il a suffit d’écrire une implémentation de l’event loop asyncio en trio pour pouvoir utiliser tout l’écosystème asyncio (ce qui inclus donc Twisted et Tornado, snif c’est beau !).

Allez pour le plasir un exemple d’utilisation de asyncpg depuis trio:

import trio_asyncio
import asyncpg


class TrioConnProxy:
    # Le décorateur permet de marquer la frontière entre trio et asyncio
    @trio_asyncio.trio2aio
    async def init(self, url):
        # Ici on est donc dans asyncio
        self.conn = await asyncpg.connect(url)

    @trio_asyncio.trio2aio
    async def execute(self, *args):
        return await self.conn.execute(*args)

    @trio_asyncio.trio2aio
    async def fetch(self, *args):
        return await self.conn.fetch(*args)


async def main():
    # Ici on est dans trio, c'est la fête

    conn = TrioConnProxy()
    await conn.init('postgresql:///')

    await conn.execute('CREATE TABLE IF NOT EXISTS users(name text primary key)')

    for name in ('Riri', 'Fifi', 'Loulou'):
        await conn.execute('INSERT INTO users(name) VALUES ($1)', name)

    users = await conn.fetch('SELECT * FROM users')
    print('users:', [user[0] for user in users])


# trio_asyncio s'occupe de configurer l'event loop par défaut de asyncio
# puis lance le trio.run classique trio_asyncio.run(main)

En plus de ça trio vient avec son module pytest (avec gestion des fixtures asynchrones s’il vous plait) et Keneith Reitz a promis que la prochain version de requests supporterait async/await et trio nativement, elle est pas belle la vie !

]]>
http://sametmax.com/super-article-invite-sur-trio-que-lauteur-a-oublie-de-titrer/feed/ 11 24605
Go to (in asyncio) considered harmful http://sametmax.com/go-to-in-asyncio-considered-harmful/ http://sametmax.com/go-to-in-asyncio-considered-harmful/#comments Thu, 07 Jun 2018 07:31:29 +0000 http://sametmax.com/?p=24534 Trio, une stack toute neuve concurrente d'asyncio, lui a fait écho 50 ans plus tard, ça a beaucoup discuté sur les mailing lists et les bugs trackers.]]> Dijkstra était un intellectuel pédant, mais quand il a écrit cette lettre célèbre, il a comme souvent mis le doigt sur un truc fondamental. Et quand l’auteur de Trio, une stack toute neuve concurrente d’asyncio, lui a fait écho 50 ans plus tard, ça a beaucoup discuté sur les mailing lists et les bugs trackers.

Nathaniel J. Smith, le dev susnommé, en a profité pour introduire une nouvelle primitive, actuellement surnommée la nursery, pour répondre au problème. Une idée visiblement tellement bonne que notre Yury préféré a décidé de la porter à asyncio. La boucle d’événements est bouclée, si je puis dire.

Mais une autre chose intéressante en découle : on a mis en lumière la présence d’un goto dans asyncio, et qu’il y a de bonnes pratiques, validées par Guido himself, pour coder avec cette lib pour éviter les douleurs.

What the fuck are you talking about ?

Le problème du goto, c’est que l’instruction permet d’aller de n’importe où à n’importe où. Cela rend le flux du programme très dur à suivre. Pour éviter cela, on a catégorisé les usages clean du goto: répéter une action, changer de comportement en fonction d’un test, sortir d’un algo en cas de problème, etc. Et on en a fait des primitives : les if, les while, les exceptions… Dans les langages les plus modernes, on a carrément viré le goto pour éviter les abus et erreurs. Joie.

Dans asyncio, le “goto” en question se trouve quand on veut lancer des tâches en arrière plan, comme ceci :

import asyncio as aio
loop = aio.get_event_loop()
aio.ensure_future(foo())  # GOTO !
aio.ensure_future(bar())  # GOTO !
loop.run_forever()

Le problème d’ensure_future() est multiple:

  • Comme son nom l’indique, cette fonction retourne un objet… Task. Ca n’a rien à voir, mais je tenais à dire à quel point c’était con de l’avoir nommé ainsi (même si techniquement, Task hérite de Future).
  • Cette ligne ne garantit en aucun cas que foo() ou bar() seront terminées à une zone précise du code. Tout au plus que tuer la boucle tue les taches. Leur flux d’exécution est complètement freestyle et décorrélé de tout le reste du programme, ainsi que de l’une de l’autres. Si ces coroutines font des await, on peut basculer de n’importe où du programme vers elles et inversement à tout moment. goto
  • Cette ligne schedule le démarrage de foo() et bar() dès que la boucle peut les lancer. Ici la boucle ne tourne pas encore. Plus le programme est complexe, plus il va devenir difficile de savoir à quelle étape logique les coroutines vont démarrer.

En prime run_forever() est un piège à con, car les exceptions qui arrivent dans la boucle sont logguées, mais ne font pas crasher le programme, ce qui rend le debuggage super rude, même avec debug mode activé (dont de toute façon personne ne soupçonne l’existence).

La solution asyncio

import asyncio as aio
loop = aio.get_event_loop()
loop.run_until_complete(aio.gather(foo(), bar())

En plus d’être plus court, les exceptions vont faire planter le programme, la loop s’arrêtera quand les coroutines auront fini leur taff, leur flux a un début et une fin encapsulés par le gather(). Ceci est encore plus visible si on met le même code à l’intérieur d’une coroutine à l’intérieur d’une coroutine à l’intérieur d’une coroutine plutôt qu’à la racine du programme. En effet dans un exemple si simple, on se borne au démarrage et à l’arrêt de la boucle. Mais je suis paresseux.

Donc, c’est la bonne pratique, mais tout le monde ne le sait pas.

Pardon, correction.

Tous les devs Python ne connaissent pas asyncio. Parmi ceux qui connaissent asyncio, une petite partie comprend comme ça marche.

Dans ce lot rikiki, un pouillième sait que c’est la bonne pratique.

En fait, gather() est probablement la fonction la plus importante d’asyncio, et pourtant elle apparaît à peine dans la doc. C’est la malédiction d’asyncio, une lib que tout le monde attendait pour propulser Python dans la league des langages avec frameworks modernes, mais qui commence à peine à devenir utilisable par le commun des mortel en 2018. Et encore.

Il ne faut jamais utiliser ensure_future() à moins de vouloir attacher un callback à la main dessus, ce qui n’est probablement jamais ce que vous voulez à cette époque merveilleuse ou existe async/await. ensure_future() est un goto, gather() est un concept de plus haut niveau.

Mais deux problèmes demeurent…

Contrairement au goto banni de Python, ensure_future() est là, et va rester. Donc n’importe quel connard peut dans un code ailleurs vous niquer profond, et en tâche de fond.

ensure_future() (ou son petit frère EventLoop.create_task()) reste le seul moyen valable pour lancer une tâche, faire quelque chose, lancer une autre tâche, puis enfin faire un gather() sur les deux tâches:

async def grrr():
    task1 = aio.ensure_future(foo())
    # faire un truc pendant que task1 tourne
    task2 = aio.ensure_future(bar())
    # faire un truc pendant que task1 et task2 tournent
    # On s'assure que tout se rejoint à la fin:
    await aio.gather(task1, task2)

Et puis, faire une pyramide de gather() dans tout son code pour s’assurer que tout va bien de haut en bas, c’est facile à rater.

La nursery : la solution de trio

Une nursery agit comme un scope qui pose les limites du cycle de vie des tâches qui lui sont attachées. C’est un gather(), sous stéroide, et avec une portée visuellement claire:

async def grrr():
    async with trio.open_nursery() as nursery:
        task1 = nursery.start_soon(foo)
        # faire un truc pendant que task1 tourne
        task2 = nursery.start_soon(bar)
        # faire un truc pendant que task1 et task2 tournent

Les taches sont garanties, à la sortie du with, de se terminer. Le ensure_future() n’a pas d’équivalent en trio, et donc aucun moyen de lancer un truc dans le vent sans explicitement lui passer au moins une nursery à laquelle on souhaite l’attacher.

Résultat, on ne peut plus faire de goto, et le flux du program est clair et explicite.

Notez que, tout comme if et while ne permettaient rien qu’un utilisateur soigneux de goto ne pouvait faire, la nursery ne permet rien qu’un utilisateur soigneux de ensure_future() ne peut faire. Mais ça force un ensemble de bonnes pratiques.

Évidemment, on peut ouvrir une nursery dans un bloc d’une autre nursery, ce qui permet d’imbriquer différentes portées, comme on le ferait avec un begin() de transaction de base de données. Or, une exception à l’intérieur d’une nursery bubble naturellement comme toute exception Python, et stoppe toutes les tâches de la nursery encore en train de tourner. Alors qu’avec asyncio vous l’avez dans le cul.

En définitive, c’était la pièce manquante. La moitié du boulot avait était faite quand on a introduit un moyen de gérer des tâches asynchrones qui dépendent les unes des autres, en remplaçant les callbacks par un truc de haut niveau : async/await. Il restait la gestion des tâches en parallèle qui se faisait encore selon les goûts et compétences de chacun, mais la nursery va remplir ce vide.

Cela devrait être intégré à asyncio en Python 3.8, soit une bonne année et demie pour ceux qui ont la chance de pouvoir faire du bleeding edge.

Comme certains ne voudront pas attendre, je vous ai fait un POC qui vous montre comment ça pourrait marcher. Mais cette version ne sera jamais utilisée. En effet, elle intercepte ensure_future() (en fait le create_task() sous-jacent) pour attacher son résultat à la nursery en cours, évitant tout effet goto, et ça péterait trop de code existant. Mon pognon est plutôt sur un gros warning émis par Python quand on fait une gotise.

Dernier mot: s’il vous plaît, allez voter pour change le nom de nursery. C’est beaucoup trop long à taper pour un truc qu’on va utiliser tout le temps.

]]>
http://sametmax.com/go-to-in-asyncio-considered-harmful/feed/ 26 24534