5 минуты
Распределенные системы
The Graph - это протокол, реализованный в виде распределенной системы.
Соединения терпят неудачу. Запросы поступают не по порядку. Разные компьютеры с несинхронизированными часами и состояниями обрабатывают связанные запросы. Серверы перезапускаются. Реорганизации происходят между запросами. Эти проблемы присущи всем распределенным системам, но усугубляются в системах, работающих в глобальном масштабе.
Рассмотрим этот пример того, что может произойти, если клиент запрашивает у Индексатора последние данные во время реорганизации.
- Индексатор обрабатывает блок 8
- Запрос, отправленный клиенту для блока 8
- Индексатор обрабатывает блок 9
- Индексатор обрабатывает блок 10A
- Запрос, отправленный клиенту для блока 10A
- Индексатор обнаруживает реорганизацию на 10B и откатывается на 10A
- Запрос, отправленный клиенту для блока 9
- Индексатор обрабатывает блок 10B
- Индексатор обрабатывает блок 11
- Запрос, отправленный клиенту для блока 11
С точки зрения Индексатора, все логично продвигается вперед. Время движется вперед, хотя нам и пришлось откатить незавершенный блок и воспроизвести консенсусный блок поверх него. Попутно индексатор обслуживает запросы, используя последнее состояние, о котором ему известно на данный момент.
Однако с точки зрения клиента все выглядит хаотично. Клиент замечает, что ответы были для блоков 8, 10, 9 и 11 в таком порядке. Мы называем это проблемой “колебания блока”. Когда клиент испытывает колебания блока, со временем может показаться, что данные противоречат сами себе. Ситуация ухудшается, если учесть, что не все Индексаторы принимают последние блоки одновременно, и ваши запросы могут быть перенаправлены нескольким Индексаторам.
Клиент и сервер несут ответственность за совместную работу по предоставлению пользователю согласованных данных. Необходимо использовать различные подходы в зависимости от желаемой согласованности, поскольку не существует единой правильной программы для каждой проблемы.
Рассуждать о последствиях распределенных систем сложно, но исправления может и не быть! Мы создали API и шаблоны, чтобы помочь вам ориентироваться в некоторых распространенных вариантах использования. Следующие примеры иллюстрируют эти шаблоны, но по-прежнему исключают детали, требуемые производственным кодом (например, обработку ошибок и отмену), чтобы не запутывать основные идеи.
Опрос для получения обновленных данных
The Graph provides the block: { number_gte: $minBlock }
API, which ensures that the response is for a single block equal or higher to $minBlock
. If the request is made to a graph-node
instance and the min block is not yet synced, graph-node
will return an error. If graph-node
has synced min block, it will run the response for the latest block. If the request is made to an Edge & Node Gateway, the Gateway will filter out any Indexers that have not yet synced min block and make the request for the latest block the Indexer has synced.
We can use number_gte
to ensure that time never travels backward when polling for data in a loop. Here is an example:
1/// Updates the protocol.paused variable to the latest2/// known value in a loop by fetching it using The Graph.3async function updateProtocolPaused() {4 // It's ok to start with minBlock at 0. The query will be served5 // using the latest block available. Setting minBlock to 0 is the6 // same as leaving out that argument.7 let minBlock = 089 for (;;) {10 // Schedule a promise that will be ready once11 // the next Ethereum block will likely be available.12 const nextBlock = new Promise((f) => {13 setTimeout(f, 14000)14 })1516 const query = `17 query GetProtocol($minBlock: Int!) {18 protocol(block: { number_gte: $minBlock } id: "0") {19 paused20 }21 _meta {22 block {23 number24 }25 }26 }`2728 const variables = { minBlock }29 const response = await graphql(query, variables)30 minBlock = response._meta.block.number3132 // TODO: Do something with the response data here instead of logging it.33 console.log(response.protocol.paused)3435 // Sleep to wait for the next block36 await nextBlock37 }38}
Выборка набора связанных элементов
Другим вариантом использования является извлечение большого набора или, в более общем плане, извлечение связанных элементов по нескольким запросам. В отличие от случая опроса (где желаемая согласованность заключалась в продвижении вперед во времени), желаемая согласованность относится к одному моменту времени.
Here we will use the block: { hash: $blockHash }
argument to pin all of our results to the same block.
1/// Gets a list of domain names from a single block using pagination2async function getDomainNames() {3 // Set a cap on the maximum number of items to pull.4 let pages = 55 const perPage = 100067 // The first query will get the first page of results and also get the block8 // hash so that the remainder of the queries are consistent with the first.9 const listDomainsQuery = `10 query ListDomains($perPage: Int!) {11 domains(first: $perPage) {12 name13 id14 }15 _meta {16 block {17 hash18 }19 }20 }`2122 let data = await graphql(listDomainsQuery, { perPage })23 let result = data.domains.map((d) => d.name)24 let blockHash = data._meta.block.hash2526 let query27 // Continue fetching additional pages until either we run into the limit of28 // 5 pages total (specified above) or we know we have reached the last page29 // because the page has fewer entities than a full page.30 while (data.domains.length == perPage && --pages) {31 let lastID = data.domains[data.domains.length - 1].id32 query = `33 query ListDomains($perPage: Int!, $lastID: ID!, $blockHash: Bytes!) {34 domains(first: $perPage, where: { id_gt: $lastID }, block: { hash: $blockHash }) {35 name36 id37 }38 }`3940 data = await graphql(query, { perPage, lastID, blockHash })4142 // Accumulate domain names into the result43 for (domain of data.domains) {44 result.push(domain.name)45 }46 }47 return result48}
Обратите внимание, что в случае реорганизации клиент должен будет повторить попытку с первого запроса, чтобы обновить хэш блока до необработанного блока.