envio 3.13.0-alpha.0 → 3.13.0

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.
Files changed (94) hide show
  1. package/package.json +6 -8
  2. package/src/BatchProcessing.res +4 -5
  3. package/src/BatchProcessing.res.mjs +4 -11
  4. package/src/ChainFetching.res +18 -38
  5. package/src/ChainFetching.res.mjs +14 -20
  6. package/src/ChainState.res +113 -77
  7. package/src/ChainState.res.mjs +90 -73
  8. package/src/ChainState.resi +14 -5
  9. package/src/Config.res +14 -17
  10. package/src/Config.res.mjs +5 -20
  11. package/src/CrossChainState.res +68 -20
  12. package/src/CrossChainState.res.mjs +48 -5
  13. package/src/CrossChainState.resi +1 -0
  14. package/src/Env.res +2 -2
  15. package/src/ErrorHandling.res +15 -4
  16. package/src/ErrorHandling.res.mjs +16 -5
  17. package/src/FinalizeBackfill.res +21 -9
  18. package/src/FinalizeBackfill.res.mjs +17 -4
  19. package/src/HandlerRegister.res +0 -37
  20. package/src/HandlerRegister.res.mjs +0 -18
  21. package/src/IndexerState.res +2 -0
  22. package/src/IndexerState.res.mjs +5 -0
  23. package/src/IndexerState.resi +1 -0
  24. package/src/Logging.res +6 -38
  25. package/src/Logging.res.mjs +5 -32
  26. package/src/Main.res +6 -9
  27. package/src/Main.res.mjs +3 -5
  28. package/src/Metrics.res +9 -4
  29. package/src/Metrics.res.mjs +3 -3
  30. package/src/Persistence.res +6 -3
  31. package/src/Persistence.res.mjs +5 -3
  32. package/src/PgStorage.res +2 -2
  33. package/src/PgStorage.res.mjs +2 -2
  34. package/src/Rollback.res +3 -3
  35. package/src/Server.res +0 -3
  36. package/src/SimulateItems.res +1 -1
  37. package/src/SimulateItems.res.mjs +1 -1
  38. package/src/Supervisor.res +236 -109
  39. package/src/Supervisor.res.mjs +185 -55
  40. package/src/TestIndexer.res +6 -3
  41. package/src/TestIndexer.res.mjs +3 -3
  42. package/src/Worker.res +6 -0
  43. package/src/Worker.res.mjs +2 -1
  44. package/src/bindings/NodeJs.res +25 -16
  45. package/src/bindings/NodeJs.res.mjs +8 -1
  46. package/src/sources/AddressSet.res +5 -3
  47. package/src/sources/AddressStore.res +19 -11
  48. package/src/sources/AddressStore.res.mjs +2 -7
  49. package/src/sources/BlockStore.res +10 -9
  50. package/src/sources/BlockStore.res.mjs +4 -4
  51. package/src/sources/ChainSources.res +4 -0
  52. package/src/sources/ChainSources.res.mjs +2 -2
  53. package/src/sources/EvmChain.res +4 -0
  54. package/src/sources/EvmChain.res.mjs +3 -1
  55. package/src/sources/EvmEventItem.res +56 -0
  56. package/src/sources/EvmEventItem.res.mjs +32 -0
  57. package/src/sources/EvmHyperSyncSource.res +7 -37
  58. package/src/sources/EvmHyperSyncSource.res.mjs +8 -29
  59. package/src/sources/EvmRpcClient.res +41 -59
  60. package/src/sources/EvmRpcClient.res.mjs +4 -54
  61. package/src/sources/EvmRpcWs.res +4 -1
  62. package/src/sources/EvmRpcWs.res.mjs +6 -2
  63. package/src/sources/FuelHyperSyncSource.res +6 -4
  64. package/src/sources/FuelHyperSyncSource.res.mjs +7 -9
  65. package/src/sources/HeightFeed.res +7 -1
  66. package/src/sources/HeightFeed.res.mjs +9 -7
  67. package/src/sources/HyperSyncClient.res +1 -14
  68. package/src/sources/RequestStat.res +7 -0
  69. package/src/sources/RequestStat.res.mjs +14 -1
  70. package/src/sources/RpcSource.res +100 -1043
  71. package/src/sources/RpcSource.res.mjs +81 -1056
  72. package/src/sources/SimulateSource.res +2 -5
  73. package/src/sources/SimulateSource.res.mjs +2 -3
  74. package/src/sources/Source.res +34 -12
  75. package/src/sources/Source.res.mjs +11 -0
  76. package/src/sources/SourceManager.res +111 -31
  77. package/src/sources/SourceManager.res.mjs +65 -31
  78. package/src/sources/SourceManager.resi +4 -0
  79. package/src/sources/StartBlockResolver.res +8 -5
  80. package/src/sources/StartBlockResolver.res.mjs +4 -2
  81. package/src/sources/SvmHyperSyncSource.res +4 -2
  82. package/src/sources/SvmHyperSyncSource.res.mjs +4 -6
  83. package/src/sources/TransactionStore.res +9 -6
  84. package/src/sources/TransactionStore.res.mjs +2 -2
  85. package/src/tui/Tui.res +4 -18
  86. package/src/tui/Tui.res.mjs +3 -6
  87. package/src/LazyLoader.res +0 -135
  88. package/src/LazyLoader.res.mjs +0 -118
  89. package/src/bindings/SDSL.res +0 -12
  90. package/src/bindings/SDSL.res.mjs +0 -9
  91. package/src/bindings/Yargs.res +0 -8
  92. package/src/bindings/Yargs.res.mjs +0 -2
  93. package/src/sources/Rpc.res +0 -185
  94. package/src/sources/Rpc.res.mjs +0 -183
@@ -17,7 +17,7 @@ let make = (
17
17
  pollingInterval: 0,
18
18
  getBlockHashes: (~blockNumbers as _, ~logger as _) => {
19
19
  Promise.resolve({
20
- Source.result: Ok(BlockStore.fromJs([], ~ecosystem, ~shouldChecksum=false)),
20
+ Source.result: Ok(BlockStore.fromJs([], ~ecosystem)),
21
21
  requestStats: [],
22
22
  })
23
23
  },
@@ -112,10 +112,7 @@ let make = (
112
112
  // EVM/Fuel keep transaction and block inline. SVM simulate returns a
113
113
  // store page so materialize + activity attach run the same path as HyperSync.
114
114
  transactionStore,
115
- blockStore: blockStore->Option.getOr(
116
- BlockStore.fromJs([], ~ecosystem, ~shouldChecksum=false),
117
- ),
118
- fromBlockQueried: fromBlock,
115
+ blockStore: blockStore->Option.getOr(BlockStore.fromJs([], ~ecosystem)),
119
116
  latestFetchedBlockNumber: toBlockQueried,
120
117
  stats: {
121
118
  totalTimeElapsed: 0.,
@@ -19,7 +19,7 @@ function make(items, endBlock, chainId, addressStore, ecosystemOpt, transactionS
19
19
  getBlockHashes: (param, param$1) => Promise.resolve({
20
20
  result: {
21
21
  TAG: "Ok",
22
- _0: BlockStore.fromJs([], ecosystem, false)
22
+ _0: BlockStore.fromJs([], ecosystem)
23
23
  },
24
24
  requestStats: []
25
25
  }),
@@ -80,8 +80,7 @@ function make(items, endBlock, chainId, addressStore, ecosystemOpt, transactionS
80
80
  knownHeight: reportedHeight,
81
81
  parsedQueueItems: parsedQueueItems,
82
82
  transactionStore: transactionStore,
83
- blockStore: Stdlib_Option.getOr(blockStore, BlockStore.fromJs([], ecosystem, false)),
84
- fromBlockQueried: fromBlock,
83
+ blockStore: Stdlib_Option.getOr(blockStore, BlockStore.fromJs([], ecosystem)),
85
84
  latestFetchedBlockNumber: toBlockQueried,
86
85
  stats: {
87
86
  "total time elapsed (s)": 0
@@ -78,14 +78,13 @@ type blockRangeFetchResponse = {
78
78
  parsedQueueItems: array<Internal.item>,
79
79
  // Page of transactions for this response's items, keyed by (blockNumber,
80
80
  // transactionIndex); merged into the chain's store on apply. `None` for
81
- // sources that keep the transaction inline on the payload (RPC/Fuel/Simulate).
81
+ // sources that keep the transaction inline on the payload (Fuel/Simulate).
82
82
  transactionStore: option<TransactionStore.t>,
83
83
  // Page of blocks observed while fetching this range, keyed by block number;
84
84
  // merged into the chain's store on apply, where its hashes drive reorg
85
- // detection. Sources that keep the block inline on the payload (RPC/Simulate)
86
- // contribute hash-only rows built from the block hashes they saw.
85
+ // detection. A source that keeps the block inline on the payload (Simulate)
86
+ // contributes hash-only rows built from the block hashes it saw.
87
87
  blockStore: BlockStore.t,
88
- fromBlockQueried: int,
89
88
  latestFetchedBlockNumber: int,
90
89
  stats: blockRangeFetchStats,
91
90
  requestStats: array<requestStat>,
@@ -122,13 +121,35 @@ type getItemsRetry =
122
121
  type rateLimited = {resetMs: int, requestStats: array<requestStat>}
123
122
  exception RateLimited(rateLimited)
124
123
 
124
+ // The two failures that can follow requests carry their timings, so a page
125
+ // that ends in a retry still counts towards the source's metrics — the same
126
+ // contract `RateLimited` and `SourceBehindHead` keep.
125
127
  type getItemsError =
126
128
  | UnsupportedSelection({message: string})
127
- | FailedGettingFieldSelection({exn: exn, blockNumber: int, logIndex: int, message: string})
128
- | FailedGettingItems({exn: exn, attemptedToBlock: int, retry: getItemsRetry})
129
+ | FailedGettingFieldSelection({
130
+ // The cause, where the source has one beyond `message`.
131
+ exn?: exn,
132
+ blockNumber: int,
133
+ message: string,
134
+ requestStats: array<requestStat>,
135
+ })
136
+ | FailedGettingItems({
137
+ // The cause, where the source has one beyond the retry's own message.
138
+ exn?: exn,
139
+ attemptedToBlock: int,
140
+ retry: getItemsRetry,
141
+ requestStats: array<requestStat>,
142
+ })
129
143
 
130
144
  exception GetItemsError(getItemsError)
131
145
 
146
+ let getItemsErrorRequestStats = (error: getItemsError) =>
147
+ switch error {
148
+ // Raised before anything is requested.
149
+ | UnsupportedSelection(_) => []
150
+ | FailedGettingFieldSelection({requestStats}) | FailedGettingItems({requestStats}) => requestStats
151
+ }
152
+
132
153
  type sourceFor = Sync | Fallback | Realtime
133
154
 
134
155
  // Why a height subscription's connection ended. Closed, so a consumer decides
@@ -222,11 +243,12 @@ type t = {
222
243
  ~onHeight: int => unit,
223
244
  ~onStatus: heightSubscriptionStatus => unit,
224
245
  ) => unit => unit,
225
- // Invoked when a reorg or internally inconsistent response means local state
226
- // may point at an orphaned chain (e.g. the RPC block cache): drop all of it.
227
- // Deliberately takes no rollback target — the deepest reorged block isn't
228
- // known until the depth search runs, and that search reads back through this
229
- // very state, so pruning relative to a target would keep exactly the entries
230
- // that make it answer wrong.
246
+ // Invoked when a reorg or an internally inconsistent response means what the
247
+ // source is holding may describe an orphaned chain — for RPC, the reads still
248
+ // in flight, which anything asking afterwards would otherwise join. The
249
+ // stores are rolled back separately; this is only about what the source
250
+ // itself carries. Deliberately takes no rollback target: the deepest reorged
251
+ // block isn't known until the depth search runs, and that search reads back
252
+ // through this very state.
231
253
  onReorg?: unit => unit,
232
254
  }
@@ -50,6 +50,16 @@ let RateLimited = /* @__PURE__ */Primitive_exceptions.create("Source.RateLimited
50
50
 
51
51
  let GetItemsError = /* @__PURE__ */Primitive_exceptions.create("Source.GetItemsError");
52
52
 
53
+ function getItemsErrorRequestStats(error) {
54
+ switch (error.TAG) {
55
+ case "UnsupportedSelection" :
56
+ return [];
57
+ case "FailedGettingFieldSelection" :
58
+ case "FailedGettingItems" :
59
+ return error.requestStats;
60
+ }
61
+ }
62
+
53
63
  function downReasonLabel(reason) {
54
64
  if (typeof reason === "object") {
55
65
  return `http-` + reason._0.toString();
@@ -82,6 +92,7 @@ export {
82
92
  SourceBehindHead,
83
93
  RateLimited,
84
94
  GetItemsError,
95
+ getItemsErrorRequestStats,
85
96
  downReasonLabel,
86
97
  }
87
98
  /* nativeFailurePayloadSchema Not a pure module */
@@ -84,6 +84,10 @@ type t = {
84
84
  // Should take into consideration partitions fetching for previous states (before rollback)
85
85
  mutable fetchingPartitionsCount: int,
86
86
  recoveryTimeout: float,
87
+ // Why a source was last disabled for good. A disabled source is never
88
+ // retried, so by the time the manager runs out of sources this is the reason
89
+ // it did — and the only thing that tells a caller what to change.
90
+ mutable disableReason: option<string>,
87
91
  mutable hasRealtime: bool,
88
92
  mutable committedRateLimitTimeMs: float,
89
93
  mutable rateLimitWaiters: int,
@@ -386,6 +390,7 @@ let make = (
386
390
  idleSeconds: 0.,
387
391
  waitingForNewBlockSeconds: 0.,
388
392
  queryingSeconds: 0.,
393
+ disableReason: None,
389
394
  hasRealtime,
390
395
  committedRateLimitTimeMs: 0.0,
391
396
  rateLimitWaiters: 0,
@@ -637,10 +642,20 @@ let retryBehindHead = async (
637
642
  )
638
643
  }
639
644
 
640
- // A source that keeps contradicting itself is not mid-reorg, it is broken, and
641
- // no amount of retrying moves the chain forward. Roughly five minutes of the
642
- // backoff schedule below.
643
- let inconsistentResponseStallRetries = 13
645
+ // A query that has failed this many times running has outlived every failover
646
+ // the schedule above can offer: it has rotated through the chain's sources
647
+ // several times over and none of them answered. Whatever the reported cause,
648
+ // the chain is no longer making progress and the operator has to hear about it.
649
+ let stallRetries = 13
650
+
651
+ // Which level a retry is reported at, and what it says once the retries have
652
+ // outlasted the rotation.
653
+ let stalledLog = (~retry, ~warnAfter, ~msg, ~stalled) =>
654
+ if retry >= stallRetries {
655
+ (Logging.childError, msg ++ stalled)
656
+ } else {
657
+ (retry >= warnAfter ? Logging.childWarn : Logging.childTrace, msg)
658
+ }
644
659
 
645
660
  // The response contradicted itself (the same block twice with different hashes,
646
661
  // or a requested hash missing). It may be a reorg mid-request, so refetch before
@@ -657,14 +672,12 @@ let retryInconsistentResponse = async (
657
672
  ) => {
658
673
  let backoffMillis = retry->retryBackoffMillis
659
674
  let msg = `Received a partial indicator of a possible reorg from the ${sourceState.source.name} source while fetching ${method}. Retrying the request to better identify whether a reorg happened.`
660
- let (log, msg) = if retry >= inconsistentResponseStallRetries {
661
- (
662
- Logging.childError,
663
- msg ++ " It has disagreed with itself on every attempt for several minutes now, so this chain has stopped making progress - the endpoint is likely serving blocks and logs from nodes on different chains.",
664
- )
665
- } else {
666
- (retry >= 2 ? Logging.childWarn : Logging.childTrace, msg)
667
- }
675
+ let (log, msg) = stalledLog(
676
+ ~retry,
677
+ ~warnAfter=2,
678
+ ~msg,
679
+ ~stalled=" It has disagreed with itself on every attempt for several minutes now, so this chain has stopped making progress - the endpoint is likely serving blocks and logs from nodes on different chains.",
680
+ )
668
681
  logger->log({
669
682
  "msg": msg,
670
683
  "method": method,
@@ -686,6 +699,45 @@ let retryInconsistentResponse = async (
686
699
  )
687
700
  }
688
701
 
702
+ // The source could not serve the page and reported nothing that changes the
703
+ // query, so the only move left is to wait and let the schedule try another
704
+ // source. Escalates once the retries have outlasted every failover.
705
+ let retryFailedPage = async (
706
+ sourceManager: t,
707
+ sourceState: sourceState,
708
+ ~retry,
709
+ ~isRealtime,
710
+ ~logger: Pino.t,
711
+ ~msg: string,
712
+ ~attemptedToBlock: int,
713
+ ~backoffMillis: int,
714
+ ~minBackoffMillis=0,
715
+ ~err: option<exn>,
716
+ ~excludedSources=?,
717
+ ) => {
718
+ let (log, msg) = stalledLog(
719
+ ~retry,
720
+ ~warnAfter=4,
721
+ ~msg,
722
+ ~stalled=" No source available to this chain has served this query on any attempt since, so indexing has stopped making progress.",
723
+ )
724
+ logger->log({
725
+ "msg": msg,
726
+ "toBlock": attemptedToBlock,
727
+ "backOffMilliseconds": backoffMillis,
728
+ "retry": retry,
729
+ "err": err->Option.map(Utils.prettifyExn),
730
+ })
731
+ await sourceManager->backoffBeforeRetry(
732
+ sourceState,
733
+ ~retry,
734
+ ~isRealtime,
735
+ ~backoffMillis,
736
+ ~minBackoffMillis,
737
+ ~excludedSources?,
738
+ )
739
+ }
740
+
689
741
  // Polls for a block height greater than the given block number to ensure a new block is available for indexing.
690
742
  /*
691
743
  Resolves at the first height above `knownHeight` that any eligible source
@@ -895,7 +947,13 @@ let executeQuery = async (
895
947
  ~knownHeight,
896
948
  ~isRealtime,
897
949
  ) => {
898
- let noSourcesError = "The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team."
950
+ // Read when the manager actually runs out of sources, not on the way in: the
951
+ // reason is recorded by the failure that disabled the last one.
952
+ let noSourcesError = () => switch sourceManager.disableReason {
953
+ | Some(reason) =>
954
+ `The indexer doesn't have data-sources which can continue fetching. The last one was disabled because ${reason}`
955
+ | None => "The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team."
956
+ }
899
957
 
900
958
  // Sources where the query is impossible - lazily allocated, excluded for the duration of this query
901
959
  let excludedSourcesRef = ref(None)
@@ -923,7 +981,7 @@ let executeQuery = async (
923
981
  s
924
982
  | None =>
925
983
  let logger = Logging.createChild(~params={"chainId": sourceManager.activeSource.chainId})
926
- %raw(`null`)->ErrorHandling.mkLogAndRaise(~logger, ~msg=noSourcesError)
984
+ %raw(`null`)->ErrorHandling.mkLogAndRaise(~logger, ~msg=noSourcesError())
927
985
  }
928
986
  sourceManager.activeSource = sourceState.source
929
987
  let source = sourceState.source
@@ -999,6 +1057,9 @@ let executeQuery = async (
999
1057
  retryRef := retryRef.contents + 1
1000
1058
 
1001
1059
  | Source.GetItemsError(error) =>
1060
+ // A page that ends in a retry still made requests; count them, as the
1061
+ // rate-limit and behind-head arms above do.
1062
+ sourceState->recordRequestStats(error->Source.getItemsErrorRequestStats)
1002
1063
  switch error {
1003
1064
  | UnsupportedSelection(_)
1004
1065
  | FailedGettingFieldSelection(_) => {
@@ -1010,13 +1071,15 @@ let executeQuery = async (
1010
1071
  // failing at the same time. Log only once
1011
1072
  if notAlreadyDisabled {
1012
1073
  switch error {
1013
- | UnsupportedSelection({message}) => logger->Logging.childError(message)
1014
- | FailedGettingFieldSelection({exn, message, blockNumber, logIndex}) =>
1074
+ | UnsupportedSelection({message}) =>
1075
+ sourceManager.disableReason = Some(message)
1076
+ logger->Logging.childError(message)
1077
+ | FailedGettingFieldSelection({?exn, message, blockNumber}) =>
1078
+ sourceManager.disableReason = Some(message)
1015
1079
  logger->Logging.childError({
1016
1080
  "msg": message,
1017
- "err": exn->Utils.prettifyExn,
1081
+ "err": exn->Option.map(Utils.prettifyExn),
1018
1082
  "blockNumber": blockNumber,
1019
- "logIndex": logIndex,
1020
1083
  })
1021
1084
  | _ => ()
1022
1085
  }
@@ -1024,6 +1087,26 @@ let executeQuery = async (
1024
1087
 
1025
1088
  retryRef := 0
1026
1089
  }
1090
+ // A suggestion that doesn't narrow the range asks for the query that just
1091
+ // failed. Retrying it unchanged and without a wait makes no progress and
1092
+ // never counts against the source, so pace it like any other failure and
1093
+ // let the schedule move on to one that can answer.
1094
+ | FailedGettingItems({?exn, attemptedToBlock, retry: WithSuggestedToBlock({toBlock})})
1095
+ if toBlock >= attemptedToBlock =>
1096
+ await sourceManager->retryFailedPage(
1097
+ sourceState,
1098
+ ~retry,
1099
+ ~isRealtime,
1100
+ ~logger,
1101
+ ~msg=`The ${source.name} source rejected the block range and suggested #${toBlock->Int.toString}, which is no narrower than the #${attemptedToBlock->Int.toString} it just refused.`,
1102
+ ~attemptedToBlock,
1103
+ ~backoffMillis=retry->retryBackoffMillis,
1104
+ ~minBackoffMillis=minRecoverableBackoffMillis,
1105
+ ~err=exn,
1106
+ ~excludedSources=?excludedSourcesRef.contents,
1107
+ )
1108
+ retryRef := retryRef.contents + 1
1109
+
1027
1110
  | FailedGettingItems({attemptedToBlock, retry: WithSuggestedToBlock({toBlock})}) =>
1028
1111
  logger->Logging.childTrace({
1029
1112
  "msg": "Failed getting data for the block range. Immediately retrying with the suggested block range from response.",
@@ -1031,8 +1114,10 @@ let executeQuery = async (
1031
1114
  "suggestedToBlock": toBlock,
1032
1115
  })
1033
1116
  toBlockRef := Some(toBlock)
1117
+ // The next attempt asks a strictly smaller question, so it starts a
1118
+ // fresh schedule rather than continuing this one.
1034
1119
  retryRef := 0
1035
- | FailedGettingItems({exn, attemptedToBlock, retry: ImpossibleForTheQuery({message})}) =>
1120
+ | FailedGettingItems({?exn, attemptedToBlock, retry: ImpossibleForTheQuery({message})}) =>
1036
1121
  // Don't set lastFailedAt - the source isn't broken, the query just can't work on it
1037
1122
  let excludedSources = switch excludedSourcesRef.contents {
1038
1123
  | Some(s) => s
@@ -1046,25 +1131,20 @@ let executeQuery = async (
1046
1131
  logger->Logging.childWarn({
1047
1132
  "msg": message ++ " - Attempting another source",
1048
1133
  "toBlock": attemptedToBlock,
1049
- "err": exn->Utils.prettifyExn,
1134
+ "err": exn->Option.map(Utils.prettifyExn),
1050
1135
  })
1051
1136
  retryRef := 0
1052
1137
 
1053
- | FailedGettingItems({exn, attemptedToBlock, retry: WithBackoff({message, backoffMillis})}) =>
1054
- // Start displaying warnings after 4 failures
1055
- let log = retry >= 4 ? Logging.childWarn : Logging.childTrace
1056
- logger->log({
1057
- "msg": message,
1058
- "toBlock": attemptedToBlock,
1059
- "backOffMilliseconds": backoffMillis,
1060
- "retry": retry,
1061
- "err": exn->Utils.prettifyExn,
1062
- })
1063
- await sourceManager->backoffBeforeRetry(
1138
+ | FailedGettingItems({?exn, attemptedToBlock, retry: WithBackoff({message, backoffMillis})}) =>
1139
+ await sourceManager->retryFailedPage(
1064
1140
  sourceState,
1065
1141
  ~retry,
1066
1142
  ~isRealtime,
1143
+ ~logger,
1144
+ ~msg=message,
1145
+ ~attemptedToBlock,
1067
1146
  ~backoffMillis,
1147
+ ~err=exn,
1068
1148
  ~excludedSources=?excludedSourcesRef.contents,
1069
1149
  )
1070
1150
  retryRef := retryRef.contents + 1
@@ -311,6 +311,7 @@ function make(sources, isRealtime, newBlockStallTimeoutOpt, newBlockStallTimeout
311
311
  waitingLogged: false,
312
312
  fetchingPartitionsCount: 0,
313
313
  recoveryTimeout: recoveryTimeout,
314
+ disableReason: undefined,
314
315
  hasRealtime: hasRealtime,
315
316
  committedRateLimitTimeMs: 0.0,
316
317
  rateLimitWaiters: 0,
@@ -509,16 +510,24 @@ async function retryBehindHead(sourceManager, sourceState, retry, isRealtime, lo
509
510
  return await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, 50, excludedSources);
510
511
  }
511
512
 
512
- async function retryInconsistentResponse(sourceManager, sourceState, retry, isRealtime, logger, method, err, excludedSources) {
513
- let backoffMillis = Utils.expBackoff(100, retry, 60000);
514
- let msg = `Received a partial indicator of a possible reorg from the ` + sourceState.source.name + ` source while fetching ` + method + `. Retrying the request to better identify whether a reorg happened.`;
515
- let match = retry >= 13 ? [
513
+ function stalledLog(retry, warnAfter, msg, stalled) {
514
+ if (retry >= 13) {
515
+ return [
516
516
  Logging.childError,
517
- msg + " It has disagreed with itself on every attempt for several minutes now, so this chain has stopped making progress - the endpoint is likely serving blocks and logs from nodes on different chains."
518
- ] : [
519
- retry >= 2 ? Logging.childWarn : Logging.childTrace,
517
+ msg + stalled
518
+ ];
519
+ } else {
520
+ return [
521
+ retry >= warnAfter ? Logging.childWarn : Logging.childTrace,
520
522
  msg
521
523
  ];
524
+ }
525
+ }
526
+
527
+ async function retryInconsistentResponse(sourceManager, sourceState, retry, isRealtime, logger, method, err, excludedSources) {
528
+ let backoffMillis = Utils.expBackoff(100, retry, 60000);
529
+ let msg = `Received a partial indicator of a possible reorg from the ` + sourceState.source.name + ` source while fetching ` + method + `. Retrying the request to better identify whether a reorg happened.`;
530
+ let match = stalledLog(retry, 2, msg, " It has disagreed with itself on every attempt for several minutes now, so this chain has stopped making progress - the endpoint is likely serving blocks and logs from nodes on different chains.");
522
531
  match[0](logger, {
523
532
  msg: match[1],
524
533
  method: method,
@@ -530,6 +539,19 @@ async function retryInconsistentResponse(sourceManager, sourceState, retry, isRe
530
539
  return await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, 50, excludedSources);
531
540
  }
532
541
 
542
+ async function retryFailedPage(sourceManager, sourceState, retry, isRealtime, logger, msg, attemptedToBlock, backoffMillis, minBackoffMillisOpt, err, excludedSources) {
543
+ let minBackoffMillis = minBackoffMillisOpt !== undefined ? minBackoffMillisOpt : 0;
544
+ let match = stalledLog(retry, 4, msg, " No source available to this chain has served this query on any attempt since, so indexing has stopped making progress.");
545
+ match[0](logger, {
546
+ msg: match[1],
547
+ toBlock: attemptedToBlock,
548
+ backOffMilliseconds: backoffMillis,
549
+ retry: retry,
550
+ err: Stdlib_Option.map(err, Utils.prettifyExn)
551
+ });
552
+ return await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, minBackoffMillis, excludedSources);
553
+ }
554
+
533
555
  function waitForNewBlock(sourceManager, knownHeight, isRealtime, reducedPolling) {
534
556
  let sourcesState = sourceManager.sourcesState;
535
557
  let logger = Logging.createChild({
@@ -650,6 +672,14 @@ function waitForNewBlock(sourceManager, knownHeight, isRealtime, reducedPolling)
650
672
  }
651
673
 
652
674
  async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
675
+ let noSourcesError = () => {
676
+ let reason = sourceManager.disableReason;
677
+ if (reason !== undefined) {
678
+ return `The indexer doesn't have data-sources which can continue fetching. The last one was disabled because ` + reason;
679
+ } else {
680
+ return "The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.";
681
+ }
682
+ };
653
683
  let excludedSourcesRef;
654
684
  let toBlockRef = query.toBlock;
655
685
  let responseRef;
@@ -674,7 +704,7 @@ async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
674
704
  let logger$1 = Logging.createChild({
675
705
  chainId: sourceManager.activeSource.chainId
676
706
  });
677
- sourceState = ErrorHandling.mkLogAndRaise(logger$1, "The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.", null);
707
+ sourceState = ErrorHandling.mkLogAndRaise(logger$1, noSourcesError(), null);
678
708
  }
679
709
  sourceManager.activeSource = sourceState.source;
680
710
  let source = sourceState.source;
@@ -713,6 +743,8 @@ async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
713
743
  retryRef = retryRef + 1 | 0;
714
744
  } else if (err.RE_EXN_ID === Source.GetItemsError) {
715
745
  let error = err._1;
746
+ let requestStats = Source.getItemsErrorRequestStats(error);
747
+ recordStatsInto(sourceState.requestStats, requestStats);
716
748
  let exit = 0;
717
749
  switch (error.TAG) {
718
750
  case "UnsupportedSelection" :
@@ -726,25 +758,21 @@ async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
726
758
  switch (match$1.TAG) {
727
759
  case "WithSuggestedToBlock" :
728
760
  let toBlock$1 = match$1.toBlock;
729
- Logging.childTrace(logger$2, {
730
- msg: "Failed getting data for the block range. Immediately retrying with the suggested block range from response.",
731
- toBlock: attemptedToBlock,
732
- suggestedToBlock: toBlock$1
733
- });
734
- toBlockRef = toBlock$1;
735
- retryRef = 0;
761
+ if (toBlock$1 >= attemptedToBlock) {
762
+ await retryFailedPage(sourceManager, sourceState, retry, isRealtime, logger$2, `The ` + source.name + ` source rejected the block range and suggested #` + toBlock$1.toString() + `, which is no narrower than the #` + attemptedToBlock.toString() + ` it just refused.`, attemptedToBlock, Utils.expBackoff(100, retry, 60000), 50, exn, excludedSourcesRef);
763
+ retryRef = retryRef + 1 | 0;
764
+ } else {
765
+ Logging.childTrace(logger$2, {
766
+ msg: "Failed getting data for the block range. Immediately retrying with the suggested block range from response.",
767
+ toBlock: attemptedToBlock,
768
+ suggestedToBlock: toBlock$1
769
+ });
770
+ toBlockRef = toBlock$1;
771
+ retryRef = 0;
772
+ }
736
773
  break;
737
774
  case "WithBackoff" :
738
- let backoffMillis = match$1.backoffMillis;
739
- let log = retry >= 4 ? Logging.childWarn : Logging.childTrace;
740
- log(logger$2, {
741
- msg: match$1.message,
742
- toBlock: attemptedToBlock,
743
- backOffMilliseconds: backoffMillis,
744
- retry: retry,
745
- err: Utils.prettifyExn(exn)
746
- });
747
- await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, undefined, excludedSourcesRef);
775
+ await retryFailedPage(sourceManager, sourceState, retry, isRealtime, logger$2, match$1.message, attemptedToBlock, match$1.backoffMillis, undefined, exn, excludedSourcesRef);
748
776
  retryRef = retryRef + 1 | 0;
749
777
  break;
750
778
  case "ImpossibleForTheQuery" :
@@ -761,7 +789,7 @@ async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
761
789
  Logging.childWarn(logger$2, {
762
790
  msg: match$1.message + " - Attempting another source",
763
791
  toBlock: attemptedToBlock,
764
- err: Utils.prettifyExn(exn)
792
+ err: Stdlib_Option.map(exn, Utils.prettifyExn)
765
793
  });
766
794
  retryRef = 0;
767
795
  break;
@@ -773,14 +801,17 @@ async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
773
801
  if (notAlreadyDisabled) {
774
802
  switch (error.TAG) {
775
803
  case "UnsupportedSelection" :
776
- Logging.childError(logger$2, error.message);
804
+ let message = error.message;
805
+ sourceManager.disableReason = message;
806
+ Logging.childError(logger$2, message);
777
807
  break;
778
808
  case "FailedGettingFieldSelection" :
809
+ let message$1 = error.message;
810
+ sourceManager.disableReason = message$1;
779
811
  Logging.childError(logger$2, {
780
- msg: error.message,
781
- err: Utils.prettifyExn(error.exn),
782
- blockNumber: error.blockNumber,
783
- logIndex: error.logIndex
812
+ msg: message$1,
813
+ err: Stdlib_Option.map(error.exn, Utils.prettifyExn),
814
+ blockNumber: error.blockNumber
784
815
  });
785
816
  break;
786
817
  case "FailedGettingItems" :
@@ -863,6 +894,8 @@ async function getBlockHashes(sourceManager, blockNumbers, isRealtime) {
863
894
  return responseRef;
864
895
  }
865
896
 
897
+ let stallRetries = 13;
898
+
866
899
  export {
867
900
  getSourceRole,
868
901
  make,
@@ -877,6 +910,7 @@ export {
877
910
  onReorg,
878
911
  dispatch,
879
912
  waitForNewBlock,
913
+ stallRetries,
880
914
  executeQuery,
881
915
  makeGetHeightRetryInterval,
882
916
  getBlockHashes,
@@ -74,6 +74,10 @@ let waitForNewBlock: (
74
74
  ~reducedPolling: bool,
75
75
  ) => promise<int>
76
76
 
77
+ // The retry at which a query that no source can answer stops being a hiccup
78
+ // and gets reported as a stalled chain.
79
+ let stallRetries: int
80
+
77
81
  let executeQuery: (
78
82
  t,
79
83
  ~query: FetchState.query,
@@ -14,12 +14,15 @@
14
14
  // sources the chain goes on to index with; those are built later, once
15
15
  // registrations exist.
16
16
  let makeProbeSources = (chainConfig: Config.chain, ~lowercaseAddresses): array<Source.t> => {
17
- let addressStore = AddressStore.make(
18
- ~ecosystem=chainConfig.ecosystem,
19
- ~shouldChecksum=!lowercaseAddresses,
20
- ~contracts=[],
17
+ let addressStore = AddressStore.make(~ecosystem=chainConfig.ecosystem, ~contracts=[])
18
+ ChainSources.make(
19
+ ~chainConfig,
20
+ ~onEventRegistrations=[],
21
+ ~addressStore,
22
+ ~lowercaseAddresses,
23
+ ~blockStore=BlockStore.make(~ecosystem=chainConfig.ecosystem),
24
+ ~transactionStore=TransactionStore.make(~ecosystem=chainConfig.ecosystem),
21
25
  )
22
- ChainSources.make(~chainConfig, ~onEventRegistrations=[], ~addressStore, ~lowercaseAddresses)
23
26
  }
24
27
 
25
28
  // How long to keep asking a chain that won't answer.
@@ -3,16 +3,18 @@
3
3
  import * as Utils from "../Utils.res.mjs";
4
4
  import * as ChainId from "../ChainId.res.mjs";
5
5
  import * as Logging from "../Logging.res.mjs";
6
+ import * as BlockStore from "./BlockStore.res.mjs";
6
7
  import * as AddressStore from "./AddressStore.res.mjs";
7
8
  import * as ChainSources from "./ChainSources.res.mjs";
8
9
  import * as SourceManager from "./SourceManager.res.mjs";
9
10
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
11
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
12
+ import * as TransactionStore from "./TransactionStore.res.mjs";
11
13
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
12
14
 
13
15
  function makeProbeSources(chainConfig, lowercaseAddresses) {
14
- let addressStore = AddressStore.make(chainConfig.ecosystem, !lowercaseAddresses, []);
15
- return ChainSources.make(chainConfig, [], addressStore, lowercaseAddresses);
16
+ let addressStore = AddressStore.make(chainConfig.ecosystem, []);
17
+ return ChainSources.make(chainConfig, [], addressStore, lowercaseAddresses, BlockStore.make(chainConfig.ecosystem), TransactionStore.make(chainConfig.ecosystem));
16
18
  }
17
19
 
18
20
  function candidateSources(sources) {
@@ -158,6 +158,8 @@ let make = (
158
158
  let totalTimeRef = Performance.now()
159
159
  let pageFetchRef = Performance.now()
160
160
 
161
+ let fetchStats = () => RequestStat.single(~method="getInstructions", ~sentAt=pageFetchRef)
162
+
161
163
  let query: SvmHyperSyncClient.EventItems.query = {
162
164
  fromSlot: fromBlock,
163
165
  toSlot: toBlock,
@@ -179,6 +181,7 @@ let make = (
179
181
  throw(
180
182
  Source.GetItemsError(
181
183
  Source.FailedGettingItems({
184
+ requestStats: fetchStats(),
182
185
  exn,
183
186
  attemptedToBlock: toBlock->Option.getOr(knownHeight),
184
187
  retry: WithBackoff({
@@ -193,7 +196,7 @@ let make = (
193
196
  )
194
197
  }
195
198
  let pageFetchTime = pageFetchRef->Performance.secondsSince
196
- let requestStats = [{Source.method: "getInstructions", seconds: pageFetchTime}]
199
+ let requestStats = fetchStats()
197
200
 
198
201
  let parsingRef = Performance.now()
199
202
 
@@ -244,7 +247,6 @@ let make = (
244
247
  latestFetchedBlockNumber: highestSlot,
245
248
  stats: {totalTimeElapsed, parsingTimeElapsed, pageFetchTime},
246
249
  knownHeight,
247
- fromBlockQueried: fromBlock,
248
250
  requestStats,
249
251
  }
250
252
  }