Hasura / Hydra / Squid un peu de recul s'impose

Suite aux discussions récentes sur le fil Duniter-squid - #41 by poka et à la MR !16, je pense qu’il est bon de prendre un peu de recul sur les raisons des choix de stack.

Le premier indexeur duniter-indexer réalisé par @ManUtopiK et aujourd’hui archivé est né alors que squid n’était pas du tout mature, notamment suite à une discussion (2022-02-22) qui a suivi un ticket subsquid :

discussion que j’ai illustrée ainsi :

Cette preuve de concept étant le premier indexeur disponible pour Duniter-v2, il a naturellement été intégré aux clients Ğecko et Ğcli.

Plus tard, motivé par les mêmes besoins qu’elois d’avoir un indexeur substrate générique

j’ai tenté de reprendre l’indexeur Hydra que j’utilisais aussi, mais comme ça ne marchait pas et qu’entre temps Hydra était devenu Subsquid, je me suis lancé dans un indexeur subsquid (sans voir le dépôt de elois)

  • 2023-11-20 je crée un dépôt d’après le tutoriel squid
  • 2023-11-22 j’ajoute les composants de “giant squid” pour avoir accès à une indexation générique
  • à ce stade, j’avais ce qu’il me fallait : de quoi débugger la blockchain avec une indexation de tous les blocs, extrinsics, calls et événements, mais j’ai décidé de pousser l’expérience un peu plus loin pour voir où ça me mènerait
  • 2023-11-26 en quelques jours de travail, j’arrive à un indexeur aussi complet que duniter-indexer et
    • sans les bugs de duniter-indexer
    • avec des types typescript entièrement dérivés des metadata du runtime
    • avec la gestion des blocs non finalisés et du rewind des forks BABE
    • avec de bien meilleures perspectives de développement (notamment runtime upgrades)
    • mais avec quelques limitations liés à des choix discutables de subsquid
      • un bug de typeorm qui empêche de checker la null safety #228 (légèrement gênant mais on peut vivre avec)
      • l’impossibilité de choisir des primary key qui ne soient pas des string (légèrement gênant également, mais pas dramatique)

Il m’est donc apparu clairement que squid était la direction à prendre, et je suis encore entièrement de cet avis. Entre temps, @poka s’est fait la main sur squid (janvier 2024) et a entrepris d’implémenter des fonctionnalités plus ambitieuses (février 2024) avec une fonction SQL pour les DU par membre. Ce faisant, il a découvert des limites de squid :

  • l’absence de sécurité par défaut contre des requêtes graphql malicieuses (aspirer la base de données entière ou des récursions infinies) dans le processeur graphql (alors que Hasura a ça par défaut)
  • la prise en charge limitée des requêtes SQL custom dans l’overlay graphql (alors que Hasura fait ça par défaut)
  • les fonction de paginations moins avancés que l’API relay de Hasura (beta)
  • (à completer si j’ai oublié quelque chose, mais il me semble que c’est les deux plus gros points)

Et donc tout feu tout flamme (en même temps c’est poka :fire: :blue_heart: :heart: ), il s’est lancé dans l’intégration de Hasura à duniter-squid pour obtenir le meilleur des deux mondes (21-22-23 février). (je vois ça en direct parce que poka bosse chez moi en ce moment).

Mais là on se frotte à deux logiques contraires :

  • d’une part squid part d’un schéma annoté et en déduit les types orm et la structure de la base de données
  • d’autre part Hasura part de la structure de la base de données et en déduit un schéma graphql

Il n’est donc pas évident de savoir qui doit gérer les migrations de la base de données, et comment profiter du meilleur des deux mondes sans subir de conflits. Pour réfléchir à cette question, voici les points qui me semblent importants :

  • garder le contrôle sur le schéma sans que cela soit trop complexe (confort des devs de clients)
  • conserver une définition de la base de données sans action manuelle (charge de travail et homogénéité)
  • pouvoir remplir la base de données avec des typeorm, donc sans sql (confort et vitesse de développement)
  • se passer de l’écriture du résolveur graphql pour les requêtes sql custom (complexité et source de bugs)
  • profiter d’une sécurisation par défaut sans action manuelle (l’API principale ne doit pas être fragile)

Trouver la bonne architecture pour répondre à ces contraintes peut prendre un peu de temps, donc je pense qu’il faut éviter de se lancer tête la première dans l’intégration de Hasura coûte que coûte de manière prématurée.

J’invite les développeurs de clients à se fier au schéma actuel, que l’on doit encore compléter et stabiliser. De mon côté je vais essayer de trouver une autre approche pour intégrer Hasura à duniter-squid qui permette de conserver le contrôle sur le schéma. @poka il faut qu’on discute des priorités sur duniter-squid et les datapods, à mon avis intégrer Hasura de cette façon est un peu prématuré, conceptuellement on pourrait même le faire après la migration sans trop de problème.

Bon, c’est pas clair, on va peut-être trouver la solution plus vite que prévu.

1 Like

Quand @poka était chez moi, il a bossé toutes les nuits pour arriver au bout de la migration Hasura. Quasiment tout est en place, voici ce qu’il manque :

  • la pagination par curseur
    celle-ci est gérée dans une API beta dans Hasura, et un bug empêche de combiner la limitation de nombre de résultat et un curseur allant au delà de cette limitation. Il faut donc utiliser la pagination par offset qui est moins pratique
  • la non-nullabilité de certaines relations
    à cause d’une limitation de typeORM qui empêche de pousser un objet avec des relations non nullables, la structure de la base de données ne peut pas assurer la non nullabilité de ces champs. Jusque là le schéma n’était pas altéré et le problème était caché sous le tapis, mais comme Hasura déduit le schéma de la structure de la base de données, le problème refait surface. En pratique, ça consiste à ajouter un ! ou un .unwrap() côté client plutôt que de cacher le problème côté serveur. Embêtant mais a priori pas gênant.
  • les enum
    dans le moteur graphql de squid, les enum n’étaient pas gérées directement dans la base de données mais par le résolveur. On a donc actuellement des string en base de données qui ne sont pas des enums, et donc ça apparaît dans le schéma graphql. Valable pour le IdentityStatus, le SmithStatus, et le EventType. Pas dramatique mais léger manque de confort pour les devs de clients.

Pour l’instant :

Il reste à adapter les clients, à commencer par Ğcli. Dès que celui-ci est adapté pour Hasura, je mets à jour mon instance. En attendant Cesium-v2 peut utiliser l’instance Axiom.

2 Likes

Merci pour ce checkpoint.
J’ai fait les changements sur Ğcli quand j’étais chez toi mais j’ai oublié de les pousser, je fais ça.
Mais c’est un wip, car il reste un “bug” (enfin c’est une feature) que tu n’a pas mentionné et que j’étais en train de traiter avec un autre jolie wip côté indexer sur la branche convert-hash-bytes-to-hexa: wip (50c8b561) · Commits · nodes / duniter-squid · GitLab

Le format des hash en DB sont des bytes, et sont donc affiché avec \\ au lieu de 0x via l’API graphql hasura.
Plutôt que de parser ça comme un mal propre côté gcli j’ai voulu traiter le problème à la racine, côté indexer.
Ca fonctionne mais le soucis c’est que ça m’oblige à créer un computed field qui convertie au bon format, et donc un nouveau champ graphql style hashHexa. Des plus ça demande à traiter ça pour chaque hash, je n’ai pas encore trouvé de solution générique.

Les enums sont également en wip sur une branche, plus compliqué que prévus …


Lors de tests rapides, on constate qu’en terme de latence, le moteur graphql de squid et celui de Hasura sont dans le même ordre de grandeur, mais avec une légère réactivité supplémentaire relevé avec le moteur de squid.


Je vais finir de traiter ces derniers points, mais je dois avouer que l’intégration de Hasura a été bien moins straightforward qu’annoncé. C’est dû au fait que le schéma de base de donnée n’est pas aussi expressif que ce que je pensais. Certaines informations ne sont présente que dans le schema.graphql, et malgré le parseur typeORM de squid, cela a nécessité la génération de certaines migration SQL squid custom.

Je vais finir par arriver au bout de cette intégration Hasura, et ça me semble proprement intégré et facile à maintenir et faire évolué vue que j’ai automatisé pas mal de choses, mais plus j’avance, plus je me demande si le jeu en vaut la chandelle.

Une fois terminé je reprendrais un tableau comparatif plus complet pour comparer les deux solutions.

3 Likes

Il manque aussi une fonctionnalité essentielle pas encore intégré à la version hasura:

curl -s -H "Content-Type: application/json" -d '{"query":"{ galuel }"}' https://subsquid.gdev.coinduf.eu/graphql | jq -r '.data.galuel'

Je vois pas pourquoi ce serait mieux d’adopter une représentation string qui commence par 0x plutôt que \\. Pas besoin de se faire chier côté serveur pour remplacer deux caractères qu’on ignore de toute façon côté client. Ce qui suit reste une représentation hexadécimale, il n’y a rien à parser.

Parceque

  • Certaines lib interprêtent le 0x des string et les traitent alors automatiquement comme de l’hexa
  • C’est une convention largement répandu

Mais si ça s’avère trop compliqué a traiter, alors je laisse tomber.

Et est-ce que tu as une idée de pourquoi Hasura nous sérialise ça sous forme de string en \\x ? Est-ce qu’on peut le paramétrer pour changer la sérialisation ?

Justement je n’ai pas trouvé comment faire (encore).
C’est dû au fait qu’en DB les champs hash sont des bytea, car déclarés comme Bytes dans le schema graphql. Il tente d’échapper les caractères spéciaux à la lecture de la string via l’API, car ce sont êtres des bytes normalement.
Hors, c’est bien du format hexa qui est stocké, donc en réalité c’est le format Bytes du schema graphql qui est a remettre en question.

Ah oui, je savais pas. C’est ça qui est con, tu as raison. Encore une connerie de squid. Stocker des bytes comme des string, c’est étrange.

[edit] comme tu dis :

Au cas où je t’ai mis un exemple de déserialisation dans ta branche. [edit: gcli]

1 Like

Je me suis pas encore repenché dessus je vais ptetre reprendre là et explorer ça, merci :slight_smile:

J’ai réfléchi à ce problème de curseur, et de profondeur d’arbre de subsquid. J’aimerai qu’on en reparle tous ensemble au RML18, car j’aimerai comprendre plus en détail les limitations, et voir un peu d’avantage le code… Avoir aussi l’avis de cgeek.

@poka tu serais prêt pour une démo + explication de code ?
En particulier les limitations et l’écriture de resolver, pour subsquid.

2 Likes

Voici le resolver squid pour l’historique des DU par comptes: src/server-extension/resolvers/du_history_resolver.ts · c44964da9d8858d03253a3ddfbbfe9e2333135c6 · nodes / duniter-squid · GitLab

Il n’est donc plus présent sur la version Hasura car généré automatiquement par ce dernier.

La requête SQL utilisé pour ce resolver: src/server-extension/sql/du_history_query.sql · c44964da9d8858d03253a3ddfbbfe9e2333135c6 · nodes / duniter-squid · GitLab

C’est la même requête SQL renseigné comme computed field dans Hasura.

Il n’y a pas de limitations particulière, dans un cas comme dans l’autre. Simplement dans le cas de squid, la nécessité de ré-implémenter les clauses conditionnelles, les limites, offset, en SQL, avec son parsing TS côté resolver.

Pour la demo, les instances sont déjà en ligne et rappelé par Hugo juste au dessus.
Je veux bien faire des explications de codes si nécessaire, mais je ne compte rien prévoir de ce côté là, je pourrais répondre aux questions.

1 Like

J’ai parlé un peu vite, les hash sont bien converties en bytes (Buffer) (sans quoi ça ne pourrais pas être stocké dans un champ sql bytea…)

export function decodeHex(value: string): Buffer {
    assert(isHex(value))
    return Buffer.from(value.slice(2), 'hex')
}

Je suis étonné que l’API ne nous affiche pas un Uint8Array du coup.
Du coup en local, en stockant les hash en string c’est sûr que ça ressort bien en hexa

{
  "data": {
    "block": [
      {
        "id": "0000158019-2c002",
        "height": 158019,
        "hash": "0x2c002dd9c261ada90f34b1478a559edc42db525a15f829659f2f1e454f547488"
      }
    ]
  }
}

Mais bon on perds gros en optimisation de stockage (32 octets contre 66 octets par hash), quand on compte 3 hash par block + les hash d’extrinsics, et 1 blocs toutes les 6 secondes, ça grimpe très vite. Sans comptez les perfs de calcule sur du binaire comparer à des strings pour les tries et conversions graphql.

Il s’agit donc d’une simple différence de sérialisation. Postgre sérialise les chaines bytea en hexa avec \x, là où en générale ailleurs, on sérialise avec 0x. Ce sont en fait 2 conventions hexa (je ne connaissais pas celle de postegres). Le format JSON échappe le caractère \ avec \, ce qui donne \\ (passionnant hein?)

Je regarde un peu si on peut facilement changer cette serialization côté postgres ou hasura, mais je crois que c’est compromis.

Une issue est ouverte à ce sujet, qui en référence une autre: Return `bytea` hex values without `\\x` prefix · Issue #48 · bitauth/chaingraph · GitHub

Mais branche ajoute de la lourdeur non nécessaire, donc on va rester comme ça sur ce point je pense.
Sinon on peut aussi forker Hasura pour résoudre le ticket #48


Sinon en résultat ça donne ça, les champs hash, mais avec Hex devant:

query MyQuery {
  block(limit: 1, orderBy: {height: ASC}) {
    hashHex
    parentHashHex
    stateRootHex
    validatorHex
  }
  extrinsic(limit: 1, orderBy: {index: ASC}) {
    hashHex
  }
}

{
  "data": {
    "block": [
      {
        "hashHex": "0xc184c4ccde8e771483bba7a01533d007a3e19a66d3537c7fd59c5d9e3550b6c3",
        "parentHashHex": "0xc184c4ccde8e771483bba7a01533d007a3e19a66d3537c7fd59c5d9e3550b6c3",
        "stateRootHex": "0xc184c4ccde8e771483bba7a01533d007a3e19a66d3537c7fd59c5d9e3550b6c3",
        "validatorHex": "0xc184c4ccde8e771483bba7a01533d007a3e19a66d3537c7fd59c5d9e3550b6c3"
      }
    ],
    "extrinsic": [
      {
        "hashHex": "0x2c3a488a9b4053b938745ca5114372db78153d3ebff9c094b05dcac34713d2f5"
      }
    ]
  }
}

J’ai tourné le truc dans tous les sens, le code le plus generic que j’ai pu faire est ce commit: add computed fields for all hash (!18) · Merge requests · nodes / duniter-squid · GitLab

Ca juste juste pour remplacer \\ par 0… Aller j’arrête.

2 Likes

Sont maintenant intégrés via la MR!19.
En prod sur https://gdev-squid.axiom-team.fr/v1/graphql

Il reste donc ces deux points gênants, mais non bloquants:

Au final, je trouve que cette intégration Hasura nous offre plus de contrôle sur les migrations squids, et la config graphql.

1 Like

Merci @poka pour ce travail d’analyse et de réalisation sur l’indexeur.

Tu coup, j’aimerais bien aussi avoir une visualisation des tables du schéma utilisé par squid, lors des RML.

Je ne trouve pas le resolver UD history très complexe, comparativement a d’autres projets graphql que j’ai vu, et qui tourne bien (en production).
Par ailleurs, après avoir expérimenté les curseurs, je leur trouve énormément d’intérêt. Par exemple côté App, la gestion de l’historique des transactions via offset devient vite complexe, dès qu’on veut ajouter du cache. Cela vient du fait que la réponse a un même offset n’est pas invariant. Pour avoir une réponse invariant il faut jouer sur les paramètres de la requête, et filtrer sur une période fixe (blockTime < t1).

Bref, comme hasura a aussi des limitations de son côté je serai assez motivé (avant d’en discuter au RML) pour continuer de creuser la piste subsquid sans Hasura qu’avait débuté @HugoTrentesaux. Rien que pour les curseurs ça me semble valoir le coup.

En fait ce que je sais pas c’est si cela serait compliqué de limiter la profondeur de la requete nous même. Je dois creuser.

L’avis de @cgeek n’intéresse aussi fortement car il connaît bien typescript. Par ailleurs je penses que l’indexeur fait partie du noyau de Duniter : car rien ne peut être fait sans lui. C’est en quelque sorte le remplaçant de BMA / GVA. Il faut donc que les développeurs du noyau donne leur expertise, dans la mesure du possible.

L’impact des changements actuels de l’API de l’indexeur n’est pas négligeable. Pour être clair je dois réécrire une bonne partie de Cesium2… Alors que j’étais très satisfait jusque là de la forme des requêtes.

Voilà, désolé @poka d’être moins emballé que vous par Hasura… Mais c’est pourquoi on doit en parler (calmement si possible :slight_smile: ) de visu. Cela lèvera peut-être mes réticences.

Merci de m’avoir lu.

3 Likes

Si tu savais le nombre de refonte que j’ai dû faire sur Ğecko en 2 ans… J’ai commencé sur GVA, qui a changé X fois. D’abords en binding rust, puis ma lib Durt. Ensuite v2s, passage sur la lib polkawallet-sdk, avec webview et tout ce que cela induit. Et là je m’apprête à changer de nouveau, car j’ai le feu vert des dev de la lib pure dart: Project Status · Issue #433 · leonardocustodio/polkadart · GitHub
Et il faut que je migre mes requêtes de l’indexer v1 à l’indexer squid aussi.
A chaque je n’avais pas le choix, c’était réfléchie, rien n’indiquait que j’aurais du changer de telle sorte, avant même la mise en prod du réseau.

Je comprends que c’est embêtant, mais vue que c’était quelque chose que nous projetions dès le début de l’arrivé de squid, j’ai voulu le faire suffisamment tôt (avant la migration) pour ne pas s’enfoncer trop profondément dans le moteur graphql de squid justement.

Oui le resolver est simple en soit, mais par exemple je n’ai pas traité toutes les clauses where possibles, juste les principales (pas sûr que les autres auaient été utiles hein).
Et peut être qu’a l’avenir nous voudrons ajouter d’autres computed fields, avec peut être des resolvers plus complexe. Avec Hasura cette ajout est une formalité une fois qu’on a la bonne requête SQL.
Aussi, Hasura nous offre les requêtes aggregates, que je trouve très pratique.

Au niveau des changements, je l’ai est effectué dans gcli, c’était très rapide. C’est certains pluriels qui sont passé infinitif, et les eq et ce genre de clause imbriqués au lieu de collé à la clause par _ (ce que je trouve beaucoup plus claire et logique personnellement).

Ensuite oui le gros point c’est le pagination par curseur.
A savoir qu’en utilisant l’API relay, actuellement cette pagination par curseur fonctionne sans bug étant donnée qu’aucune limite de requête n’a été définit par table (pour le moment). Le bug est juste quand cette limite est définit, la somme des lignes retourné par la requête paginé est limité par cette valeur (ex: 10 pages x 100 lignes max si la limite est fixé à 1000 ligne pour cette tables, même si il y a 50000 lignes au total à requête). C’est pourquoi dans gecko on ne pouvait pas remonté l’historique des transactions jusqu’au début pour les gros historique dans l’indexer Hasura de manu.

Donc on peut aussi décider de ne pas fixer de limite de ligne par table pour pouvoir utiliser la pagination par curseur de l’api relay hasura (qui est dispo dès à près en, voir le switch dans la console Hasura pour voir le endpoint est le requêtes), et de fixer ces limites autrement.
Au moins même sans limites, je n’ai pas réussi à faire crasher le moteur graphql de Hasura même avec d’énorme requêtes, juste une très longue attente côté client. Alors qu’avec le moteur graphql de squid, un peu trop de lignes, et c’est tchao, il crash, faut le rebooter.


Je ne suis pas du tout contre l’idée de finalement garder le moteur graphql de squid, je considère qu’on ne pouvais pas trancher sans aller à fond dans l’implémentation Hasura pour en voir clairement les contours, et donc trancher en connaissances de cause.

Mais il faut peser les pour et les contres

Toujours :slight_smile: :hocho:

2 Likes

Merci pour ta réponse, ouverte, que j’apprécie :slight_smile:

En regardant la doc subsquid, je tombe la dessus : DoS protection | Subsquid

Du coup, je me dis que l’on devrait pouvoir limiter de manière precise la taille des flux de retour. Et même écrire des tests unitaires pour vérifier nos réglages, par exemple en repartant des cas de plantage que tu obtenu ?

Leur approche me paraît assez propre.

1 Like

Si la limitation par @cardinality ne suffit pas, nous pourrions aussi utiliser leur système de control d’accès, en analysant la portée et profondeur de grappe de la requête, suivant nos critères, puis renvoyer une erreur “not allowed” le cas échéant.

1 Like

Oui c’est ce que j’expliquais ici dans ce ticket : Define @cardinality for every type in graphql.schema (#12) · Issues · nodes / duniter-squid · GitLab

Et dans le topic de forum associé.
Je trouve que la définition de ces cardinalités alourdissent le schema.graphql pour rien, et le complexifie.

Mais tu peux rouvrire le ticket si tu veux.

Hasura ne nous apporte pas que ce genre de chose, il nous permet un plus grande souplesse dans le schema graphql via ses metadata.

C’est ce que je pense aussi, c’est ce qui m’a motivé à m’y mettre fin 2023 et ça va donc être un gros sujet de discussion pour les RML.

Normalement non, la forme des requêtes ne change que légèrement entre squid sans et avec Hasura, et la forme des réponses est quasiment identique (uniquement l’histoire de null safety je pense, qui est une limitation de typeorm et pas de hasura).

L’intérêt de squid et de typeorm est qu’on n’a jamais à définir manuellement les tables (d’où certaines limitations), vu que la seule chose à définir est le schéma. Et le schéma qu’on définit est extrêmement simple, il tient en gros sur 150 lignes, ce qui va rendre les discussions intéressantes puisqu’on pourra tous facilement rentrer dans le vif du sujet.

Hâte d’y être pour pouvoir échanger dessus.

3 Likes