envio 2.20.1 → 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.
@@ -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