envio 2.21.0 → 2.21.2

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