envio 3.0.0-alpha.3 → 3.0.0-alpha.4

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.
@@ -2,13 +2,21 @@ open Belt
2
2
 
3
3
  type sourceManagerStatus = Idle | WaitingForNewBlock | Querieng
4
4
 
5
+ type sourceState = {
6
+ source: Source.t,
7
+ mutable knownHeight: int,
8
+ mutable unsubscribe: option<unit => unit>,
9
+ mutable pendingHeightResolvers: array<int => unit>,
10
+ mutable disabled: bool,
11
+ }
12
+
5
13
  // Ideally the ChainFetcher name suits this better
6
14
  // But currently the ChainFetcher module is immutable
7
15
  // and handles both processing and fetching.
8
16
  // So this module is to encapsulate the fetching logic only
9
17
  // with a mutable state for easier reasoning and testing.
10
18
  type t = {
11
- sources: Utils.Set.t<Source.t>,
19
+ sourcesState: array<sourceState>,
12
20
  mutable statusStart: Hrtime.timeRef,
13
21
  mutable status: sourceManagerStatus,
14
22
  maxPartitionConcurrency: int,
@@ -63,7 +71,13 @@ let make = (
63
71
  )
64
72
  {
65
73
  maxPartitionConcurrency,
66
- sources: Utils.Set.fromArray(sources),
74
+ sourcesState: sources->Array.map(source => {
75
+ source,
76
+ knownHeight: 0,
77
+ unsubscribe: None,
78
+ pendingHeightResolvers: [],
79
+ disabled: false,
80
+ }),
67
81
  activeSource: initialActiveSource,
68
82
  waitingForNewBlockStateId: None,
69
83
  fetchingPartitionsCount: 0,
@@ -159,60 +173,109 @@ let fetchNext = async (
159
173
 
160
174
  type status = Active | Stalled | Done
161
175
 
176
+ let disableSource = (sourceState: sourceState) => {
177
+ if !sourceState.disabled {
178
+ sourceState.disabled = true
179
+ switch sourceState.unsubscribe {
180
+ | Some(unsubscribe) => unsubscribe()
181
+ | None => ()
182
+ }
183
+ true
184
+ } else {
185
+ false
186
+ }
187
+ }
188
+
162
189
  let getSourceNewHeight = async (
163
190
  sourceManager,
164
- ~source: Source.t,
191
+ ~sourceState: sourceState,
165
192
  ~knownHeight,
166
193
  ~status: ref<status>,
167
194
  ~logger,
168
195
  ) => {
169
- let newHeight = ref(0)
196
+ let source = sourceState.source
197
+ let initialHeight = sourceState.knownHeight
198
+ let newHeight = ref(initialHeight)
170
199
  let retry = ref(0)
171
200
 
172
201
  while newHeight.contents <= knownHeight && status.contents !== Done {
173
- try {
174
- // Use to detect if the source is taking too long to respond
175
- let endTimer = Prometheus.SourceGetHeightDuration.startTimer({
176
- "source": source.name,
177
- "chainId": source.chain->ChainMap.Chain.toChainId,
202
+ // If subscription exists, wait for next height event
203
+ switch sourceState.unsubscribe {
204
+ | Some(_) =>
205
+ let height = await Promise.make((resolve, _reject) => {
206
+ sourceState.pendingHeightResolvers->Array.push(resolve)
178
207
  })
179
- let height = await source.getHeightOrThrow()
180
- endTimer()
181
-
182
- newHeight := height
183
- if height <= knownHeight {
184
- retry := 0
185
- // Slowdown polling when the chain isn't progressing
186
- let pollingInterval = if status.contents === Stalled {
187
- sourceManager.stalledPollingInterval
188
- } else {
189
- source.pollingInterval
208
+
209
+ // Only accept heights greater than initialHeight
210
+ if height > initialHeight {
211
+ newHeight := height
212
+ }
213
+ | None =>
214
+ // No subscription, use REST polling
215
+ try {
216
+ // Use to detect if the source is taking too long to respond
217
+ let endTimer = Prometheus.SourceGetHeightDuration.startTimer({
218
+ "source": source.name,
219
+ "chainId": source.chain->ChainMap.Chain.toChainId,
220
+ })
221
+ let height = await source.getHeightOrThrow()
222
+ endTimer()
223
+
224
+ newHeight := height
225
+ if height <= knownHeight {
226
+ retry := 0
227
+
228
+ // If createHeightSubscription is available and height hasn't changed,
229
+ // create subscription instead of polling
230
+ switch source.createHeightSubscription {
231
+ | Some(createSubscription) =>
232
+ let unsubscribe = createSubscription(~onHeight=newHeight => {
233
+ sourceState.knownHeight = newHeight
234
+ // Resolve all pending height resolvers
235
+ let resolvers = sourceState.pendingHeightResolvers
236
+ sourceState.pendingHeightResolvers = []
237
+ resolvers->Array.forEach(resolve => resolve(newHeight))
238
+ })
239
+ sourceState.unsubscribe = Some(unsubscribe)
240
+ | None =>
241
+ // Slowdown polling when the chain isn't progressing
242
+ let pollingInterval = if status.contents === Stalled {
243
+ sourceManager.stalledPollingInterval
244
+ } else {
245
+ source.pollingInterval
246
+ }
247
+ await Utils.delay(pollingInterval)
248
+ }
190
249
  }
191
- await Utils.delay(pollingInterval)
250
+ } catch {
251
+ | exn =>
252
+ let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
253
+ logger->Logging.childTrace({
254
+ "msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
255
+ "source": source.name,
256
+ "err": exn->Utils.prettifyExn,
257
+ })
258
+ retry := retry.contents + 1
259
+ await Utils.delay(retryInterval)
192
260
  }
193
- } catch {
194
- | exn =>
195
- let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
196
- logger->Logging.childTrace({
197
- "msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
198
- "source": source.name,
199
- "err": exn->Utils.prettifyExn,
200
- })
201
- retry := retry.contents + 1
202
- await Utils.delay(retryInterval)
203
261
  }
204
262
  }
205
- Prometheus.SourceHeight.set(
206
- ~sourceName=source.name,
207
- ~chainId=source.chain->ChainMap.Chain.toChainId,
208
- ~blockNumber=newHeight.contents,
209
- )
263
+
264
+ // Update Prometheus only if height increased
265
+ if newHeight.contents > initialHeight {
266
+ Prometheus.SourceHeight.set(
267
+ ~sourceName=source.name,
268
+ ~chainId=source.chain->ChainMap.Chain.toChainId,
269
+ ~blockNumber=newHeight.contents,
270
+ )
271
+ }
272
+
210
273
  newHeight.contents
211
274
  }
212
275
 
213
276
  // Polls for a block height greater than the given block number to ensure a new block is available for indexing.
214
277
  let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
215
- let {sources} = sourceManager
278
+ let {sourcesState} = sourceManager
216
279
 
217
280
  let logger = Logging.createChild(
218
281
  ~params={
@@ -230,8 +293,12 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
230
293
 
231
294
  let syncSources = []
232
295
  let fallbackSources = []
233
- sources->Utils.Set.forEach(source => {
234
- if (
296
+ sourcesState->Array.forEach(sourceState => {
297
+ let source = sourceState.source
298
+ if sourceState.disabled {
299
+ // Skip disabled sources
300
+ ()
301
+ } else if (
235
302
  source.sourceFor === Sync ||
236
303
  // Include Live sources only after initial sync has started
237
304
  // Live sources are optimized for real-time indexing with lower latency
@@ -241,9 +308,9 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
241
308
  // if all main sync sources are still not valid
242
309
  source === sourceManager.activeSource
243
310
  ) {
244
- syncSources->Array.push(source)
311
+ syncSources->Array.push(sourceState)
245
312
  } else {
246
- fallbackSources->Array.push(source)
313
+ fallbackSources->Array.push(sourceState)
247
314
  }
248
315
  })
249
316
 
@@ -251,8 +318,11 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
251
318
 
252
319
  let (source, newBlockHeight) = await Promise.race(
253
320
  syncSources
254
- ->Array.map(async source => {
255
- (source, await sourceManager->getSourceNewHeight(~source, ~knownHeight, ~status, ~logger))
321
+ ->Array.map(async sourceState => {
322
+ (
323
+ sourceState.source,
324
+ await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
325
+ )
256
326
  })
257
327
  ->Array.concat([
258
328
  Utils.delay(sourceManager.newBlockFallbackStallTimeout)->Promise.then(() => {
@@ -275,10 +345,10 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
275
345
  // Promise.race will be forever pending if fallbackSources is empty
276
346
  // which is good for this use case
277
347
  Promise.race(
278
- fallbackSources->Array.map(async source => {
348
+ fallbackSources->Array.map(async sourceState => {
279
349
  (
280
- source,
281
- await sourceManager->getSourceNewHeight(~source, ~knownHeight, ~status, ~logger),
350
+ sourceState.source,
351
+ await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
282
352
  )
283
353
  }),
284
354
  )
@@ -301,11 +371,11 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
301
371
  newBlockHeight
302
372
  }
303
373
 
304
- let getNextSyncSource = (
374
+ let getNextSyncSourceState = (
305
375
  sourceManager,
306
376
  // This is needed to include the Fallback source to rotation
307
- ~initialSource,
308
- ~currentSource,
377
+ ~initialSourceState: sourceState,
378
+ ~currentSourceState: sourceState,
309
379
  // After multiple failures start returning fallback sources as well
310
380
  // But don't try it when main sync sources fail because of invalid configuration
311
381
  // note: The logic might be changed in the future
@@ -316,18 +386,23 @@ let getNextSyncSource = (
316
386
 
317
387
  let hasActive = ref(false)
318
388
 
319
- sourceManager.sources->Utils.Set.forEach(source => {
320
- if source === currentSource {
389
+ sourceManager.sourcesState->Array.forEach(sourceState => {
390
+ let source = sourceState.source
391
+
392
+ // Skip disabled sources
393
+ if sourceState.disabled {
394
+ ()
395
+ } else if sourceState === currentSourceState {
321
396
  hasActive := true
322
397
  } else if (
323
398
  switch source.sourceFor {
324
399
  | Sync => true
325
400
  // Live sources should NOT be used for historical sync rotation
326
401
  // They are only meant for real-time indexing once synced
327
- | Live | Fallback => attemptFallbacks || source === initialSource
402
+ | Live | Fallback => attemptFallbacks || sourceState === initialSourceState
328
403
  }
329
404
  ) {
330
- (hasActive.contents ? after : before)->Array.push(source)
405
+ (hasActive.contents ? after : before)->Array.push(sourceState)
331
406
  }
332
407
  })
333
408
 
@@ -336,7 +411,7 @@ let getNextSyncSource = (
336
411
  | None =>
337
412
  switch before->Array.get(0) {
338
413
  | Some(s) => s
339
- | None => currentSource
414
+ | None => currentSourceState
340
415
  }
341
416
  }
342
417
  }
@@ -352,12 +427,16 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
352
427
  )
353
428
  let responseRef = ref(None)
354
429
  let retryRef = ref(0)
355
- let initialSource = sourceManager.activeSource
356
- let sourceRef = ref(initialSource)
430
+ let initialSourceState =
431
+ sourceManager.sourcesState
432
+ ->Js.Array2.find(s => s.source === sourceManager.activeSource)
433
+ ->Option.getUnsafe
434
+ let sourceStateRef = ref(initialSourceState)
357
435
  let shouldUpdateActiveSource = ref(false)
358
436
 
359
437
  while responseRef.contents->Option.isNone {
360
- let source = sourceRef.contents
438
+ let sourceState = sourceStateRef.contents
439
+ let source = sourceState.source
361
440
  let toBlock = toBlockRef.contents
362
441
  let retry = retryRef.contents
363
442
 
@@ -398,15 +477,19 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
398
477
  switch error {
399
478
  | UnsupportedSelection(_)
400
479
  | FailedGettingFieldSelection(_) => {
401
- let nextSource = sourceManager->getNextSyncSource(~initialSource, ~currentSource=source)
480
+ let nextSourceState =
481
+ sourceManager->getNextSyncSourceState(
482
+ ~initialSourceState,
483
+ ~currentSourceState=sourceState,
484
+ )
402
485
 
403
- // These errors are impossible to recover, so we delete the source
404
- // from sourceManager so it's not attempted anymore
405
- let notAlreadyDeleted = sourceManager.sources->Utils.Set.delete(source)
486
+ // These errors are impossible to recover, so we disable the source
487
+ // so it's not attempted anymore
488
+ let notAlreadyDisabled = disableSource(sourceState)
406
489
 
407
490
  // In case there are multiple partitions
408
491
  // failing at the same time. Log only once
409
- if notAlreadyDeleted {
492
+ if notAlreadyDisabled {
410
493
  switch error {
411
494
  | UnsupportedSelection({message}) => logger->Logging.childError(message)
412
495
  | FailedGettingFieldSelection({exn, message, blockNumber, logIndex}) =>
@@ -420,7 +503,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
420
503
  }
421
504
  }
422
505
 
423
- if nextSource === source {
506
+ if nextSourceState === sourceState {
424
507
  %raw(`null`)->ErrorHandling.mkLogAndRaise(
425
508
  ~logger,
426
509
  ~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
@@ -428,9 +511,9 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
428
511
  } else {
429
512
  logger->Logging.childInfo({
430
513
  "msg": "Switching to another data-source",
431
- "source": nextSource.name,
514
+ "source": nextSourceState.source.name,
432
515
  })
433
- sourceRef := nextSource
516
+ sourceStateRef := nextSourceState
434
517
  shouldUpdateActiveSource := true
435
518
  retryRef := 0
436
519
  }
@@ -444,14 +527,14 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
444
527
  toBlockRef := Some(toBlock)
445
528
  retryRef := 0
446
529
  | FailedGettingItems({exn, attemptedToBlock, retry: ImpossibleForTheQuery({message})}) =>
447
- let nextSource =
448
- sourceManager->getNextSyncSource(
449
- ~initialSource,
450
- ~currentSource=source,
530
+ let nextSourceState =
531
+ sourceManager->getNextSyncSourceState(
532
+ ~initialSourceState,
533
+ ~currentSourceState=sourceState,
451
534
  ~attemptFallbacks=true,
452
535
  )
453
536
 
454
- let hasAnotherSource = nextSource !== initialSource
537
+ let hasAnotherSource = nextSourceState !== initialSourceState
455
538
 
456
539
  logger->Logging.childWarn({
457
540
  "msg": message ++ (hasAnotherSource ? " - Attempting to another source" : ""),
@@ -465,7 +548,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
465
548
  ~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
466
549
  )
467
550
  } else {
468
- sourceRef := nextSource
551
+ sourceStateRef := nextSourceState
469
552
  shouldUpdateActiveSource := false
470
553
  retryRef := 0
471
554
  }
@@ -480,19 +563,19 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
480
563
  // just keep the value high
481
564
  let attemptFallbacks = retry >= 10
482
565
 
483
- let nextSource = switch retry {
566
+ let nextSourceState = switch retry {
484
567
  // Don't attempt a switch on first two failure
485
- | 0 | 1 => source
568
+ | 0 | 1 => sourceState
486
569
  | _ =>
487
570
  // Then try to switch every second failure
488
571
  if retry->mod(2) === 0 {
489
- sourceManager->getNextSyncSource(
490
- ~initialSource,
572
+ sourceManager->getNextSyncSourceState(
573
+ ~initialSourceState,
491
574
  ~attemptFallbacks,
492
- ~currentSource=source,
575
+ ~currentSourceState=sourceState,
493
576
  )
494
577
  } else {
495
- source
578
+ sourceState
496
579
  }
497
580
  }
498
581
 
@@ -506,13 +589,13 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
506
589
  "err": exn->Utils.prettifyExn,
507
590
  })
508
591
 
509
- let shouldSwitch = nextSource !== source
592
+ let shouldSwitch = nextSourceState !== sourceState
510
593
  if shouldSwitch {
511
594
  logger->Logging.childInfo({
512
595
  "msg": "Switching to another data-source",
513
- "source": nextSource.name,
596
+ "source": nextSourceState.source.name,
514
597
  })
515
- sourceRef := nextSource
598
+ sourceStateRef := nextSourceState
516
599
  shouldUpdateActiveSource := true
517
600
  } else {
518
601
  await Utils.delay(Pervasives.min(backoffMillis, 60_000))
@@ -526,7 +609,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeig
526
609
  }
527
610
 
528
611
  if shouldUpdateActiveSource.contents {
529
- sourceManager.activeSource = sourceRef.contents
612
+ sourceManager.activeSource = sourceStateRef.contents.source
530
613
  }
531
614
 
532
615
  responseRef.contents->Option.getUnsafe