envio 3.0.0-alpha.2 → 3.0.0-alpha.20

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 (175) hide show
  1. package/README.md +164 -30
  2. package/bin.mjs +49 -0
  3. package/evm.schema.json +79 -169
  4. package/fuel.schema.json +50 -21
  5. package/index.d.ts +497 -1
  6. package/index.js +4 -0
  7. package/package.json +42 -31
  8. package/rescript.json +4 -1
  9. package/src/Batch.res +11 -8
  10. package/src/Batch.res.mjs +11 -9
  11. package/src/ChainFetcher.res +531 -0
  12. package/src/ChainFetcher.res.mjs +339 -0
  13. package/src/ChainManager.res +190 -0
  14. package/src/ChainManager.res.mjs +166 -0
  15. package/src/Change.res +3 -3
  16. package/src/Config.gen.ts +19 -0
  17. package/src/Config.res +737 -22
  18. package/src/Config.res.mjs +703 -26
  19. package/src/{Indexer.res → Ctx.res} +1 -1
  20. package/src/Ecosystem.res +9 -124
  21. package/src/Ecosystem.res.mjs +19 -160
  22. package/src/Env.res +30 -74
  23. package/src/Env.res.mjs +25 -87
  24. package/src/Envio.gen.ts +3 -1
  25. package/src/Envio.res +20 -9
  26. package/src/EventProcessing.res +469 -0
  27. package/src/EventProcessing.res.mjs +337 -0
  28. package/src/EvmTypes.gen.ts +6 -0
  29. package/src/EvmTypes.res +1 -0
  30. package/src/FetchState.res +1256 -639
  31. package/src/FetchState.res.mjs +1135 -612
  32. package/src/GlobalState.res +1190 -0
  33. package/src/GlobalState.res.mjs +1183 -0
  34. package/src/GlobalStateManager.res +68 -0
  35. package/src/GlobalStateManager.res.mjs +75 -0
  36. package/src/GlobalStateManager.resi +7 -0
  37. package/src/HandlerLoader.res +89 -0
  38. package/src/HandlerLoader.res.mjs +79 -0
  39. package/src/HandlerRegister.res +357 -0
  40. package/src/HandlerRegister.res.mjs +299 -0
  41. package/src/{EventRegister.resi → HandlerRegister.resi} +13 -13
  42. package/src/Hasura.res +111 -175
  43. package/src/Hasura.res.mjs +88 -150
  44. package/src/InMemoryStore.res +1 -1
  45. package/src/InMemoryStore.res.mjs +3 -3
  46. package/src/InMemoryTable.res +1 -1
  47. package/src/InMemoryTable.res.mjs +1 -1
  48. package/src/Internal.gen.ts +4 -0
  49. package/src/Internal.res +230 -12
  50. package/src/Internal.res.mjs +115 -1
  51. package/src/LoadLayer.res +444 -0
  52. package/src/LoadLayer.res.mjs +296 -0
  53. package/src/LoadLayer.resi +32 -0
  54. package/src/LogSelection.res +33 -27
  55. package/src/LogSelection.res.mjs +6 -0
  56. package/src/Logging.res +21 -7
  57. package/src/Logging.res.mjs +16 -8
  58. package/src/Main.res +377 -0
  59. package/src/Main.res.mjs +339 -0
  60. package/src/Persistence.res +7 -21
  61. package/src/Persistence.res.mjs +3 -3
  62. package/src/PgStorage.gen.ts +10 -0
  63. package/src/PgStorage.res +116 -69
  64. package/src/PgStorage.res.d.mts +5 -0
  65. package/src/PgStorage.res.mjs +93 -50
  66. package/src/Prometheus.res +294 -224
  67. package/src/Prometheus.res.mjs +353 -340
  68. package/src/ReorgDetection.res +6 -10
  69. package/src/ReorgDetection.res.mjs +6 -6
  70. package/src/SafeCheckpointTracking.res +4 -4
  71. package/src/SafeCheckpointTracking.res.mjs +2 -2
  72. package/src/Sink.res +4 -2
  73. package/src/Sink.res.mjs +2 -1
  74. package/src/TableIndices.res +0 -1
  75. package/src/TestIndexer.res +692 -0
  76. package/src/TestIndexer.res.mjs +527 -0
  77. package/src/TestIndexerProxyStorage.res +205 -0
  78. package/src/TestIndexerProxyStorage.res.mjs +151 -0
  79. package/src/TopicFilter.res +1 -1
  80. package/src/Types.ts +1 -1
  81. package/src/UserContext.res +424 -0
  82. package/src/UserContext.res.mjs +279 -0
  83. package/src/Utils.res +97 -26
  84. package/src/Utils.res.mjs +91 -44
  85. package/src/bindings/BigInt.res +10 -0
  86. package/src/bindings/BigInt.res.mjs +15 -0
  87. package/src/bindings/ClickHouse.res +120 -23
  88. package/src/bindings/ClickHouse.res.mjs +118 -28
  89. package/src/bindings/DateFns.res +74 -0
  90. package/src/bindings/DateFns.res.mjs +22 -0
  91. package/src/bindings/EventSource.res +8 -1
  92. package/src/bindings/EventSource.res.mjs +8 -1
  93. package/src/bindings/Express.res +1 -0
  94. package/src/bindings/Hrtime.res +14 -1
  95. package/src/bindings/Hrtime.res.mjs +22 -2
  96. package/src/bindings/Hrtime.resi +4 -0
  97. package/src/bindings/Lodash.res +0 -1
  98. package/src/bindings/NodeJs.res +49 -3
  99. package/src/bindings/NodeJs.res.mjs +11 -3
  100. package/src/bindings/Pino.res +24 -10
  101. package/src/bindings/Pino.res.mjs +14 -8
  102. package/src/bindings/Postgres.gen.ts +8 -0
  103. package/src/bindings/Postgres.res +5 -1
  104. package/src/bindings/Postgres.res.d.mts +5 -0
  105. package/src/bindings/PromClient.res +0 -10
  106. package/src/bindings/PromClient.res.mjs +0 -3
  107. package/src/bindings/Vitest.res +142 -0
  108. package/src/bindings/Vitest.res.mjs +9 -0
  109. package/src/bindings/WebSocket.res +27 -0
  110. package/src/bindings/WebSocket.res.mjs +2 -0
  111. package/src/bindings/Yargs.res +8 -0
  112. package/src/bindings/Yargs.res.mjs +2 -0
  113. package/src/db/EntityHistory.res +7 -7
  114. package/src/db/EntityHistory.res.mjs +9 -9
  115. package/src/db/InternalTable.res +59 -111
  116. package/src/db/InternalTable.res.mjs +73 -104
  117. package/src/db/Table.res +27 -8
  118. package/src/db/Table.res.mjs +25 -14
  119. package/src/sources/Evm.res +84 -0
  120. package/src/sources/Evm.res.mjs +105 -0
  121. package/src/sources/EvmChain.res +94 -0
  122. package/src/sources/EvmChain.res.mjs +60 -0
  123. package/src/sources/Fuel.res +19 -34
  124. package/src/sources/Fuel.res.mjs +34 -16
  125. package/src/sources/FuelSDK.res +38 -0
  126. package/src/sources/FuelSDK.res.mjs +29 -0
  127. package/src/sources/HyperFuel.res +2 -2
  128. package/src/sources/HyperFuel.resi +1 -1
  129. package/src/sources/HyperFuelClient.res +2 -2
  130. package/src/sources/HyperFuelSource.res +33 -13
  131. package/src/sources/HyperFuelSource.res.mjs +24 -16
  132. package/src/sources/HyperSync.res +36 -6
  133. package/src/sources/HyperSync.res.mjs +9 -7
  134. package/src/sources/HyperSync.resi +4 -0
  135. package/src/sources/HyperSyncClient.res +1 -1
  136. package/src/sources/HyperSyncHeightStream.res +47 -116
  137. package/src/sources/HyperSyncHeightStream.res.mjs +46 -73
  138. package/src/sources/HyperSyncSource.res +118 -139
  139. package/src/sources/HyperSyncSource.res.mjs +104 -121
  140. package/src/sources/Rpc.res +86 -14
  141. package/src/sources/Rpc.res.mjs +101 -9
  142. package/src/sources/RpcSource.res +621 -364
  143. package/src/sources/RpcSource.res.mjs +843 -410
  144. package/src/sources/RpcWebSocketHeightStream.res +181 -0
  145. package/src/sources/RpcWebSocketHeightStream.res.mjs +196 -0
  146. package/src/sources/Source.res +7 -5
  147. package/src/sources/SourceManager.res +325 -225
  148. package/src/sources/SourceManager.res.mjs +314 -171
  149. package/src/sources/SourceManager.resi +17 -6
  150. package/src/sources/Svm.res +81 -0
  151. package/src/sources/Svm.res.mjs +90 -0
  152. package/src/tui/Tui.res +247 -0
  153. package/src/tui/Tui.res.mjs +337 -0
  154. package/src/tui/bindings/Ink.res +371 -0
  155. package/src/tui/bindings/Ink.res.mjs +72 -0
  156. package/src/tui/bindings/Style.res +123 -0
  157. package/src/tui/bindings/Style.res.mjs +2 -0
  158. package/src/tui/components/BufferedProgressBar.res +40 -0
  159. package/src/tui/components/BufferedProgressBar.res.mjs +57 -0
  160. package/src/tui/components/CustomHooks.res +122 -0
  161. package/src/tui/components/CustomHooks.res.mjs +179 -0
  162. package/src/tui/components/Messages.res +41 -0
  163. package/src/tui/components/Messages.res.mjs +75 -0
  164. package/src/tui/components/SyncETA.res +174 -0
  165. package/src/tui/components/SyncETA.res.mjs +263 -0
  166. package/src/tui/components/TuiData.res +47 -0
  167. package/src/tui/components/TuiData.res.mjs +34 -0
  168. package/svm.schema.json +112 -0
  169. package/bin.js +0 -48
  170. package/src/EventRegister.res +0 -241
  171. package/src/EventRegister.res.mjs +0 -240
  172. package/src/bindings/Ethers.gen.ts +0 -14
  173. package/src/bindings/Ethers.res +0 -204
  174. package/src/bindings/Ethers.res.mjs +0 -130
  175. /package/src/{Indexer.res.mjs → Ctx.res.mjs} +0 -0
@@ -0,0 +1,1190 @@
1
+ open Belt
2
+
3
+ type chain = ChainMap.Chain.t
4
+ type rollbackState =
5
+ | NoRollback
6
+ | ReorgDetected({chain: chain, blockNumber: int})
7
+ | FindingReorgDepth
8
+ | FoundReorgDepth({chain: chain, rollbackTargetBlockNumber: int})
9
+ | RollbackReady({diffInMemoryStore: InMemoryStore.t, eventsProcessedDiffByChain: dict<float>})
10
+
11
+ module WriteThrottlers = {
12
+ type t = {
13
+ chainMetaData: Throttler.t,
14
+ pruneStaleEntityHistory: Throttler.t,
15
+ }
16
+ let make = (): t => {
17
+ let chainMetaData = {
18
+ let intervalMillis = Env.ThrottleWrites.chainMetadataIntervalMillis
19
+ let logger = Logging.createChild(
20
+ ~params={
21
+ "context": "Throttler for chain metadata writes",
22
+ "intervalMillis": intervalMillis,
23
+ },
24
+ )
25
+ Throttler.make(~intervalMillis, ~logger)
26
+ }
27
+
28
+ let pruneStaleEntityHistory = {
29
+ let intervalMillis = Env.ThrottleWrites.pruneStaleDataIntervalMillis
30
+ let logger = Logging.createChild(
31
+ ~params={
32
+ "context": "Throttler for pruning stale entity history data",
33
+ "intervalMillis": intervalMillis,
34
+ },
35
+ )
36
+ Throttler.make(~intervalMillis, ~logger)
37
+ }
38
+ {chainMetaData, pruneStaleEntityHistory}
39
+ }
40
+ }
41
+
42
+ type t = {
43
+ ctx: Ctx.t,
44
+ chainManager: ChainManager.t,
45
+ processedBatches: int,
46
+ currentlyProcessingBatch: bool,
47
+ rollbackState: rollbackState,
48
+ indexerStartTime: Js.Date.t,
49
+ writeThrottlers: WriteThrottlers.t,
50
+ loadManager: LoadManager.t,
51
+ keepProcessAlive: bool,
52
+ //Initialized as 0, increments, when rollbacks occur to invalidate
53
+ //responses based on the wrong stateId
54
+ id: int,
55
+ }
56
+
57
+ let make = (
58
+ ~ctx: Ctx.t,
59
+ ~chainManager: ChainManager.t,
60
+ ~isDevelopmentMode=false,
61
+ ~shouldUseTui=false,
62
+ ) => {
63
+ {
64
+ ctx,
65
+ currentlyProcessingBatch: false,
66
+ processedBatches: 0,
67
+ chainManager,
68
+ indexerStartTime: Js.Date.make(),
69
+ rollbackState: NoRollback,
70
+ writeThrottlers: WriteThrottlers.make(),
71
+ loadManager: LoadManager.make(),
72
+ keepProcessAlive: isDevelopmentMode || shouldUseTui,
73
+ id: 0,
74
+ }
75
+ }
76
+
77
+ let getId = self => self.id
78
+ let setChainManager = (self, chainManager) => {
79
+ ...self,
80
+ chainManager,
81
+ }
82
+
83
+ let isPreparingRollback = state =>
84
+ switch state.rollbackState {
85
+ | NoRollback
86
+ | // We already updated fetch states here
87
+ // so we treat it as not rolling back
88
+ RollbackReady(_) => false
89
+ | FindingReorgDepth
90
+ | ReorgDetected(_)
91
+ | FoundReorgDepth(_) => true
92
+ }
93
+
94
+ type partitionQueryResponse = {
95
+ chain: chain,
96
+ response: Source.blockRangeFetchResponse,
97
+ query: FetchState.query,
98
+ }
99
+
100
+ type shouldExit = ExitWithSuccess | NoExit
101
+
102
+ // Need to dispatch an action for every async operation
103
+ // to get access to the latest state.
104
+ type action =
105
+ // After a response is received, we validate it with the new state
106
+ // if there's no reorg to continue processing the response.
107
+ | ValidatePartitionQueryResponse(partitionQueryResponse)
108
+ // This should be a separate action from ValidatePartitionQueryResponse
109
+ // because when processing the response, there might be an async contract registration.
110
+ // So after it's finished we dispatch the submit action to get the latest fetch state.
111
+ | SubmitPartitionQueryResponse({
112
+ newItems: array<Internal.item>,
113
+ newItemsWithDcs: array<Internal.item>,
114
+ knownHeight: int,
115
+ latestFetchedBlock: FetchState.blockNumberAndTimestamp,
116
+ query: FetchState.query,
117
+ chain: chain,
118
+ })
119
+ | FinishWaitingForNewBlock({chain: chain, knownHeight: int})
120
+ | EventBatchProcessed({batch: Batch.t})
121
+ | StartProcessingBatch
122
+ | StartFindingReorgDepth
123
+ | FindReorgDepth({chain: chain, rollbackTargetBlockNumber: int})
124
+ | EnterReorgThreshold
125
+ | UpdateQueues({
126
+ progressedChainsById: dict<Batch.chainAfterBatch>,
127
+ // Needed to prevent overwriting the blockLag
128
+ // set by EnterReorgThreshold
129
+ shouldEnterReorgThreshold: bool,
130
+ })
131
+ | SuccessExit
132
+ | ErrorExit(ErrorHandling.t)
133
+ | SetRollbackState({
134
+ diffInMemoryStore: InMemoryStore.t,
135
+ rollbackedChainManager: ChainManager.t,
136
+ eventsProcessedDiffByChain: dict<float>,
137
+ })
138
+
139
+ type queryChain = CheckAllChains | Chain(chain)
140
+ type task =
141
+ | NextQuery(queryChain)
142
+ | ProcessPartitionQueryResponse(partitionQueryResponse)
143
+ | ProcessEventBatch
144
+ | UpdateChainMetaDataAndCheckForExit(shouldExit)
145
+ | Rollback
146
+ | PruneStaleEntityHistory
147
+
148
+ let updateChainMetadataTable = (
149
+ cm: ChainManager.t,
150
+ ~persistence: Persistence.t,
151
+ ~throttler: Throttler.t,
152
+ ) => {
153
+ let chainsData: dict<InternalTable.Chains.metaFields> = Js.Dict.empty()
154
+
155
+ cm.chainFetchers
156
+ ->ChainMap.values
157
+ ->Belt.Array.forEach(cf => {
158
+ chainsData->Js.Dict.set(
159
+ cf.chainConfig.id->Belt.Int.toString,
160
+ {
161
+ firstEventBlockNumber: cf.fetchState.firstEventBlock->Js.Null.fromOption,
162
+ isHyperSync: (cf.sourceManager->SourceManager.getActiveSource).poweredByHyperSync,
163
+ latestFetchedBlockNumber: cf.fetchState->FetchState.bufferBlockNumber,
164
+ timestampCaughtUpToHeadOrEndblock: cf.timestampCaughtUpToHeadOrEndblock->Js.Null.fromOption,
165
+ },
166
+ )
167
+ })
168
+
169
+ //Don't await this set, it can happen in its own time
170
+ throttler->Throttler.schedule(() =>
171
+ persistence.storage.setChainMeta(chainsData)->Promise.ignoreValue
172
+ )
173
+ }
174
+
175
+ /**
176
+ Takes in a chain manager and sets all chains timestamp caught up to head
177
+ when valid state lines up and returns an updated chain manager
178
+ */
179
+ let updateProgressedChains = (chainManager: ChainManager.t, ~batch: Batch.t, ~ctx: Ctx.t) => {
180
+ let nextQueueItemIsNone = chainManager->ChainManager.nextItemIsNone
181
+
182
+ let allChainsAtHead = chainManager->ChainManager.isProgressAtHead
183
+ //Update the timestampCaughtUpToHeadOrEndblock values
184
+ let allChainsReady = ref(true)
185
+ let chainFetchers = chainManager.chainFetchers->ChainMap.map(prev => {
186
+ let cf = prev
187
+ let chain = ChainMap.Chain.makeUnsafe(~chainId=cf.chainConfig.id)
188
+
189
+ let maybeChainAfterBatch =
190
+ batch.progressedChainsById->Utils.Dict.dangerouslyGetByIntNonOption(
191
+ chain->ChainMap.Chain.toChainId,
192
+ )
193
+
194
+ let cf = switch maybeChainAfterBatch {
195
+ | Some(chainAfterBatch) => {
196
+ if cf.committedProgressBlockNumber !== chainAfterBatch.progressBlockNumber {
197
+ Prometheus.ProgressBlockNumber.set(
198
+ ~blockNumber=chainAfterBatch.progressBlockNumber,
199
+ ~chainId=chain->ChainMap.Chain.toChainId,
200
+ )
201
+ }
202
+ if cf.numEventsProcessed !== chainAfterBatch.totalEventsProcessed {
203
+ Prometheus.ProgressEventsCount.set(
204
+ ~processedCount=chainAfterBatch.totalEventsProcessed,
205
+ ~chainId=chain->ChainMap.Chain.toChainId,
206
+ )
207
+ }
208
+
209
+ // Calculate and set latency metrics
210
+ switch batch->Batch.findLastEventItem(~chainId=chain->ChainMap.Chain.toChainId) {
211
+ | Some(eventItem) => {
212
+ let blockTimestamp = eventItem.event.block->ctx.config.ecosystem.getTimestamp
213
+ let currentTimeMs = Js.Date.now()->Float.toInt
214
+ let blockTimestampMs = blockTimestamp * 1000
215
+ let latencyMs = currentTimeMs - blockTimestampMs
216
+
217
+ Prometheus.ProgressLatency.set(~latencyMs, ~chainId=chain->ChainMap.Chain.toChainId)
218
+ }
219
+ | None => ()
220
+ }
221
+
222
+ {
223
+ ...cf,
224
+ // Since we process per chain always in order,
225
+ // we need to calculate it once, by using the first item in a batch
226
+ fetchState: switch cf.fetchState.firstEventBlock {
227
+ | Some(_) => cf.fetchState
228
+ | None =>
229
+ switch batch->Batch.findFirstEventBlockNumber(
230
+ ~chainId=chain->ChainMap.Chain.toChainId,
231
+ ) {
232
+ | Some(_) as firstEventBlock => {...cf.fetchState, firstEventBlock}
233
+ | None => cf.fetchState
234
+ }
235
+ },
236
+ committedProgressBlockNumber: chainAfterBatch.progressBlockNumber,
237
+ numEventsProcessed: chainAfterBatch.totalEventsProcessed,
238
+ isProgressAtHead: cf.isProgressAtHead || chainAfterBatch.isProgressAtHeadWhenBatchCreated,
239
+ safeCheckpointTracking: switch cf.safeCheckpointTracking {
240
+ | Some(safeCheckpointTracking) =>
241
+ Some(
242
+ safeCheckpointTracking->SafeCheckpointTracking.updateOnNewBatch(
243
+ ~sourceBlockNumber=cf.fetchState.knownHeight,
244
+ ~chainId=chain->ChainMap.Chain.toChainId,
245
+ ~batchCheckpointIds=batch.checkpointIds,
246
+ ~batchCheckpointBlockNumbers=batch.checkpointBlockNumbers,
247
+ ~batchCheckpointChainIds=batch.checkpointChainIds,
248
+ ),
249
+ )
250
+ | None => None
251
+ },
252
+ }
253
+ }
254
+ | None => cf
255
+ }
256
+
257
+ /* strategy for TUI synced status:
258
+ * Firstly -> only update synced status after batch is processed (not on batch creation). But also set when a batch tries to be created and there is no batch
259
+ *
260
+ * Secondly -> reset timestampCaughtUpToHead and isFetching at head when dynamic contracts get registered to a chain if they are not within 0.001 percent of the current block height
261
+ *
262
+ * New conditions for valid synced:
263
+ *
264
+ * CASE 1 (chains are being synchronised at the head)
265
+ *
266
+ * All chain fetchers are fetching at the head AND
267
+ * No events that can be processed on the queue (even if events still exist on the individual queues)
268
+ * CASE 2 (chain finishes earlier than any other chain)
269
+ *
270
+ * CASE 3 endblock has been reached and latest processed block is greater than or equal to endblock (both fields must be Some)
271
+ *
272
+ * The given chain fetcher is fetching at the head or latest processed block >= endblock
273
+ * The given chain has processed all events on the queue
274
+ * see https://github.com/Float-Capital/indexer/pull/1388 */
275
+ let cf = if cf->ChainFetcher.hasProcessedToEndblock {
276
+ // in the case this is already set, don't reset and instead propagate the existing value
277
+ let timestampCaughtUpToHeadOrEndblock =
278
+ cf->ChainFetcher.isReady ? cf.timestampCaughtUpToHeadOrEndblock : Js.Date.make()->Some
279
+ {
280
+ ...cf,
281
+ timestampCaughtUpToHeadOrEndblock,
282
+ }
283
+ } else if !(cf->ChainFetcher.isReady) && cf.isProgressAtHead {
284
+ //Only calculate and set timestampCaughtUpToHeadOrEndblock if chain fetcher is at the head and
285
+ //its not already set
286
+ //CASE1
287
+ //All chains are caught up to head chainManager queue returns None
288
+ //Meaning we are busy synchronizing chains at the head
289
+ if nextQueueItemIsNone && allChainsAtHead {
290
+ {
291
+ ...cf,
292
+ timestampCaughtUpToHeadOrEndblock: Js.Date.make()->Some,
293
+ }
294
+ } else {
295
+ //CASE2 -> Only calculate if case1 fails
296
+ //All events have been processed on the chain fetchers queue
297
+ //Other chains may be busy syncing
298
+ let hasNoMoreEventsToProcess = cf->ChainFetcher.hasNoMoreEventsToProcess
299
+
300
+ if hasNoMoreEventsToProcess {
301
+ {
302
+ ...cf,
303
+ timestampCaughtUpToHeadOrEndblock: Js.Date.make()->Some,
304
+ }
305
+ } else {
306
+ //Default to just returning cf
307
+ cf
308
+ }
309
+ }
310
+ } else {
311
+ //Default to just returning cf
312
+ cf
313
+ }
314
+
315
+ // Set envio_progress_ready per-chain when it first becomes ready
316
+ if cf->ChainFetcher.isReady {
317
+ if !(prev->ChainFetcher.isReady) {
318
+ Prometheus.ProgressReady.set(~chainId=chain->ChainMap.Chain.toChainId)
319
+ }
320
+ } else {
321
+ allChainsReady := false
322
+ }
323
+
324
+ cf
325
+ })
326
+
327
+ if allChainsReady.contents {
328
+ Prometheus.ProgressReady.setAllReady()
329
+ }
330
+
331
+ {
332
+ ...chainManager,
333
+ committedCheckpointId: switch batch.checkpointIds->Utils.Array.last {
334
+ | Some(checkpointId) => checkpointId
335
+ | None => chainManager.committedCheckpointId
336
+ },
337
+ chainFetchers,
338
+ }
339
+ }
340
+
341
+ let validatePartitionQueryResponse = (
342
+ state,
343
+ {chain, response} as partitionQueryResponse: partitionQueryResponse,
344
+ ) => {
345
+ let chainFetcher = state.chainManager.chainFetchers->ChainMap.get(chain)
346
+ let {
347
+ parsedQueueItems,
348
+ latestFetchedBlockNumber,
349
+ stats,
350
+ knownHeight,
351
+ reorgGuard,
352
+ fromBlockQueried,
353
+ } = response
354
+
355
+ if knownHeight > chainFetcher.fetchState.knownHeight {
356
+ Prometheus.SourceHeight.set(
357
+ ~blockNumber=knownHeight,
358
+ ~chainId=chainFetcher.chainConfig.id,
359
+ // The knownHeight from response won't necessarily
360
+ // belong to the currently active source.
361
+ // But for simplicity, assume it does.
362
+ ~sourceName=(chainFetcher.sourceManager->SourceManager.getActiveSource).name,
363
+ )
364
+ }
365
+
366
+ Prometheus.FetchingBlockRange.increment(
367
+ ~chainId=chain->ChainMap.Chain.toChainId,
368
+ ~totalTimeElapsed=stats.totalTimeElapsed,
369
+ ~parsingTimeElapsed=stats.parsingTimeElapsed->Belt.Option.getWithDefault(0.),
370
+ ~numEvents=parsedQueueItems->Array.length,
371
+ ~blockRangeSize=latestFetchedBlockNumber - fromBlockQueried + 1,
372
+ )
373
+
374
+ let (updatedReorgDetection, reorgResult: ReorgDetection.reorgResult) =
375
+ chainFetcher.reorgDetection->ReorgDetection.registerReorgGuard(~reorgGuard, ~knownHeight)
376
+
377
+ let updatedChainFetcher = {
378
+ ...chainFetcher,
379
+ reorgDetection: updatedReorgDetection,
380
+ }
381
+
382
+ let nextState = {
383
+ ...state,
384
+ chainManager: {
385
+ ...state.chainManager,
386
+ chainFetchers: state.chainManager.chainFetchers->ChainMap.set(chain, updatedChainFetcher),
387
+ },
388
+ }
389
+
390
+ let rollbackWithReorgDetectedBlockNumber = switch reorgResult {
391
+ | ReorgDetected(reorgDetected) => {
392
+ chainFetcher.logger->Logging.childInfo(
393
+ reorgDetected->ReorgDetection.reorgDetectedToLogParams(
394
+ ~shouldRollbackOnReorg=state.ctx.config.shouldRollbackOnReorg,
395
+ ),
396
+ )
397
+ Prometheus.ReorgCount.increment(~chain)
398
+ Prometheus.ReorgDetectionBlockNumber.set(
399
+ ~blockNumber=reorgDetected.scannedBlock.blockNumber,
400
+ ~chain,
401
+ )
402
+ if state.ctx.config.shouldRollbackOnReorg {
403
+ Some(reorgDetected.scannedBlock.blockNumber)
404
+ } else {
405
+ None
406
+ }
407
+ }
408
+ | NoReorg => None
409
+ }
410
+
411
+ switch rollbackWithReorgDetectedBlockNumber {
412
+ | None => (nextState, [ProcessPartitionQueryResponse(partitionQueryResponse)])
413
+ | Some(reorgDetectedBlockNumber) => {
414
+ let chainManager = switch state.rollbackState {
415
+ | RollbackReady({eventsProcessedDiffByChain}) => {
416
+ ...state.chainManager,
417
+ // Restore event counters for ALL chains, not just the reorg chain.
418
+ // The previous rollback subtracted from all chains' counters,
419
+ // but was never committed to DB. So we must undo the subtraction
420
+ // for every chain before the new rollback subtracts again.
421
+ chainFetchers: state.chainManager.chainFetchers->ChainMap.mapWithKey((
422
+ c,
423
+ chainFetcher,
424
+ ) => {
425
+ switch eventsProcessedDiffByChain->Utils.Dict.dangerouslyGetByIntNonOption(
426
+ c->ChainMap.Chain.toChainId,
427
+ ) {
428
+ | Some(eventsProcessedDiff) => {
429
+ ...chainFetcher,
430
+ // Since we detected a reorg, until rollback wasn't completed in the db
431
+ // We return the events processed counter to the pre-rollback value,
432
+ // to decrease it once more for the new rollback.
433
+ numEventsProcessed: chainFetcher.numEventsProcessed +. eventsProcessedDiff,
434
+ }
435
+ | None => chainFetcher
436
+ }
437
+ }),
438
+ }
439
+ | _ => state.chainManager
440
+ }
441
+ (
442
+ {
443
+ ...nextState,
444
+ id: nextState.id + 1,
445
+ chainManager: {
446
+ ...chainManager,
447
+ chainFetchers: chainManager.chainFetchers->ChainMap.map(chainFetcher => {
448
+ ...chainFetcher,
449
+ // TODO: It's not optimal to abort pending queries for all chains,
450
+ // this is how it always worked, but we should consider a better approach.
451
+ fetchState: chainFetcher.fetchState->FetchState.resetPendingQueries,
452
+ }),
453
+ },
454
+ rollbackState: ReorgDetected({
455
+ chain,
456
+ blockNumber: reorgDetectedBlockNumber,
457
+ }),
458
+ },
459
+ [Rollback],
460
+ )
461
+ }
462
+ }
463
+ }
464
+
465
+ let submitPartitionQueryResponse = (
466
+ state,
467
+ ~newItems,
468
+ ~newItemsWithDcs,
469
+ ~knownHeight,
470
+ ~latestFetchedBlock,
471
+ ~query,
472
+ ~chain,
473
+ ) => {
474
+ let chainFetcher = state.chainManager.chainFetchers->ChainMap.get(chain)
475
+
476
+ let updatedChainFetcher =
477
+ chainFetcher->ChainFetcher.handleQueryResult(
478
+ ~query,
479
+ ~latestFetchedBlock,
480
+ ~newItems,
481
+ ~newItemsWithDcs,
482
+ ~knownHeight,
483
+ )
484
+
485
+ if !chainFetcher.isProgressAtHead && updatedChainFetcher.isProgressAtHead {
486
+ updatedChainFetcher.logger->Logging.childInfo("All events have been fetched")
487
+ }
488
+
489
+ let nextState = {
490
+ ...state,
491
+ chainManager: {
492
+ ...state.chainManager,
493
+ chainFetchers: state.chainManager.chainFetchers->ChainMap.set(chain, updatedChainFetcher),
494
+ },
495
+ }
496
+
497
+ (
498
+ nextState,
499
+ [UpdateChainMetaDataAndCheckForExit(NoExit), ProcessEventBatch, NextQuery(Chain(chain))],
500
+ )
501
+ }
502
+
503
+ let processPartitionQueryResponse = async (
504
+ state,
505
+ {chain, response, query}: partitionQueryResponse,
506
+ ~dispatchAction,
507
+ ) => {
508
+ let {
509
+ parsedQueueItems,
510
+ latestFetchedBlockNumber,
511
+ knownHeight,
512
+ latestFetchedBlockTimestamp,
513
+ } = response
514
+
515
+ let itemsWithContractRegister = []
516
+ let newItems = []
517
+
518
+ for idx in 0 to parsedQueueItems->Array.length - 1 {
519
+ let item = parsedQueueItems->Array.getUnsafe(idx)
520
+ let eventItem = item->Internal.castUnsafeEventItem
521
+ if eventItem.eventConfig.contractRegister !== None {
522
+ itemsWithContractRegister->Array.push(item)
523
+ }
524
+
525
+ // TODO: Don't really need to keep it in the queue
526
+ // when there's no handler (besides raw_events, processed counter, and dcsToStore consuming)
527
+ newItems->Array.push(item)
528
+ }
529
+
530
+ let newItemsWithDcs = switch itemsWithContractRegister {
531
+ | [] as empty => empty
532
+ | _ =>
533
+ await ChainFetcher.runContractRegistersOrThrow(
534
+ ~itemsWithContractRegister,
535
+ ~chain,
536
+ ~config=state.ctx.config,
537
+ )
538
+ }
539
+
540
+ dispatchAction(
541
+ SubmitPartitionQueryResponse({
542
+ newItems,
543
+ newItemsWithDcs,
544
+ knownHeight,
545
+ latestFetchedBlock: {
546
+ blockNumber: latestFetchedBlockNumber,
547
+ blockTimestamp: latestFetchedBlockTimestamp,
548
+ },
549
+ chain,
550
+ query,
551
+ }),
552
+ )
553
+ }
554
+
555
+ let updateChainFetcher = (chainFetcherUpdate, ~state, ~chain) => {
556
+ (
557
+ {
558
+ ...state,
559
+ chainManager: {
560
+ ...state.chainManager,
561
+ chainFetchers: state.chainManager.chainFetchers->ChainMap.update(chain, chainFetcherUpdate),
562
+ },
563
+ },
564
+ [],
565
+ )
566
+ }
567
+
568
+ let onEnterReorgThreshold = (~state: t) => {
569
+ Logging.info("Reorg threshold reached")
570
+ Prometheus.ReorgThreshold.set(~isInReorgThreshold=true)
571
+
572
+ let chainFetchers = state.chainManager.chainFetchers->ChainMap.map(chainFetcher => {
573
+ {
574
+ ...chainFetcher,
575
+ fetchState: chainFetcher.fetchState->FetchState.updateInternal(
576
+ ~blockLag=chainFetcher.chainConfig.blockLag,
577
+ ),
578
+ }
579
+ })
580
+
581
+ {
582
+ ...state,
583
+ chainManager: {
584
+ ...state.chainManager,
585
+ chainFetchers,
586
+ isInReorgThreshold: true,
587
+ },
588
+ }
589
+ }
590
+
591
+ let actionReducer = (state: t, action: action) => {
592
+ switch action {
593
+ | FinishWaitingForNewBlock({chain, knownHeight}) => {
594
+ let updatedChainFetchers = state.chainManager.chainFetchers->ChainMap.update(
595
+ chain,
596
+ chainFetcher => {
597
+ let updatedFetchState =
598
+ chainFetcher.fetchState->FetchState.updateKnownHeight(~knownHeight)
599
+ if updatedFetchState !== chainFetcher.fetchState {
600
+ {
601
+ ...chainFetcher,
602
+ fetchState: updatedFetchState,
603
+ }
604
+ } else {
605
+ chainFetcher
606
+ }
607
+ },
608
+ )
609
+
610
+ let isBelowReorgThreshold =
611
+ !state.chainManager.isInReorgThreshold && state.ctx.config.shouldRollbackOnReorg
612
+ let shouldEnterReorgThreshold =
613
+ isBelowReorgThreshold &&
614
+ updatedChainFetchers
615
+ ->ChainMap.values
616
+ ->Array.every(chainFetcher => {
617
+ chainFetcher.fetchState->FetchState.isReadyToEnterReorgThreshold
618
+ })
619
+
620
+ let state = {
621
+ ...state,
622
+ chainManager: {
623
+ ...state.chainManager,
624
+ chainFetchers: updatedChainFetchers,
625
+ },
626
+ }
627
+
628
+ // Attempt ProcessEventBatch in case if we have block handlers to run
629
+ if shouldEnterReorgThreshold {
630
+ (onEnterReorgThreshold(~state), [NextQuery(CheckAllChains), ProcessEventBatch])
631
+ } else {
632
+ (state, [NextQuery(Chain(chain)), ProcessEventBatch])
633
+ }
634
+ }
635
+ | ValidatePartitionQueryResponse(partitionQueryResponse) =>
636
+ state->validatePartitionQueryResponse(partitionQueryResponse)
637
+ | SubmitPartitionQueryResponse({
638
+ newItems,
639
+ newItemsWithDcs,
640
+ knownHeight,
641
+ latestFetchedBlock,
642
+ query,
643
+ chain,
644
+ }) =>
645
+ state->submitPartitionQueryResponse(
646
+ ~newItems,
647
+ ~newItemsWithDcs,
648
+ ~knownHeight,
649
+ ~latestFetchedBlock,
650
+ ~query,
651
+ ~chain,
652
+ )
653
+ | EventBatchProcessed({batch}) =>
654
+ let maybePruneEntityHistory =
655
+ state.ctx.config->Config.shouldPruneHistory(
656
+ ~isInReorgThreshold=state.chainManager.isInReorgThreshold,
657
+ )
658
+ ? [PruneStaleEntityHistory]
659
+ : []
660
+
661
+ let state = {
662
+ ...state,
663
+ // Can safely reset rollback state, since overwrite is not possible.
664
+ // If rollback is pending, the EventBatchProcessed will be handled by the invalid action reducer instead.
665
+ rollbackState: NoRollback,
666
+ chainManager: state.chainManager->updateProgressedChains(~batch, ~ctx=state.ctx),
667
+ currentlyProcessingBatch: false,
668
+ processedBatches: state.processedBatches + 1,
669
+ }
670
+
671
+ let shouldExit = EventProcessing.allChainsEventsProcessedToEndblock(
672
+ state.chainManager.chainFetchers,
673
+ )
674
+ ? {
675
+ Logging.info("All chains are caught up to end blocks.")
676
+
677
+ // Keep the indexer process running when in development mode (for Dev Console)
678
+ // or when TUI is enabled (for display)
679
+ if state.keepProcessAlive {
680
+ NoExit
681
+ } else {
682
+ ExitWithSuccess
683
+ }
684
+ }
685
+ : NoExit
686
+
687
+ (
688
+ state,
689
+ [UpdateChainMetaDataAndCheckForExit(shouldExit), ProcessEventBatch]->Array.concat(
690
+ maybePruneEntityHistory,
691
+ ),
692
+ )
693
+
694
+ | StartProcessingBatch => ({...state, currentlyProcessingBatch: true}, [])
695
+ | StartFindingReorgDepth => ({...state, rollbackState: FindingReorgDepth}, [])
696
+ | FindReorgDepth({chain, rollbackTargetBlockNumber}) => (
697
+ {
698
+ ...state,
699
+ rollbackState: FoundReorgDepth({
700
+ chain,
701
+ rollbackTargetBlockNumber,
702
+ }),
703
+ },
704
+ [Rollback],
705
+ )
706
+ | EnterReorgThreshold => (onEnterReorgThreshold(~state), [NextQuery(CheckAllChains)])
707
+ | UpdateQueues({progressedChainsById, shouldEnterReorgThreshold}) =>
708
+ let chainFetchers = state.chainManager.chainFetchers->ChainMap.mapWithKey((chain, cf) => {
709
+ let fs = switch progressedChainsById->Utils.Dict.dangerouslyGetByIntNonOption(
710
+ chain->ChainMap.Chain.toChainId,
711
+ ) {
712
+ | Some(chainAfterBatch) => chainAfterBatch.fetchState
713
+ | None => cf.fetchState
714
+ }
715
+ {
716
+ ...cf,
717
+ fetchState: shouldEnterReorgThreshold
718
+ ? fs->FetchState.updateInternal(~blockLag=cf.chainConfig.blockLag)
719
+ : fs,
720
+ }
721
+ })
722
+
723
+ let chainManager = {
724
+ ...state.chainManager,
725
+ chainFetchers,
726
+ }
727
+
728
+ (
729
+ {
730
+ ...state,
731
+ chainManager,
732
+ },
733
+ [NextQuery(CheckAllChains)],
734
+ )
735
+ | SetRollbackState({diffInMemoryStore, rollbackedChainManager, eventsProcessedDiffByChain}) => (
736
+ {
737
+ ...state,
738
+ rollbackState: RollbackReady({
739
+ diffInMemoryStore,
740
+ eventsProcessedDiffByChain,
741
+ }),
742
+ chainManager: rollbackedChainManager,
743
+ },
744
+ [NextQuery(CheckAllChains), ProcessEventBatch],
745
+ )
746
+ | SuccessExit => {
747
+ Logging.info("Exiting with success")
748
+ NodeJs.process->NodeJs.exitWithCode(Success)
749
+ (state, [])
750
+ }
751
+ | ErrorExit(errHandler) =>
752
+ errHandler->ErrorHandling.log
753
+ NodeJs.process->NodeJs.exitWithCode(Failure)
754
+ (state, [])
755
+ }
756
+ }
757
+
758
+ let invalidatedActionReducer = (state: t, action: action) =>
759
+ switch action {
760
+ | EventBatchProcessed({batch}) if state->isPreparingRollback =>
761
+ Logging.info("Finished processing batch before rollback, actioning rollback")
762
+ (
763
+ {
764
+ ...state,
765
+ chainManager: state.chainManager->updateProgressedChains(~batch, ~ctx=state.ctx),
766
+ currentlyProcessingBatch: false,
767
+ processedBatches: state.processedBatches + 1,
768
+ },
769
+ [Rollback],
770
+ )
771
+ | ErrorExit(_) => actionReducer(state, action)
772
+ | _ =>
773
+ Logging.trace({
774
+ "msg": "Invalidated action discarded",
775
+ "action": action->S.convertOrThrow(Utils.Schema.variantTag),
776
+ })
777
+ (state, [])
778
+ }
779
+
780
+ let checkAndFetchForChain = (
781
+ //Used for dependency injection for tests
782
+ ~waitForNewBlock,
783
+ ~executeQuery,
784
+ //required args
785
+ ~state,
786
+ ~dispatchAction,
787
+ ) => async chain => {
788
+ let chainFetcher = state.chainManager.chainFetchers->ChainMap.get(chain)
789
+ if !isPreparingRollback(state) {
790
+ let {fetchState} = chainFetcher
791
+
792
+ await chainFetcher.sourceManager->SourceManager.fetchNext(
793
+ ~fetchState,
794
+ ~waitForNewBlock=(~knownHeight) =>
795
+ chainFetcher.sourceManager->waitForNewBlock(
796
+ ~knownHeight,
797
+ ~isLive=chainFetcher->ChainFetcher.isReady,
798
+ ),
799
+ ~onNewBlock=(~knownHeight) => dispatchAction(FinishWaitingForNewBlock({chain, knownHeight})),
800
+ ~executeQuery=async query => {
801
+ try {
802
+ let response =
803
+ await chainFetcher.sourceManager->executeQuery(
804
+ ~query,
805
+ ~knownHeight=fetchState.knownHeight,
806
+ ~isLive=chainFetcher->ChainFetcher.isReady,
807
+ )
808
+ dispatchAction(ValidatePartitionQueryResponse({chain, response, query}))
809
+ } catch {
810
+ | exn => dispatchAction(ErrorExit(exn->ErrorHandling.make))
811
+ }
812
+ },
813
+ ~stateId=state.id,
814
+ )
815
+ }
816
+ }
817
+
818
+ let injectedTaskReducer = (
819
+ //Used for dependency injection for tests
820
+ ~waitForNewBlock,
821
+ ~executeQuery,
822
+ ~getLastKnownValidBlock,
823
+ ) => async (
824
+ //required args
825
+ state: t,
826
+ task: task,
827
+ ~dispatchAction,
828
+ ) => {
829
+ switch task {
830
+ | ProcessPartitionQueryResponse(partitionQueryResponse) =>
831
+ state->processPartitionQueryResponse(partitionQueryResponse, ~dispatchAction)->Promise.done
832
+ | PruneStaleEntityHistory =>
833
+ let runPrune = async () => {
834
+ switch state.chainManager->ChainManager.getSafeCheckpointId {
835
+ | None => ()
836
+ | Some(safeCheckpointId) =>
837
+ await state.ctx.persistence.storage.pruneStaleCheckpoints(~safeCheckpointId)
838
+
839
+ for idx in 0 to state.ctx.persistence.allEntities->Array.length - 1 {
840
+ if idx !== 0 {
841
+ // Add some delay between entities
842
+ // To unblock the pg client if it's needed for something else
843
+ await Utils.delay(1000)
844
+ }
845
+ let entityConfig = state.ctx.persistence.allEntities->Array.getUnsafe(idx)
846
+ let timeRef = Hrtime.makeTimer()
847
+ try {
848
+ let () = await state.ctx.persistence.storage.pruneStaleEntityHistory(
849
+ ~entityName=entityConfig.name,
850
+ ~entityIndex=entityConfig.index,
851
+ ~safeCheckpointId,
852
+ )
853
+ } catch {
854
+ | exn =>
855
+ exn->ErrorHandling.mkLogAndRaise(
856
+ ~msg=`Failed to prune stale entity history`,
857
+ ~logger=Logging.createChild(
858
+ ~params={
859
+ "entityName": entityConfig.name,
860
+ "safeCheckpointId": safeCheckpointId,
861
+ },
862
+ ),
863
+ )
864
+ }
865
+ Prometheus.RollbackHistoryPrune.increment(
866
+ ~timeSeconds=Hrtime.timeSince(timeRef)->Hrtime.toSecondsFloat,
867
+ ~entityName=entityConfig.name,
868
+ )
869
+ }
870
+ }
871
+ }
872
+ state.writeThrottlers.pruneStaleEntityHistory->Throttler.schedule(runPrune)
873
+
874
+ | UpdateChainMetaDataAndCheckForExit(shouldExit) =>
875
+ let {chainManager, writeThrottlers} = state
876
+ switch shouldExit {
877
+ | ExitWithSuccess =>
878
+ updateChainMetadataTable(
879
+ chainManager,
880
+ ~throttler=writeThrottlers.chainMetaData,
881
+ ~persistence=state.ctx.persistence,
882
+ )
883
+ dispatchAction(SuccessExit)
884
+ | NoExit =>
885
+ updateChainMetadataTable(
886
+ chainManager,
887
+ ~throttler=writeThrottlers.chainMetaData,
888
+ ~persistence=state.ctx.persistence,
889
+ )->ignore
890
+ }
891
+ | NextQuery(chainCheck) =>
892
+ let fetchForChain = checkAndFetchForChain(
893
+ ~waitForNewBlock,
894
+ ~executeQuery,
895
+ ~state,
896
+ ~dispatchAction,
897
+ )
898
+
899
+ switch chainCheck {
900
+ | Chain(chain) => await chain->fetchForChain
901
+ | CheckAllChains =>
902
+ //Mapping from the states chainManager so we can construct tests that don't use
903
+ //all chains
904
+ let _ =
905
+ await state.chainManager.chainFetchers
906
+ ->ChainMap.keys
907
+ ->Array.map(fetchForChain(_))
908
+ ->Promise.all
909
+ }
910
+ | ProcessEventBatch =>
911
+ if !state.currentlyProcessingBatch && !isPreparingRollback(state) {
912
+ //In the case of a rollback, use the provided in memory store
913
+ //With rolled back values
914
+ let rollbackInMemStore = switch state.rollbackState {
915
+ | RollbackReady({diffInMemoryStore}) => Some(diffInMemoryStore)
916
+ | _ => None
917
+ }
918
+
919
+ let batch =
920
+ state.chainManager->ChainManager.createBatch(
921
+ ~batchSizeTarget=state.ctx.config.batchSize,
922
+ ~isRollback=rollbackInMemStore !== None,
923
+ )
924
+
925
+ let progressedChainsById = batch.progressedChainsById
926
+
927
+ let isInReorgThreshold = state.chainManager.isInReorgThreshold
928
+ let shouldSaveHistory = state.ctx.config->Config.shouldSaveHistory(~isInReorgThreshold)
929
+
930
+ let isBelowReorgThreshold =
931
+ !state.chainManager.isInReorgThreshold && state.ctx.config.shouldRollbackOnReorg
932
+ let shouldEnterReorgThreshold =
933
+ isBelowReorgThreshold &&
934
+ state.chainManager.chainFetchers
935
+ ->ChainMap.values
936
+ ->Array.every(chainFetcher => {
937
+ let fetchState = switch progressedChainsById->Utils.Dict.dangerouslyGetByIntNonOption(
938
+ chainFetcher.fetchState.chainId,
939
+ ) {
940
+ | Some(chainAfterBatch) => chainAfterBatch.fetchState
941
+ | None => chainFetcher.fetchState
942
+ }
943
+ fetchState->FetchState.isReadyToEnterReorgThreshold
944
+ })
945
+
946
+ if shouldEnterReorgThreshold {
947
+ dispatchAction(EnterReorgThreshold)
948
+ }
949
+
950
+ if progressedChainsById->Utils.Dict.isEmpty {
951
+ // When resuming from persisted state, all events may already be processed.
952
+ // Log the same completion message and handle exit just like EventBatchProcessed does.
953
+ if EventProcessing.allChainsEventsProcessedToEndblock(state.chainManager.chainFetchers) {
954
+ Logging.info("All chains are caught up to end blocks.")
955
+ if !state.keepProcessAlive {
956
+ updateChainMetadataTable(
957
+ state.chainManager,
958
+ ~persistence=state.ctx.persistence,
959
+ ~throttler=state.writeThrottlers.chainMetaData,
960
+ )
961
+ dispatchAction(SuccessExit)
962
+ }
963
+ }
964
+ } else {
965
+ dispatchAction(StartProcessingBatch)
966
+ dispatchAction(UpdateQueues({progressedChainsById, shouldEnterReorgThreshold}))
967
+
968
+ let inMemoryStore =
969
+ rollbackInMemStore->Option.getWithDefault(
970
+ InMemoryStore.make(~entities=state.ctx.persistence.allEntities),
971
+ )
972
+
973
+ inMemoryStore->InMemoryStore.setBatchDcs(~batch, ~shouldSaveHistory)
974
+
975
+ switch await EventProcessing.processEventBatch(
976
+ ~batch,
977
+ ~inMemoryStore,
978
+ ~isInReorgThreshold,
979
+ ~loadManager=state.loadManager,
980
+ ~ctx=state.ctx,
981
+ ~chainFetchers=state.chainManager.chainFetchers,
982
+ ) {
983
+ | exception exn =>
984
+ //All casese should be handled/caught before this with better user messaging.
985
+ //This is just a safety in case something unexpected happens
986
+ let errHandler =
987
+ exn->ErrorHandling.make(~msg="A top level unexpected error occurred during processing")
988
+ dispatchAction(ErrorExit(errHandler))
989
+ | res =>
990
+ switch res {
991
+ | Ok() => dispatchAction(EventBatchProcessed({batch: batch}))
992
+ | Error(errHandler) => dispatchAction(ErrorExit(errHandler))
993
+ }
994
+ }
995
+ }
996
+ }
997
+ | Rollback =>
998
+ //If it isn't processing a batch currently continue with rollback otherwise wait for current batch to finish processing
999
+ switch state {
1000
+ | {rollbackState: NoRollback | RollbackReady(_)} =>
1001
+ Js.Exn.raiseError("Internal error: Rollback initiated with invalid state")
1002
+ | {rollbackState: ReorgDetected({chain, blockNumber: reorgBlockNumber})} => {
1003
+ let chainFetcher = state.chainManager.chainFetchers->ChainMap.get(chain)
1004
+
1005
+ dispatchAction(StartFindingReorgDepth)
1006
+ let rollbackTargetBlockNumber =
1007
+ await chainFetcher->getLastKnownValidBlock(~reorgBlockNumber)
1008
+
1009
+ dispatchAction(FindReorgDepth({chain, rollbackTargetBlockNumber}))
1010
+ }
1011
+ // We can come to this case when event batch finished processing
1012
+ // while we are still finding the reorg depth
1013
+ // Do nothing here, just wait for reorg depth to be found
1014
+ | {rollbackState: FindingReorgDepth} => ()
1015
+ | {rollbackState: FoundReorgDepth(_), currentlyProcessingBatch: true} =>
1016
+ Logging.info("Waiting for batch to finish processing before executing rollback")
1017
+ | {rollbackState: FoundReorgDepth({chain: reorgChain, rollbackTargetBlockNumber})} =>
1018
+ let startTime = Hrtime.makeTimer()
1019
+
1020
+ let chainFetcher = state.chainManager.chainFetchers->ChainMap.get(reorgChain)
1021
+
1022
+ let logger = Logging.createChildFrom(
1023
+ ~logger=chainFetcher.logger,
1024
+ ~params={
1025
+ "action": "Rollback",
1026
+ "reorgChain": reorgChain,
1027
+ "targetBlockNumber": rollbackTargetBlockNumber,
1028
+ },
1029
+ )
1030
+ logger->Logging.childInfo("Started rollback on reorg")
1031
+ Prometheus.RollbackTargetBlockNumber.set(
1032
+ ~blockNumber=rollbackTargetBlockNumber,
1033
+ ~chain=reorgChain,
1034
+ )
1035
+
1036
+ let reorgChainId = reorgChain->ChainMap.Chain.toChainId
1037
+
1038
+ let rollbackTargetCheckpointId = {
1039
+ switch await state.ctx.persistence.storage.getRollbackTargetCheckpoint(
1040
+ ~reorgChainId,
1041
+ ~lastKnownValidBlockNumber=rollbackTargetBlockNumber,
1042
+ ) {
1043
+ | Some(checkpointId) => checkpointId
1044
+ | None => 0n
1045
+ }
1046
+ }
1047
+
1048
+ let eventsProcessedDiffByChain = Js.Dict.empty()
1049
+ let newProgressBlockNumberPerChain = Js.Dict.empty()
1050
+ let rollbackedProcessedEvents = ref(0.)
1051
+
1052
+ {
1053
+ let rollbackProgressDiff = await state.ctx.persistence.storage.getRollbackProgressDiff(
1054
+ ~rollbackTargetCheckpointId,
1055
+ )
1056
+ for idx in 0 to rollbackProgressDiff->Js.Array2.length - 1 {
1057
+ let diff = rollbackProgressDiff->Js.Array2.unsafe_get(idx)
1058
+ eventsProcessedDiffByChain->Utils.Dict.setByInt(
1059
+ diff["chain_id"],
1060
+ {
1061
+ let eventsProcessedDiff = Float.fromString(
1062
+ diff["events_processed_diff"],
1063
+ )->Option.getExn
1064
+ rollbackedProcessedEvents :=
1065
+ rollbackedProcessedEvents.contents +. eventsProcessedDiff
1066
+ eventsProcessedDiff
1067
+ },
1068
+ )
1069
+ newProgressBlockNumberPerChain->Utils.Dict.setByInt(
1070
+ diff["chain_id"],
1071
+ if rollbackTargetCheckpointId === 0n && diff["chain_id"] === reorgChainId {
1072
+ Pervasives.min(diff["new_progress_block_number"], rollbackTargetBlockNumber)
1073
+ } else {
1074
+ diff["new_progress_block_number"]
1075
+ },
1076
+ )
1077
+ }
1078
+ }
1079
+
1080
+ let chainFetchers = state.chainManager.chainFetchers->ChainMap.mapWithKey((chain, cf) => {
1081
+ switch newProgressBlockNumberPerChain->Utils.Dict.dangerouslyGetByIntNonOption(
1082
+ chain->ChainMap.Chain.toChainId,
1083
+ ) {
1084
+ | Some(newProgressBlockNumber) =>
1085
+ let fetchState =
1086
+ cf.fetchState->FetchState.rollback(~targetBlockNumber=newProgressBlockNumber)
1087
+ let newTotalEventsProcessed =
1088
+ cf.numEventsProcessed -. (eventsProcessedDiffByChain
1089
+ ->Utils.Dict.dangerouslyGetByIntNonOption(chain->ChainMap.Chain.toChainId)
1090
+ ->Option.getUnsafe)
1091
+
1092
+ if cf.committedProgressBlockNumber !== newProgressBlockNumber {
1093
+ Prometheus.ProgressBlockNumber.set(
1094
+ ~blockNumber=newProgressBlockNumber,
1095
+ ~chainId=chain->ChainMap.Chain.toChainId,
1096
+ )
1097
+ }
1098
+ if cf.numEventsProcessed !== newTotalEventsProcessed {
1099
+ Prometheus.ProgressEventsCount.set(
1100
+ ~processedCount=newTotalEventsProcessed,
1101
+ ~chainId=chain->ChainMap.Chain.toChainId,
1102
+ )
1103
+ }
1104
+
1105
+ {
1106
+ ...cf,
1107
+ reorgDetection: chain == reorgChain
1108
+ ? cf.reorgDetection->ReorgDetection.rollbackToValidBlockNumber(
1109
+ ~blockNumber=rollbackTargetBlockNumber,
1110
+ )
1111
+ : cf.reorgDetection,
1112
+ safeCheckpointTracking: switch cf.safeCheckpointTracking {
1113
+ | Some(safeCheckpointTracking) =>
1114
+ Some(
1115
+ safeCheckpointTracking->SafeCheckpointTracking.rollback(
1116
+ ~targetBlockNumber=newProgressBlockNumber,
1117
+ ),
1118
+ )
1119
+ | None => None
1120
+ },
1121
+ fetchState,
1122
+ committedProgressBlockNumber: newProgressBlockNumber,
1123
+ numEventsProcessed: newTotalEventsProcessed,
1124
+ }
1125
+
1126
+ | None =>
1127
+ // Even without a progress diff entry, the reorg chain must have its
1128
+ // reorgDetection and fetchState rolled back. Otherwise the stale block hash
1129
+ // stays in dataByBlockNumber and the same reorg is re-detected on the next
1130
+ // fetch, causing an infinite reorg→rollback loop.
1131
+ if chain == reorgChain {
1132
+ {
1133
+ ...cf,
1134
+ reorgDetection: cf.reorgDetection->ReorgDetection.rollbackToValidBlockNumber(
1135
+ ~blockNumber=rollbackTargetBlockNumber,
1136
+ ),
1137
+ fetchState: cf.fetchState->FetchState.rollback(
1138
+ ~targetBlockNumber=rollbackTargetBlockNumber,
1139
+ ),
1140
+ }
1141
+ } else {
1142
+ cf
1143
+ }
1144
+ }
1145
+ })
1146
+
1147
+ // Construct in Memory store with rollback diff
1148
+ let diff =
1149
+ await state.ctx.persistence->Persistence.prepareRollbackDiff(
1150
+ ~rollbackTargetCheckpointId,
1151
+ ~rollbackDiffCheckpointId=state.chainManager.committedCheckpointId->BigInt.add(1n),
1152
+ )
1153
+
1154
+ let chainManager = {
1155
+ ...state.chainManager,
1156
+ chainFetchers,
1157
+ }
1158
+
1159
+ logger->Logging.childTrace({
1160
+ "msg": "Finished rollback on reorg",
1161
+ "entityChanges": {
1162
+ "deleted": diff["deletedEntities"],
1163
+ "upserted": diff["setEntities"],
1164
+ },
1165
+ "rollbackedEvents": rollbackedProcessedEvents.contents,
1166
+ "beforeCheckpointId": state.chainManager.committedCheckpointId,
1167
+ "targetCheckpointId": rollbackTargetCheckpointId,
1168
+ })
1169
+ Prometheus.RollbackSuccess.increment(
1170
+ ~timeSeconds=Hrtime.timeSince(startTime)->Hrtime.toSecondsFloat,
1171
+ ~rollbackedProcessedEvents=rollbackedProcessedEvents.contents,
1172
+ )
1173
+
1174
+ dispatchAction(
1175
+ SetRollbackState({
1176
+ diffInMemoryStore: diff["inMemStore"],
1177
+ rollbackedChainManager: chainManager,
1178
+ eventsProcessedDiffByChain,
1179
+ }),
1180
+ )
1181
+ }
1182
+ }
1183
+ }
1184
+
1185
+ let taskReducer = injectedTaskReducer(
1186
+ ~waitForNewBlock=SourceManager.waitForNewBlock,
1187
+ ~executeQuery=SourceManager.executeQuery,
1188
+ ~getLastKnownValidBlock=(chainFetcher, ~reorgBlockNumber) =>
1189
+ chainFetcher->ChainFetcher.getLastKnownValidBlock(~reorgBlockNumber),
1190
+ )