Systèmes distribués
Reading time: 5 min
Le graph est un protocole mis en œuvre sous la forme d'un système distribué.
Les connexions échouent. Les demandes arrivent dans le désordre. Différents ordinateurs dont les horloges et les états ne sont pas synchronisés traitent les demandes correspondantes. Les serveurs redémarrent. Des réorgs se produisent entre les demandes. Ces problèmes sont inhérents à tous les systèmes distribués, mais ils sont exacerbés dans les systèmes fonctionnant à l'échelle mondiale.
Considérez cet exemple de ce qui peut se produire si un client interroge un indexeur pour connaître les dernières données lors d'une réorganisation.
- L'indexeur ingère le bloc 8
- Demande transmise au client pour le bloc 8
- L'indexeur ingère le bloc 9
- L'indexeur ingère le bloc 10A
- Demande transmise au client pour le bloc 10A
- L'indexeur détecte la réorganisation à 10B et annule 10A
- Demande transmise au client pour le bloc 9
- L'indexeur ingère le bloc 10B
- L'indexeur ingère le bloc 11
- Demande transmise au client pour le bloc 11
Du point de vue de l'indexeur, les choses progressent logiquement. Le temps avance, bien que nous ayons dû revenir en arrière sur un bloc oncle et faire avancer le bloc faisant l'objet d'un consensus par-dessus. En cours de route, l'indexeur répond aux demandes en utilisant le dernier état dont il a connaissance à ce moment-là.
Mais du point de vue du client, les choses semblent chaotiques. Le client observe que les réponses concernaient les blocs 8, 10, 9 et 11 dans cet ordre. Nous appelons cela le problème de « l’oscillation du bloc ». Lorsqu'un client subit une oscillation de blocage, les données peuvent sembler se contredire au fil du temps. La situation s'aggrave lorsque l'on considère que les indexeurs n'ingèrent pas tous les derniers blocs simultanément et que vos requêtes peuvent être acheminées vers plusieurs indexeurs.
Il est de la responsabilité du client et du serveur de travailler ensemble pour fournir des données cohérentes à l'utilisateur. Différentes approches doivent être utilisées en fonction de la cohérence souhaitée, car il n’existe pas de programme adapté à chaque problème.
Il est difficile de raisonner sur les implications des systèmes distribués, mais la solution ne l'est pas nécessairement ! Nous avons établi des API et des modèles pour vous aider à naviguer dans certains cas d'utilisation courants. Les exemples suivants illustrent ces modèles tout en éludant les détails requis par le code de production (comme la gestion des erreurs et l'annulation) afin de ne pas obscurcir les idées principales.
Le graph fournit l'API block : { number_gte: $minBlock }
, qui garantit que la réponse concerne un seul bloc égal ou supérieur à $minBlock
. Si la requête est faite à une instance de graph-node
et que le bloc min n'est pas encore synchronisé, graph-node
renverra une erreur. Si graph-node
a synchronisé le bloc min, il exécutera la réponse pour le dernier bloc. Si la demande est adressée à un Edge & Node Gateway, la passerelle filtrera tous les indexeurs qui n'ont pas encore synchronisé le bloc min et fera la demande pour le dernier bloc synchronisé par l'indexeur.
Nous pouvons utiliser number_gte
pour nous assurer que le temps ne recule jamais lorsque nous interrogeons des données dans une boucle. Voici un exemple :
/// Met à jour la variable protocol.paused avec la dernière valeur/// connue dans une boucle en la récupérant en utilisant The Graph.async function updateProtocolPaused() {// Il est correct de commencer avec minBlock à 0. La requête sera servie// en utilisant le dernier bloc disponible. Définir minBlock à 0 revient// à omettre cet argument.let minBlock = 0for (;;) {// Planifie une promesse qui sera prête une fois que le prochain bloc// Ethereum sera probablement disponible.const nextBlock = new Promise((f) => {setTimeout(f, 14000)})const query = `query GetProtocol($minBlock: Int!) {protocol(block: { number_gte: $minBlock } id: "0") {paused}_meta {block {number}}}`const variables = { minBlock }const response = await graphql(query, variables)minBlock = response._meta.block.number// TODO: Faites quelque chose avec les données de réponse ici au lieu de les journaliser.console.log(response.protocol.paused)// Dort pour attendre le prochain blocawait nextBlock}}
Un autre cas d'utilisation est la récupération d'un grand ensemble ou, plus généralement, la récupération d'éléments liés entre plusieurs requêtes. Contrairement au cas des sondages (où la cohérence souhaitée était d'avancer dans le temps), la cohérence souhaitée est pour un seul point dans le temps.
Ici, nous utiliserons l'argument block: { hash: $blockHash }
pour épingler tous nos résultats dans le même bloc.
/// Obtient une liste de noms de domaine à partir d'un seul bloc en utilisant la paginationasync function getDomainNames() {// Définit un plafond sur le nombre maximum d'éléments à extraire.let pages = 5const perPage = 1000// La première requête obtiendra la première page de résultats et obtiendra également le// hachage du bloc afin que le reste des requêtes soit cohérent avec la première.const listDomainsQuery = `query ListDomains($perPage: Int!) {domains(first: $perPage) {nameid}_meta {block {hash}}}`let data = await graphql(listDomainsQuery, { perPage })let result = data.domains.map((d) => d.name)let blockHash = data._meta.block.hashlet query// Continue à récupérer des pages supplémentaires jusqu'à ce que nous atteignions la limite de// 5 pages au total (spécifiée ci-dessus) ou que nous sachions que nous avons atteint la dernière page// parce que la page a moins d'entités qu'une page complète.while (data.domains.length == perPage && --pages) {let lastID = data.domains[data.domains.length - 1].idquery = `query ListDomains($perPage: Int!, $lastID: ID!, $blockHash: Bytes!) {domains(first: $perPage, where: { id_gt: $lastID }, block: { hash: $blockHash }) {nameid}}`data = await graphql(query, { perPage, lastID, blockHash })// Accumule les noms de domaine dans le résultatfor (domain of data.domains) {result.push(domain.name)}}return result}
Il convient de noter qu'en cas de réorganisation, le client devra réessayer à partir de la première demande de mise à jour du hachage du bloc vers un bloc qui n'a pas été supprimé.