Skip to content

Commit 29a8ec2

Browse files
Explanation
1 parent eac76ff commit 29a8ec2

File tree

1 file changed

+49
-44
lines changed

1 file changed

+49
-44
lines changed

website/src/pages/en/subgraphs/querying/distributed-systems.mdx

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,60 @@
22
title: Distributed Systems
33
---
44

5-
The Graph is a protocol implemented as a distributed system.
5+
Distributed systems offer vast capabilities, but they also come with inherent complexities. In **The Graph**, these complexities are amplified at a global scale.
66

7-
Connections fail. Requests arrive out of order. Different computers with out-of-sync clocks and states process related requests. Servers restart. Re-orgs happen between requests. These problems are inherent to all distributed systems but are exacerbated in systems operating at a global scale.
7+
This document provides an explanation of why requests can appear inconsistent, how block reorganization (re-org) events affect data delivery, and why certain solutions exist to maintain consistency.
88

9-
Consider this example of what may occur if a client polls an Indexer for the latest data during a re-org.
9+
### Why Distributed Systems Can Appear Inconsistent
1010

11-
1. Indexer ingests block 8
12-
2. Request served to the client for block 8
13-
3. Indexer ingests block 9
14-
4. Indexer ingests block 10A
15-
5. Request served to the client for block 10A
16-
6. Indexer detects reorg to 10B and rolls back 10A
17-
7. Request served to the client for block 9
18-
8. Indexer ingests block 10B
19-
9. Indexer ingests block 11
11+
Whenever different computers are working together across the globe, certain problems become unavoidable. Connections fail, computers get restarted, and clocks fall out of sync. In the context of **The Graph**, multiple Indexers ingest blocks at slightly different times, and clients can make requests to any of these Indexers. The result is that requests may arrive out of order or be answered based on different block states.
12+
13+
A perfect example of this is the so-called "block wobble" phenomenon, where a client sees block data appear to jump forward and backward in unexpected ways. This phenomenon becomes especially noticeable during a block re-org — a situation where a previously ingested block is replaced by a different one under consensus.
14+
15+
#### Example of Block Reorganization
16+
17+
To understand the impact, consider a scenario where a client continuously fetches the latest block from an Indexer:
18+
19+
1. Indexer ingests block 8
20+
2. Request served to the client for block 8
21+
3. Indexer ingests block 9
22+
4. Indexer ingests block 10A
23+
5. Request served to the client for block 10A
24+
6. Indexer detects re-org to 10B and rolls back 10A
25+
7. Request served to the client for block 9
26+
8. Indexer ingests block 10B
27+
9. Indexer ingests block 11
2028
10. Request served to the client for block 11
2129

22-
From the point of view of the Indexer, things are progressing forward logically. Time is moving forward, though we did have to roll back an uncle block and play the block under consensus forward on top of it. Along the way, the Indexer serves requests using the latest state it knows about at that time.
30+
From the **Indexer's viewpoint**, it sees a forward-moving progression with a brief need to roll back an invalid block. But from the **client's viewpoint**, responses seem to arrive in a puzzling order: block 8, block 10, then suddenly block 9, and finally block 11.
2331

24-
From the point of view of the client, however, things appear chaotic. The client observes that the responses were for blocks 8, 10, 9, and 11 in that order. We call this the "block wobble" problem. When a client experiences block wobble, data may appear to contradict itself over time. The situation worsens when we consider that Indexers do not all ingest the latest blocks simultaneously, and your requests may be routed to multiple Indexers.
32+
This disruption can cause data to appear contradictory. The challenge is magnified when clients are routed to multiple Indexers, each of which may be at different block heights.
2533

26-
It is the responsibility of the client and server to work together to provide consistent data to the user. Different approaches must be used depending on the desired consistency as there is no one right program for every problem.
34+
### Why Consistency Matters
2735

28-
Reasoning through the implications of distributed systems is hard, but the fix may not be! We've established APIs and patterns to help you navigate some common use-cases. The following examples illustrate those patterns but still elide details required by production code (like error handling and cancellation) to not obfuscate the main ideas.
36+
In a distributed protocol like **The Graph**, it is the responsibility of both client and server to coordinate on a consistency strategy. Systems often require different approaches based on their tolerance for receiving out-of-date data versus their need for up-to-the-moment accuracy.
2937

30-
## Polling for updated data
38+
Reasoning through these distributed-system implications is difficult, but solutions exist to reduce confusion. For instance, certain patterns and APIs ensure either forward-only progress or consistent snapshot views of data. Below, two notable methods to maintain a clearer sense of order are explored.
3139

32-
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.
40+
### Ensuring Forward-Only Progress with Block Number Constraints
3341

34-
We can use `number_gte` to ensure that time never travels backward when polling for data in a loop. Here is an example:
42+
Sometimes, your system only needs to avoid "going backward" in time. **The Graph** provides the `block: { number_gte: $minBlock }` argument to help support this by guaranteeing that returned data will always be from a block number equal to or greater than a specified minimum:
3543

3644
```javascript
37-
/// Updates the protocol.paused variable to the latest
38-
/// known value in a loop by fetching it using The Graph.
45+
/// Updates the protocol.paused variable by always querying
46+
/// for a block at or beyond the last known block.
3947
async function updateProtocolPaused() {
40-
// It's ok to start with minBlock at 0. The query will be served
41-
// using the latest block available. Setting minBlock to 0 is the
42-
// same as leaving out that argument.
4348
let minBlock = 0
4449

4550
for (;;) {
46-
// Schedule a promise that will be ready once
47-
// the next Ethereum block will likely be available.
51+
// Wait until the next block is likely available.
4852
const nextBlock = new Promise((f) => {
4953
setTimeout(f, 14000)
5054
})
5155

5256
const query = `
5357
query GetProtocol($minBlock: Int!) {
54-
protocol(block: { number_gte: $minBlock } id: "0") {
58+
protocol(block: { number_gte: $minBlock }, id: "0") {
5559
paused
5660
}
5761
_meta {
@@ -65,30 +69,27 @@ async function updateProtocolPaused() {
6569
const response = await graphql(query, variables)
6670
minBlock = response._meta.block.number
6771

68-
// TODO: Do something with the response data here instead of logging it.
72+
// This ensures time never travels backward.
6973
console.log(response.protocol.paused)
7074

71-
// Sleep to wait for the next block
75+
// Wait before fetching the next block.
7276
await nextBlock
7377
}
7478
}
7579
```
7680

77-
## Fetching a set of related items
81+
In an environment with multiple Indexers, the Gateway can filter out Indexers not yet at `minBlock`, ensuring you consistently move forward in time.
7882

79-
Another use-case is retrieving a large set or, more generally, retrieving related items across multiple requests. Unlike the polling case (where the desired consistency was to move forward in time), the desired consistency is for a single point in time.
83+
### Achieving a Consistent View with Block Hash Constraints
8084

81-
Here we will use the `block: { hash: $blockHash }` argument to pin all of our results to the same block.
85+
In other scenarios, you need to retrieve multiple related data points or perform pagination without risking differences in blocks. This is where pinning a query to a single block hash becomes critical:
8286

8387
```javascript
84-
/// Gets a list of domain names from a single block using pagination
88+
/// Gets a list of domain names from a single block using pagination.
8589
async function getDomainNames() {
86-
// Set a cap on the maximum number of items to pull.
8790
let pages = 5
8891
const perPage = 1000
8992

90-
// The first query will get the first page of results and also get the block
91-
// hash so that the remainder of the queries are consistent with the first.
9293
const listDomainsQuery = `
9394
query ListDomains($perPage: Int!) {
9495
domains(first: $perPage) {
@@ -106,29 +107,33 @@ async function getDomainNames() {
106107
let result = data.domains.map((d) => d.name)
107108
let blockHash = data._meta.block.hash
108109

109-
let query
110-
// Continue fetching additional pages until either we run into the limit of
111-
// 5 pages total (specified above) or we know we have reached the last page
112-
// because the page has fewer entities than a full page.
113110
while (data.domains.length == perPage && --pages) {
114111
let lastID = data.domains[data.domains.length - 1].id
115-
query = `
112+
let query = `
116113
query ListDomains($perPage: Int!, $lastID: ID!, $blockHash: Bytes!) {
117-
domains(first: $perPage, where: { id_gt: $lastID }, block: { hash: $blockHash }) {
114+
domains(
115+
first: $perPage,
116+
where: { id_gt: $lastID },
117+
block: { hash: $blockHash }
118+
) {
118119
name
119120
id
120121
}
121122
}`
122123

123124
data = await graphql(query, { perPage, lastID, blockHash })
124125

125-
// Accumulate domain names into the result
126126
for (domain of data.domains) {
127127
result.push(domain.name)
128128
}
129129
}
130+
130131
return result
131132
}
132133
```
133134

134-
Note that in case of a re-org, the client will need to retry from the first request to update the block hash to a non-uncle block.
135+
By pinning queries to a single block hash, all responses relate to the same point in time, eliminating any variation caused by a re-org that might occur if different blocks were referenced. However, if a re-org does affect that chosen block, the client must repeat the process with a new canonical block hash.
136+
137+
### Final Thoughts on Distributed Consistency
138+
139+
Distributed systems can seem unpredictable, but understanding the root causes of events like out-of-order requests and block reorganizations helps clarify why the data may appear contradictory. By using the patterns described above, both crossing block boundaries forward in time and maintaining a single consistent block snapshot become possible strategies in managing this inherent complexity.

0 commit comments

Comments
 (0)