envio 2.21.0 → 2.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/FetchState.res +1320 -0
- package/src/Prometheus.res +13 -1
- package/src/sources/Source.res +59 -0
- package/src/sources/SourceManager.res +467 -0
- package/src/sources/SourceManager.resi +32 -0
package/src/Prometheus.res
CHANGED
|
@@ -335,7 +335,7 @@ module IndexingMaxConcurrency = {
|
|
|
335
335
|
module IndexingConcurrency = {
|
|
336
336
|
let gauge = SafeGauge.makeOrThrow(
|
|
337
337
|
~name="envio_indexing_concurrency",
|
|
338
|
-
~help="The
|
|
338
|
+
~help="The number of executing concurrent queries to the chain data-source.",
|
|
339
339
|
~labelSchema=chainIdLabelsSchema,
|
|
340
340
|
)
|
|
341
341
|
|
|
@@ -344,6 +344,18 @@ module IndexingConcurrency = {
|
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
module IndexingPartitions = {
|
|
348
|
+
let gauge = SafeGauge.makeOrThrow(
|
|
349
|
+
~name="envio_indexing_partitions",
|
|
350
|
+
~help="The number of partitions used to split fetching logic by addresses and block ranges.",
|
|
351
|
+
~labelSchema=chainIdLabelsSchema,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
let set = (~partitionsCount, ~chainId) => {
|
|
355
|
+
gauge->SafeGauge.handleInt(~labels=chainId, ~value=partitionsCount)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
347
359
|
module IndexingBufferSize = {
|
|
348
360
|
let gauge = SafeGauge.makeOrThrow(
|
|
349
361
|
~name="envio_indexing_buffer_size",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A set of stats for logging about the block range fetch
|
|
3
|
+
*/
|
|
4
|
+
type blockRangeFetchStats = {
|
|
5
|
+
@as("total time elapsed (ms)") totalTimeElapsed: int,
|
|
6
|
+
@as("parsing time (ms)") parsingTimeElapsed?: int,
|
|
7
|
+
@as("page fetch time (ms)") pageFetchTime?: int,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
Thes response returned from a block range fetch
|
|
12
|
+
*/
|
|
13
|
+
type blockRangeFetchResponse = {
|
|
14
|
+
currentBlockHeight: int,
|
|
15
|
+
reorgGuard: ReorgDetection.reorgGuard,
|
|
16
|
+
parsedQueueItems: array<Internal.eventItem>,
|
|
17
|
+
fromBlockQueried: int,
|
|
18
|
+
latestFetchedBlockNumber: int,
|
|
19
|
+
latestFetchedBlockTimestamp: int,
|
|
20
|
+
stats: blockRangeFetchStats,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type getItemsRetry =
|
|
24
|
+
| WithSuggestedToBlock({toBlock: int})
|
|
25
|
+
| WithBackoff({message: string, backoffMillis: int})
|
|
26
|
+
|
|
27
|
+
type getItemsError =
|
|
28
|
+
| UnsupportedSelection({message: string})
|
|
29
|
+
| FailedGettingFieldSelection({exn: exn, blockNumber: int, logIndex: int, message: string})
|
|
30
|
+
| FailedParsingItems({exn: exn, blockNumber: int, logIndex: int, message: string})
|
|
31
|
+
| FailedGettingItems({exn: exn, attemptedToBlock: int, retry: getItemsRetry})
|
|
32
|
+
|
|
33
|
+
exception GetItemsError(getItemsError)
|
|
34
|
+
|
|
35
|
+
type sourceFor = Sync | Fallback
|
|
36
|
+
type t = {
|
|
37
|
+
name: string,
|
|
38
|
+
sourceFor: sourceFor,
|
|
39
|
+
chain: ChainMap.Chain.t,
|
|
40
|
+
poweredByHyperSync: bool,
|
|
41
|
+
/* Frequency (in ms) used when polling for new events on this network. */
|
|
42
|
+
pollingInterval: int,
|
|
43
|
+
getBlockHashes: (
|
|
44
|
+
~blockNumbers: array<int>,
|
|
45
|
+
~logger: Pino.t,
|
|
46
|
+
) => promise<result<array<ReorgDetection.blockDataWithTimestamp>, exn>>,
|
|
47
|
+
getHeightOrThrow: unit => promise<int>,
|
|
48
|
+
getItemsOrThrow: (
|
|
49
|
+
~fromBlock: int,
|
|
50
|
+
~toBlock: option<int>,
|
|
51
|
+
~addressesByContractName: dict<array<Address.t>>,
|
|
52
|
+
~indexingContracts: dict<FetchState.indexingContract>,
|
|
53
|
+
~currentBlockHeight: int,
|
|
54
|
+
~partitionId: string,
|
|
55
|
+
~selection: FetchState.selection,
|
|
56
|
+
~retry: int,
|
|
57
|
+
~logger: Pino.t,
|
|
58
|
+
) => promise<blockRangeFetchResponse>,
|
|
59
|
+
}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
open Belt
|
|
2
|
+
|
|
3
|
+
// Ideally the ChainFetcher name suits this better
|
|
4
|
+
// But currently the ChainFetcher module is immutable
|
|
5
|
+
// and handles both processing and fetching.
|
|
6
|
+
// So this module is to encapsulate the fetching logic only
|
|
7
|
+
// with a mutable state for easier reasoning and testing.
|
|
8
|
+
type t = {
|
|
9
|
+
sources: Utils.Set.t<Source.t>,
|
|
10
|
+
maxPartitionConcurrency: int,
|
|
11
|
+
newBlockFallbackStallTimeout: int,
|
|
12
|
+
stalledPollingInterval: int,
|
|
13
|
+
getHeightRetryInterval: (~retry: int) => int,
|
|
14
|
+
mutable activeSource: Source.t,
|
|
15
|
+
mutable waitingForNewBlockStateId: option<int>,
|
|
16
|
+
// Should take into consideration partitions fetching for previous states (before rollback)
|
|
17
|
+
mutable fetchingPartitionsCount: int,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let getActiveSource = sourceManager => sourceManager.activeSource
|
|
21
|
+
|
|
22
|
+
let makeGetHeightRetryInterval = (
|
|
23
|
+
~initialRetryInterval,
|
|
24
|
+
~backoffMultiplicative,
|
|
25
|
+
~maxRetryInterval,
|
|
26
|
+
) => {
|
|
27
|
+
(~retry: int) => {
|
|
28
|
+
let backoff = if retry === 0 {
|
|
29
|
+
1
|
|
30
|
+
} else {
|
|
31
|
+
retry * backoffMultiplicative
|
|
32
|
+
}
|
|
33
|
+
Pervasives.min(initialRetryInterval * backoff, maxRetryInterval)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let make = (
|
|
38
|
+
~sources: array<Source.t>,
|
|
39
|
+
~maxPartitionConcurrency,
|
|
40
|
+
~newBlockFallbackStallTimeout=20_000,
|
|
41
|
+
~stalledPollingInterval=5_000,
|
|
42
|
+
~getHeightRetryInterval=makeGetHeightRetryInterval(
|
|
43
|
+
~initialRetryInterval=1000,
|
|
44
|
+
~backoffMultiplicative=2,
|
|
45
|
+
~maxRetryInterval=60_000,
|
|
46
|
+
),
|
|
47
|
+
) => {
|
|
48
|
+
let initialActiveSource = switch sources->Js.Array2.find(source => source.sourceFor === Sync) {
|
|
49
|
+
| None => Js.Exn.raiseError("Invalid configuration, no data-source for historical sync provided")
|
|
50
|
+
| Some(source) => source
|
|
51
|
+
}
|
|
52
|
+
Prometheus.IndexingMaxConcurrency.set(
|
|
53
|
+
~maxConcurrency=maxPartitionConcurrency,
|
|
54
|
+
~chainId=initialActiveSource.chain->ChainMap.Chain.toChainId,
|
|
55
|
+
)
|
|
56
|
+
Prometheus.IndexingConcurrency.set(
|
|
57
|
+
~concurrency=0,
|
|
58
|
+
~chainId=initialActiveSource.chain->ChainMap.Chain.toChainId,
|
|
59
|
+
)
|
|
60
|
+
{
|
|
61
|
+
maxPartitionConcurrency,
|
|
62
|
+
sources: Utils.Set.fromArray(sources),
|
|
63
|
+
activeSource: initialActiveSource,
|
|
64
|
+
waitingForNewBlockStateId: None,
|
|
65
|
+
fetchingPartitionsCount: 0,
|
|
66
|
+
newBlockFallbackStallTimeout,
|
|
67
|
+
stalledPollingInterval,
|
|
68
|
+
getHeightRetryInterval,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let fetchNext = async (
|
|
73
|
+
sourceManager: t,
|
|
74
|
+
~fetchState: FetchState.t,
|
|
75
|
+
~currentBlockHeight,
|
|
76
|
+
~executeQuery,
|
|
77
|
+
~waitForNewBlock,
|
|
78
|
+
~onNewBlock,
|
|
79
|
+
~maxPerChainQueueSize,
|
|
80
|
+
~stateId,
|
|
81
|
+
) => {
|
|
82
|
+
let {maxPartitionConcurrency} = sourceManager
|
|
83
|
+
|
|
84
|
+
switch fetchState->FetchState.getNextQuery(
|
|
85
|
+
~concurrencyLimit={
|
|
86
|
+
maxPartitionConcurrency - sourceManager.fetchingPartitionsCount
|
|
87
|
+
},
|
|
88
|
+
~maxQueueSize=maxPerChainQueueSize,
|
|
89
|
+
~currentBlockHeight,
|
|
90
|
+
~stateId,
|
|
91
|
+
) {
|
|
92
|
+
| ReachedMaxConcurrency
|
|
93
|
+
| NothingToQuery => ()
|
|
94
|
+
| WaitingForNewBlock =>
|
|
95
|
+
switch sourceManager.waitingForNewBlockStateId {
|
|
96
|
+
| Some(waitingStateId) if waitingStateId >= stateId => ()
|
|
97
|
+
| Some(_) // Case for the prev state before a rollback
|
|
98
|
+
| None =>
|
|
99
|
+
sourceManager.waitingForNewBlockStateId = Some(stateId)
|
|
100
|
+
let currentBlockHeight = await waitForNewBlock(~currentBlockHeight)
|
|
101
|
+
switch sourceManager.waitingForNewBlockStateId {
|
|
102
|
+
| Some(waitingStateId) if waitingStateId === stateId => {
|
|
103
|
+
sourceManager.waitingForNewBlockStateId = None
|
|
104
|
+
onNewBlock(~currentBlockHeight)
|
|
105
|
+
}
|
|
106
|
+
| Some(_) // Don't reset it if we are waiting for another state
|
|
107
|
+
| None => ()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
| Ready(queries) => {
|
|
111
|
+
fetchState->FetchState.startFetchingQueries(~queries, ~stateId)
|
|
112
|
+
sourceManager.fetchingPartitionsCount =
|
|
113
|
+
sourceManager.fetchingPartitionsCount + queries->Array.length
|
|
114
|
+
Prometheus.IndexingConcurrency.set(
|
|
115
|
+
~concurrency=sourceManager.fetchingPartitionsCount,
|
|
116
|
+
~chainId=sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
117
|
+
)
|
|
118
|
+
let _ =
|
|
119
|
+
await queries
|
|
120
|
+
->Array.map(q => {
|
|
121
|
+
let promise = q->executeQuery
|
|
122
|
+
let _ = promise->Promise.thenResolve(_ => {
|
|
123
|
+
sourceManager.fetchingPartitionsCount = sourceManager.fetchingPartitionsCount - 1
|
|
124
|
+
Prometheus.IndexingConcurrency.set(
|
|
125
|
+
~concurrency=sourceManager.fetchingPartitionsCount,
|
|
126
|
+
~chainId=sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
promise
|
|
130
|
+
})
|
|
131
|
+
->Promise.all
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type status = Active | Stalled | Done
|
|
137
|
+
|
|
138
|
+
let getSourceNewHeight = async (
|
|
139
|
+
sourceManager,
|
|
140
|
+
~source: Source.t,
|
|
141
|
+
~currentBlockHeight,
|
|
142
|
+
~status: ref<status>,
|
|
143
|
+
~logger,
|
|
144
|
+
) => {
|
|
145
|
+
let newHeight = ref(0)
|
|
146
|
+
let retry = ref(0)
|
|
147
|
+
|
|
148
|
+
while newHeight.contents <= currentBlockHeight && status.contents !== Done {
|
|
149
|
+
try {
|
|
150
|
+
// Use to detect if the source is taking too long to respond
|
|
151
|
+
let endTimer = Prometheus.SourceGetHeightDuration.startTimer({
|
|
152
|
+
"source": source.name,
|
|
153
|
+
"chainId": source.chain->ChainMap.Chain.toChainId,
|
|
154
|
+
})
|
|
155
|
+
let height = await source.getHeightOrThrow()
|
|
156
|
+
endTimer()
|
|
157
|
+
|
|
158
|
+
newHeight := height
|
|
159
|
+
if height <= currentBlockHeight {
|
|
160
|
+
retry := 0
|
|
161
|
+
// Slowdown polling when the chain isn't progressing
|
|
162
|
+
let pollingInterval = if status.contents === Stalled {
|
|
163
|
+
sourceManager.stalledPollingInterval
|
|
164
|
+
} else {
|
|
165
|
+
source.pollingInterval
|
|
166
|
+
}
|
|
167
|
+
await Utils.delay(pollingInterval)
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
| exn =>
|
|
171
|
+
let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
|
|
172
|
+
logger->Logging.childTrace({
|
|
173
|
+
"msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
|
|
174
|
+
"source": source.name,
|
|
175
|
+
"err": exn->Internal.prettifyExn,
|
|
176
|
+
})
|
|
177
|
+
retry := retry.contents + 1
|
|
178
|
+
await Utils.delay(retryInterval)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
Prometheus.SourceHeight.set(
|
|
182
|
+
~sourceName=source.name,
|
|
183
|
+
~chainId=source.chain->ChainMap.Chain.toChainId,
|
|
184
|
+
~blockNumber=newHeight.contents,
|
|
185
|
+
)
|
|
186
|
+
newHeight.contents
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Polls for a block height greater than the given block number to ensure a new block is available for indexing.
|
|
190
|
+
let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
|
|
191
|
+
let {sources} = sourceManager
|
|
192
|
+
|
|
193
|
+
let logger = Logging.createChild(
|
|
194
|
+
~params={
|
|
195
|
+
"chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
|
|
196
|
+
"currentBlockHeight": currentBlockHeight,
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
logger->Logging.childTrace("Initiating check for new blocks.")
|
|
200
|
+
|
|
201
|
+
let syncSources = []
|
|
202
|
+
let fallbackSources = []
|
|
203
|
+
sources->Utils.Set.forEach(source => {
|
|
204
|
+
if (
|
|
205
|
+
source.sourceFor === Sync ||
|
|
206
|
+
// Even if the active source is a fallback, still include
|
|
207
|
+
// it to the list. So we don't wait for a timeout again
|
|
208
|
+
// if all main sync sources are still not valid
|
|
209
|
+
source === sourceManager.activeSource
|
|
210
|
+
) {
|
|
211
|
+
syncSources->Array.push(source)
|
|
212
|
+
} else {
|
|
213
|
+
fallbackSources->Array.push(source)
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
let status = ref(Active)
|
|
218
|
+
|
|
219
|
+
let (source, newBlockHeight) = await Promise.race(
|
|
220
|
+
syncSources
|
|
221
|
+
->Array.map(async source => {
|
|
222
|
+
(
|
|
223
|
+
source,
|
|
224
|
+
await sourceManager->getSourceNewHeight(~source, ~currentBlockHeight, ~status, ~logger),
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
->Array.concat([
|
|
228
|
+
Utils.delay(sourceManager.newBlockFallbackStallTimeout)->Promise.then(() => {
|
|
229
|
+
if status.contents !== Done {
|
|
230
|
+
status := Stalled
|
|
231
|
+
|
|
232
|
+
switch fallbackSources {
|
|
233
|
+
| [] =>
|
|
234
|
+
logger->Logging.childWarn(
|
|
235
|
+
`No new blocks detected within ${(sourceManager.newBlockFallbackStallTimeout / 1000)
|
|
236
|
+
->Int.toString}s. Polling will continue at a reduced rate. For better reliability, refer to our RPC fallback guide: https://docs.envio.dev/docs/HyperIndex/rpc-sync`,
|
|
237
|
+
)
|
|
238
|
+
| _ =>
|
|
239
|
+
logger->Logging.childWarn(
|
|
240
|
+
`No new blocks detected within ${(sourceManager.newBlockFallbackStallTimeout / 1000)
|
|
241
|
+
->Int.toString}s. Continuing polling with fallback RPC sources from the configuration.`,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Promise.race will be forever pending if fallbackSources is empty
|
|
246
|
+
// which is good for this use case
|
|
247
|
+
Promise.race(
|
|
248
|
+
fallbackSources->Array.map(async source => {
|
|
249
|
+
(
|
|
250
|
+
source,
|
|
251
|
+
await sourceManager->getSourceNewHeight(
|
|
252
|
+
~source,
|
|
253
|
+
~currentBlockHeight,
|
|
254
|
+
~status,
|
|
255
|
+
~logger,
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
}),
|
|
259
|
+
)
|
|
260
|
+
}),
|
|
261
|
+
]),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
sourceManager.activeSource = source
|
|
265
|
+
|
|
266
|
+
// Show a higher level log if we displayed a warning/error after newBlockFallbackStallTimeout
|
|
267
|
+
let log = status.contents === Stalled ? Logging.childInfo : Logging.childTrace
|
|
268
|
+
logger->log({
|
|
269
|
+
"msg": `New blocks successfully found.`,
|
|
270
|
+
"source": source.name,
|
|
271
|
+
"newBlockHeight": newBlockHeight,
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
status := Done
|
|
275
|
+
|
|
276
|
+
newBlockHeight
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let getNextSyncSource = (
|
|
280
|
+
sourceManager,
|
|
281
|
+
// This is needed to include the Fallback source to rotation
|
|
282
|
+
~initialSource,
|
|
283
|
+
// After multiple failures start returning fallback sources as well
|
|
284
|
+
// But don't try it when main sync sources fail because of invalid configuration
|
|
285
|
+
// note: The logic might be changed in the future
|
|
286
|
+
~attemptFallbacks=false,
|
|
287
|
+
) => {
|
|
288
|
+
let before = []
|
|
289
|
+
let after = []
|
|
290
|
+
|
|
291
|
+
let hasActive = ref(false)
|
|
292
|
+
|
|
293
|
+
sourceManager.sources->Utils.Set.forEach(source => {
|
|
294
|
+
if source === sourceManager.activeSource {
|
|
295
|
+
hasActive := true
|
|
296
|
+
} else if (
|
|
297
|
+
switch source.sourceFor {
|
|
298
|
+
| Sync => true
|
|
299
|
+
| Fallback => attemptFallbacks || source === initialSource
|
|
300
|
+
}
|
|
301
|
+
) {
|
|
302
|
+
(hasActive.contents ? after : before)->Array.push(source)
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
switch after->Array.get(0) {
|
|
307
|
+
| Some(s) => s
|
|
308
|
+
| None =>
|
|
309
|
+
switch before->Array.get(0) {
|
|
310
|
+
| Some(s) => s
|
|
311
|
+
| None => sourceManager.activeSource
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBlockHeight) => {
|
|
317
|
+
let toBlockRef = ref(
|
|
318
|
+
switch query.target {
|
|
319
|
+
| Head => None
|
|
320
|
+
| EndBlock({toBlock})
|
|
321
|
+
| Merge({toBlock}) =>
|
|
322
|
+
Some(toBlock)
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
let responseRef = ref(None)
|
|
326
|
+
let retryRef = ref(0)
|
|
327
|
+
let initialSource = sourceManager.activeSource
|
|
328
|
+
|
|
329
|
+
while responseRef.contents->Option.isNone {
|
|
330
|
+
let source = sourceManager.activeSource
|
|
331
|
+
let toBlock = toBlockRef.contents
|
|
332
|
+
let retry = retryRef.contents
|
|
333
|
+
|
|
334
|
+
let logger = Logging.createChild(
|
|
335
|
+
~params={
|
|
336
|
+
"chainId": source.chain->ChainMap.Chain.toChainId,
|
|
337
|
+
"logType": "Block Range Query",
|
|
338
|
+
"partitionId": query.partitionId,
|
|
339
|
+
"source": source.name,
|
|
340
|
+
"fromBlock": query.fromBlock,
|
|
341
|
+
"toBlock": toBlock,
|
|
342
|
+
"addresses": query.addressesByContractName->FetchState.addressesByContractNameCount,
|
|
343
|
+
"retry": retry,
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
let response = await source.getItemsOrThrow(
|
|
349
|
+
~fromBlock=query.fromBlock,
|
|
350
|
+
~toBlock,
|
|
351
|
+
~addressesByContractName=query.addressesByContractName,
|
|
352
|
+
~indexingContracts=query.indexingContracts,
|
|
353
|
+
~partitionId=query.partitionId,
|
|
354
|
+
~currentBlockHeight,
|
|
355
|
+
~selection=query.selection,
|
|
356
|
+
~retry,
|
|
357
|
+
~logger,
|
|
358
|
+
)
|
|
359
|
+
logger->Logging.childTrace({
|
|
360
|
+
"msg": "Fetched block range from server",
|
|
361
|
+
"toBlock": response.latestFetchedBlockNumber,
|
|
362
|
+
"numEvents": response.parsedQueueItems->Array.length,
|
|
363
|
+
"stats": response.stats,
|
|
364
|
+
})
|
|
365
|
+
responseRef := Some(response)
|
|
366
|
+
} catch {
|
|
367
|
+
| Source.GetItemsError(error) =>
|
|
368
|
+
switch error {
|
|
369
|
+
| UnsupportedSelection(_)
|
|
370
|
+
| FailedGettingFieldSelection(_)
|
|
371
|
+
| FailedParsingItems(_) => {
|
|
372
|
+
let nextSource = sourceManager->getNextSyncSource(~initialSource)
|
|
373
|
+
|
|
374
|
+
// These errors are impossible to recover, so we delete the source
|
|
375
|
+
// from sourceManager so it's not attempted anymore
|
|
376
|
+
let notAlreadyDeleted = sourceManager.sources->Utils.Set.delete(source)
|
|
377
|
+
|
|
378
|
+
// In case there are multiple partitions
|
|
379
|
+
// failing at the same time. Log only once
|
|
380
|
+
if notAlreadyDeleted {
|
|
381
|
+
switch error {
|
|
382
|
+
| UnsupportedSelection({message}) => logger->Logging.childError(message)
|
|
383
|
+
| FailedGettingFieldSelection({exn, message, blockNumber, logIndex})
|
|
384
|
+
| FailedParsingItems({exn, message, blockNumber, logIndex}) =>
|
|
385
|
+
logger->Logging.childError({
|
|
386
|
+
"msg": message,
|
|
387
|
+
"err": exn->Internal.prettifyExn,
|
|
388
|
+
"blockNumber": blockNumber,
|
|
389
|
+
"logIndex": logIndex,
|
|
390
|
+
})
|
|
391
|
+
| _ => ()
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if nextSource === source {
|
|
396
|
+
%raw(`null`)->ErrorHandling.mkLogAndRaise(
|
|
397
|
+
~logger,
|
|
398
|
+
~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
|
|
399
|
+
)
|
|
400
|
+
} else {
|
|
401
|
+
logger->Logging.childInfo({
|
|
402
|
+
"msg": "Switching to another data-source",
|
|
403
|
+
"source": nextSource.name,
|
|
404
|
+
})
|
|
405
|
+
sourceManager.activeSource = nextSource
|
|
406
|
+
retryRef := 0
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
| FailedGettingItems({attemptedToBlock, retry: WithSuggestedToBlock({toBlock})}) =>
|
|
410
|
+
logger->Logging.childTrace({
|
|
411
|
+
"msg": "Failed getting data for the block range. Immediately retrying with the suggested block range from response.",
|
|
412
|
+
"toBlock": attemptedToBlock,
|
|
413
|
+
"suggestedToBlock": toBlock,
|
|
414
|
+
})
|
|
415
|
+
toBlockRef := Some(toBlock)
|
|
416
|
+
retryRef := 0
|
|
417
|
+
| FailedGettingItems({exn, attemptedToBlock, retry: WithBackoff({message, backoffMillis})}) =>
|
|
418
|
+
// Starting from the 11th failure (retry=10)
|
|
419
|
+
// include fallback sources for switch
|
|
420
|
+
// (previously it would consider only sync sources or the initial one)
|
|
421
|
+
// This is a little bit tricky to find the right number,
|
|
422
|
+
// because meaning between RPC and HyperSync is different for the error
|
|
423
|
+
// but since Fallback was initially designed to be used only for height check
|
|
424
|
+
// just keep the value high
|
|
425
|
+
let attemptFallbacks = retry >= 10
|
|
426
|
+
|
|
427
|
+
let nextSource = switch retry {
|
|
428
|
+
// Don't attempt a switch on first two failure
|
|
429
|
+
| 0 | 1 => source
|
|
430
|
+
| _ =>
|
|
431
|
+
// Then try to switch every second failure
|
|
432
|
+
if retry->mod(2) === 0 {
|
|
433
|
+
sourceManager->getNextSyncSource(~initialSource, ~attemptFallbacks)
|
|
434
|
+
} else {
|
|
435
|
+
source
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Start displaying warnings after 4 failures
|
|
440
|
+
let log = retry >= 4 ? Logging.childWarn : Logging.childTrace
|
|
441
|
+
logger->log({
|
|
442
|
+
"msg": message,
|
|
443
|
+
"toBlock": attemptedToBlock,
|
|
444
|
+
"backOffMilliseconds": backoffMillis,
|
|
445
|
+
"retry": retry,
|
|
446
|
+
"err": exn->Internal.prettifyExn,
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
let shouldSwitch = nextSource !== source
|
|
450
|
+
if shouldSwitch {
|
|
451
|
+
logger->Logging.childInfo({
|
|
452
|
+
"msg": "Switching to another data-source",
|
|
453
|
+
"source": nextSource.name,
|
|
454
|
+
})
|
|
455
|
+
sourceManager.activeSource = nextSource
|
|
456
|
+
} else {
|
|
457
|
+
await Utils.delay(Pervasives.min(backoffMillis, 60_000))
|
|
458
|
+
}
|
|
459
|
+
retryRef := retryRef.contents + 1
|
|
460
|
+
}
|
|
461
|
+
// TODO: Handle more error cases and hang/retry instead of throwing
|
|
462
|
+
| exn => exn->ErrorHandling.mkLogAndRaise(~logger, ~msg="Failed to fetch block Range")
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
responseRef.contents->Option.getUnsafe
|
|
467
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type t
|
|
2
|
+
|
|
3
|
+
let make: (
|
|
4
|
+
~sources: array<Source.t>,
|
|
5
|
+
~maxPartitionConcurrency: int,
|
|
6
|
+
~newBlockFallbackStallTimeout: int=?,
|
|
7
|
+
~stalledPollingInterval: int=?,
|
|
8
|
+
~getHeightRetryInterval: (~retry: int) => int=?,
|
|
9
|
+
) => t
|
|
10
|
+
|
|
11
|
+
let getActiveSource: t => Source.t
|
|
12
|
+
|
|
13
|
+
let fetchNext: (
|
|
14
|
+
t,
|
|
15
|
+
~fetchState: FetchState.t,
|
|
16
|
+
~currentBlockHeight: int,
|
|
17
|
+
~executeQuery: FetchState.query => promise<unit>,
|
|
18
|
+
~waitForNewBlock: (~currentBlockHeight: int) => promise<int>,
|
|
19
|
+
~onNewBlock: (~currentBlockHeight: int) => unit,
|
|
20
|
+
~maxPerChainQueueSize: int,
|
|
21
|
+
~stateId: int,
|
|
22
|
+
) => promise<unit>
|
|
23
|
+
|
|
24
|
+
let waitForNewBlock: (t, ~currentBlockHeight: int) => promise<int>
|
|
25
|
+
|
|
26
|
+
let executeQuery: (
|
|
27
|
+
t,
|
|
28
|
+
~query: FetchState.query,
|
|
29
|
+
~currentBlockHeight: int,
|
|
30
|
+
) => promise<Source.blockRangeFetchResponse>
|
|
31
|
+
|
|
32
|
+
let makeGetHeightRetryInterval: (~initialRetryInterval: int, ~backoffMultiplicative: int, ~maxRetryInterval: int) => (~retry: int) => int
|