envio 3.11.0 → 3.12.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 (89) hide show
  1. package/index.d.ts +3 -0
  2. package/package.json +6 -6
  3. package/src/Batch.res +111 -78
  4. package/src/Batch.res.mjs +53 -46
  5. package/src/BatchProcessing.res +1 -11
  6. package/src/BatchProcessing.res.mjs +1 -4
  7. package/src/ChainState.res +72 -19
  8. package/src/ChainState.res.mjs +50 -12
  9. package/src/ChainState.resi +5 -0
  10. package/src/Config.res +58 -37
  11. package/src/Config.res.mjs +63 -53
  12. package/src/CrossChainState.res +49 -52
  13. package/src/CrossChainState.res.mjs +25 -23
  14. package/src/CrossChainState.resi +6 -13
  15. package/src/EffectState.res +13 -4
  16. package/src/EffectState.resi +7 -1
  17. package/src/EventProcessing.res +6 -6
  18. package/src/EventProcessing.res.mjs +6 -6
  19. package/src/FetchState.res +14 -0
  20. package/src/FetchState.res.mjs +14 -0
  21. package/src/FinalizeBackfill.res +16 -12
  22. package/src/FinalizeBackfill.res.mjs +3 -3
  23. package/src/HistoryPolicy.res +46 -0
  24. package/src/HistoryPolicy.res.mjs +50 -0
  25. package/src/InMemoryStore.res +30 -32
  26. package/src/InMemoryStore.res.mjs +33 -28
  27. package/src/IndexerState.res +66 -45
  28. package/src/IndexerState.res.mjs +57 -29
  29. package/src/IndexerState.resi +8 -13
  30. package/src/Internal.res +4 -2
  31. package/src/LoadLayer.res +11 -4
  32. package/src/LoadLayer.res.mjs +8 -5
  33. package/src/Main.res +25 -24
  34. package/src/Main.res.mjs +24 -25
  35. package/src/Metrics.res +49 -1
  36. package/src/Metrics.res.mjs +26 -3
  37. package/src/Persistence.res +55 -16
  38. package/src/Persistence.res.mjs +18 -3
  39. package/src/PgStorage.res +194 -118
  40. package/src/PgStorage.res.mjs +132 -72
  41. package/src/PruneStaleHistory.res +9 -15
  42. package/src/PruneStaleHistory.res.mjs +11 -20
  43. package/src/Rollback.res +9 -14
  44. package/src/Rollback.res.mjs +5 -3
  45. package/src/Sink.res +7 -5
  46. package/src/Sink.res.mjs +4 -4
  47. package/src/Staging.res +298 -0
  48. package/src/Staging.res.mjs +257 -0
  49. package/src/TestIndexer.res +5 -5
  50. package/src/TestIndexer.res.mjs +7 -6
  51. package/src/UserContext.res +7 -4
  52. package/src/UserContext.res.mjs +5 -2
  53. package/src/Utils.res +9 -0
  54. package/src/Utils.res.mjs +9 -0
  55. package/src/Writing.res +34 -20
  56. package/src/Writing.res.mjs +15 -20
  57. package/src/bindings/ClickHouse.res +41 -34
  58. package/src/bindings/ClickHouse.res.mjs +36 -30
  59. package/src/bindings/ClickHouseSink.res +45 -186
  60. package/src/bindings/ClickHouseSink.res.mjs +21 -174
  61. package/src/db/CheckpointSequence.res +109 -0
  62. package/src/db/CheckpointSequence.res.mjs +99 -0
  63. package/src/db/EntityHistory.res +6 -5
  64. package/src/db/EntityHistory.res.mjs +5 -5
  65. package/src/db/Frontier.res +86 -0
  66. package/src/db/Frontier.res.mjs +126 -0
  67. package/src/db/InternalTable.res +145 -27
  68. package/src/db/InternalTable.res.mjs +115 -29
  69. package/src/db/RollbackFloors.res +34 -41
  70. package/src/db/RollbackFloors.res.mjs +27 -71
  71. package/src/sources/BlockStore.res +3 -0
  72. package/src/sources/EvmHyperSyncSource.res +3 -1
  73. package/src/sources/EvmHyperSyncSource.res.mjs +2 -1
  74. package/src/sources/HyperSync.res +2 -0
  75. package/src/sources/HyperSync.res.mjs +1 -0
  76. package/src/sources/HyperSync.resi +1 -0
  77. package/src/sources/HyperSyncClient.res +3 -0
  78. package/src/sources/RequestStat.res +8 -1
  79. package/src/sources/Source.res +4 -3
  80. package/src/sources/SourceManager.res +30 -10
  81. package/src/sources/SourceManager.res.mjs +24 -7
  82. package/src/sources/SourceManager.resi +2 -0
  83. package/src/tui/Tui.res +56 -143
  84. package/src/tui/Tui.res.mjs +70 -166
  85. package/src/tui/components/TuiData.res +61 -16
  86. package/src/tui/components/TuiData.res.mjs +53 -15
  87. package/svm.schema.json +0 -7
  88. package/src/db/CheckpointBounds.res +0 -53
  89. package/src/db/CheckpointBounds.res.mjs +0 -44
@@ -69,17 +69,7 @@ and processNextBatch = async (state: IndexerState.t, ~scheduleFetch): unit => {
69
69
  let isInReorgThresholdBeforeUpdate = state->IndexerState.isInReorgThreshold
70
70
  let isRealtimeBeforeUpdate = state->IndexerState.isRealtime
71
71
 
72
- let isRollbackBatch = switch state->IndexerState.rollbackState {
73
- | RollbackReady(_) => true
74
- | _ => false
75
- }
76
-
77
- let batch =
78
- state->IndexerState.createBatch(
79
- ~processedCheckpointId=state->IndexerState.processedCheckpointId,
80
- ~batchSizeTarget=(state->IndexerState.config).batchSize,
81
- ~isRollback=isRollbackBatch,
82
- )
72
+ let batch = state->IndexerState.createBatch(~batchSizeTarget=(state->IndexerState.config).batchSize)
83
73
 
84
74
  let progressedChainsById = batch.progressedChainsById
85
75
 
@@ -23,10 +23,7 @@ function yieldTick() {
23
23
  async function processNextBatch(state, scheduleFetch) {
24
24
  let isInReorgThresholdBeforeUpdate = IndexerState.isInReorgThreshold(state);
25
25
  let isRealtimeBeforeUpdate = IndexerState.isRealtime(state);
26
- let match = IndexerState.rollbackState(state);
27
- let isRollbackBatch;
28
- isRollbackBatch = typeof match !== "object" ? false : match.TAG === "RollbackReady";
29
- let batch = IndexerState.createBatch(state, IndexerState.processedCheckpointId(state), IndexerState.config(state).batchSize, isRollbackBatch);
26
+ let batch = IndexerState.createBatch(state, IndexerState.config(state).batchSize);
30
27
  let progressedChainsById = batch.progressedChainsById;
31
28
  let isBelowReorgThreshold = !isInReorgThresholdBeforeUpdate && IndexerState.config(state).shouldRollbackOnReorg;
32
29
  let shouldEnterReorgThreshold = isBelowReorgThreshold && Object.values(IndexerState.chainStates(state)).every(cs => ChainState.isReadyToEnterReorgThresholdAfterBatch(cs, batch));
@@ -41,6 +41,8 @@ type t = {
41
41
  // detected reorg either rolls back or is only logged.
42
42
  shouldRollbackOnReorg: bool,
43
43
  maxReorgDepth: int,
44
+ // One-way: past it, everything the chain writes can still be rolled back.
45
+ mutable isInReorgThreshold: bool,
44
46
  // Holds this chain's transactions (kept in Rust) keyed by (blockNumber,
45
47
  // transactionIndex). Fetch responses merge their page in; entries are pruned
46
48
  // as the chain progresses and dropped above the target on rollback.
@@ -60,6 +62,9 @@ type t = {
60
62
  mutable reorgDetectedBlock: option<int>,
61
63
  mutable rollbackTargetBlock: option<int>,
62
64
  mutable progressLatencyMs: option<int>,
65
+ // Timestamp of the committed progress block itself. See
66
+ // `Batch.chainAfterBatch.progressBlockTime`.
67
+ mutable committedProgressBlockTime: option<int>,
63
68
  }
64
69
 
65
70
  // The chain's config-declared addresses as storage rows: what a clean run
@@ -123,9 +128,11 @@ let make = (
123
128
  ~addressStore: AddressStore.t,
124
129
  ~sourceManager: SourceManager.t,
125
130
  ~committedProgressBlockNumber: int,
131
+ ~committedProgressBlockTime=None,
126
132
  ~safeCheckpointTracking=None,
127
133
  ~shouldRollbackOnReorg,
128
134
  ~maxReorgDepth,
135
+ ~isInReorgThreshold=false,
129
136
  ~numEventsProcessed=0.,
130
137
  ~timestampCaughtUpToHeadOrEndblock=None,
131
138
  ~isProgressAtHead=false,
@@ -155,6 +162,7 @@ let make = (
155
162
  safeCheckpointTracking,
156
163
  shouldRollbackOnReorg,
157
164
  maxReorgDepth,
165
+ isInReorgThreshold,
158
166
  transactionStore,
159
167
  blockStore,
160
168
  reorgThresholdReadyTolerance,
@@ -167,6 +175,7 @@ let make = (
167
175
  reorgDetectedBlock: None,
168
176
  rollbackTargetBlock: None,
169
177
  progressLatencyMs: None,
178
+ committedProgressBlockTime,
170
179
  }
171
180
  }
172
181
 
@@ -180,6 +189,7 @@ let makeInternal = (
180
189
  ~endBlock,
181
190
  ~firstEventBlock=None,
182
191
  ~progressBlockNumber,
192
+ ~committedProgressBlockTime=None,
183
193
  ~config: Config.t,
184
194
  ~registrationsByChainId: HandlerRegister.registrationsByChainId,
185
195
  ~logger,
@@ -308,12 +318,14 @@ let makeInternal = (
308
318
  ~sourceManager=SourceManager.make(~sources, ~isRealtime, ~reducedPollingInterval?),
309
319
  ~shouldRollbackOnReorg=config.shouldRollbackOnReorg,
310
320
  ~maxReorgDepth,
321
+ ~isInReorgThreshold,
311
322
  ~safeCheckpointTracking=SafeCheckpointTracking.make(
312
323
  ~maxReorgDepth,
313
324
  ~shouldRollbackOnReorg=config.shouldRollbackOnReorg,
314
325
  ~chainReorgCheckpoints,
315
326
  ),
316
327
  ~committedProgressBlockNumber=progressBlockNumber,
328
+ ~committedProgressBlockTime,
317
329
  ~perChainEntities=config.userEntities->EntityTables.perChain,
318
330
  ~timestampCaughtUpToHeadOrEndblock,
319
331
  ~numEventsProcessed,
@@ -363,6 +375,10 @@ let makeFromDbState = (
363
375
  ~maxReorgDepth=resumedChainState.maxReorgDepth,
364
376
  ~firstEventBlock=resumedChainState.firstEventBlockNumber,
365
377
  ~progressBlockNumber,
378
+ // A block's time never changes, so the stored one stays true for the block
379
+ // progress is at - and keeps the metric reporting from the first scrape
380
+ // after a restart, rather than only once a batch commits.
381
+ ~committedProgressBlockTime=resumedChainState.progressBlockTime,
366
382
  ~timestampCaughtUpToHeadOrEndblock=resumedChainState.timestampCaughtUpToHeadOrEndblock,
367
383
  ~numEventsProcessed=resumedChainState.numEventsProcessed,
368
384
  ~logger,
@@ -402,6 +418,7 @@ let getLatestValidScannedBlock = (cs: t, ~blockStore: BlockStore.t, ~blockNumber
402
418
  let safeCheckpointTracking = (cs: t) => cs.safeCheckpointTracking
403
419
  let isProgressAtHead = (cs: t) => cs.isProgressAtHead
404
420
  let committedProgressBlockNumber = (cs: t) => cs.committedProgressBlockNumber
421
+ let committedProgressBlockTime = (cs: t) => cs.committedProgressBlockTime
405
422
  let numEventsProcessed = (cs: t) => cs.numEventsProcessed
406
423
  let pendingBudget = (cs: t) => cs.pendingBudget
407
424
  let timestampCaughtUpToHeadOrEndblock = (cs: t) => cs.timestampCaughtUpToHeadOrEndblock
@@ -640,15 +657,21 @@ let hasProcessedToEndblock = (cs: t) => {
640
657
  // head has run away from it while the indexer was down.
641
658
  let isDurablyCaughtUp = (cs: t) => {
642
659
  let {committedProgressBlockNumber, fetchState} = cs
643
- switch fetchState.endBlock {
644
- | Some(endBlock) => committedProgressBlockNumber >= endBlock
645
- | None =>
646
- // The configured lag, not the fetch state's: pre-threshold that one also
647
- // carries maxReorgDepth, which would read a chain a whole reorg depth behind
648
- // the head as caught up.
660
+ let atEndBlock =
661
+ fetchState.endBlock->Option.mapOr(false, endBlock => committedProgressBlockNumber >= endBlock)
662
+ // Either one, like `isFetchingAtHead`: an `end_block` above the head is never
663
+ // reached, and testing only for it would leave such a chain reading as behind
664
+ // however long it sits at the head.
665
+ //
666
+ // The configured lag, not the fetch state's: pre-threshold that one also
667
+ // carries maxReorgDepth, which would read a chain a whole reorg depth behind
668
+ // the head as caught up. Clamped at zero: the -1 a run that has processed
669
+ // nothing carries would otherwise clear a chain younger than its own lag.
670
+ let atHead =
649
671
  fetchState.knownHeight > 0 &&
650
- committedProgressBlockNumber >= fetchState.knownHeight - cs.chainConfig.blockLag
651
- }
672
+ committedProgressBlockNumber >=
673
+ Pervasives.max(0, fetchState.knownHeight - cs.chainConfig.blockLag)
674
+ atEndBlock || atHead
652
675
  }
653
676
 
654
677
  let getHighestBlockBelowThreshold = (cs: t): int => {
@@ -929,8 +952,18 @@ let setEndBlockToFirstEvent = (cs: t, ~blockNumber) =>
929
952
  }
930
953
 
931
954
  // Shrink the fetch buffer by the configured blockLag on entering the reorg threshold.
932
- let enterReorgThreshold = (cs: t) =>
955
+ let enterReorgThreshold = (cs: t) => {
956
+ cs.isInReorgThreshold = true
933
957
  cs.fetchState = cs.fetchState->FetchState.updateInternal(~blockLag=cs.chainConfig.blockLag)
958
+ }
959
+
960
+ let isInReorgThreshold = (cs: t) => cs.isInReorgThreshold
961
+
962
+ // Whether the chain's writes need history: only what a rollback could still
963
+ // reach. A chain with no reorg depth is never rolled back, however far its
964
+ // progress has run.
965
+ let shouldSaveHistory = (cs: t) =>
966
+ cs.shouldRollbackOnReorg && cs.maxReorgDepth > 0 && cs.isInReorgThreshold
934
967
 
935
968
  // Snapshot the chain's metadata fields for staging into the chains table.
936
969
  let toChainMetadata = (cs: t): InternalTable.Chains.metaFields => {
@@ -964,6 +997,7 @@ let toMetrics = (cs: t): Metrics.chainMetrics => {
964
997
  sourceBlockNumber: cs.fetchState.knownHeight,
965
998
  progressBlockNumber: cs.committedProgressBlockNumber,
966
999
  progressLatencyMs: cs.progressLatencyMs,
1000
+ progressBlockTime: cs.committedProgressBlockTime,
967
1001
  concurrency: cs.sourceManager->SourceManager.inFlightCount,
968
1002
  partitionsCount: cs.fetchState->FetchState.partitionsCount,
969
1003
  bufferSize: cs.fetchState->FetchState.bufferSize,
@@ -979,11 +1013,14 @@ let toMetrics = (cs: t): Metrics.chainMetrics => {
979
1013
  reorgCount: cs.reorgCount,
980
1014
  reorgDetectedBlock: cs.reorgDetectedBlock,
981
1015
  rollbackTargetBlock: cs.rollbackTargetBlock,
1016
+ rateLimitTimeMs: cs.sourceManager->SourceManager.getRateLimitTimeMs,
1017
+ rateLimitResetInMs: cs.sourceManager->SourceManager.getRateLimitResetInMs,
982
1018
  }
983
1019
 
984
- // Snapshot the inputs a batch build needs from this chain, including an
985
- // immutable copy of the scanned in-threshold block hashes so the batch never
986
- // reads the live store mid-build.
1020
+ // Snapshot the inputs a batch build needs from this chain. The scanned
1021
+ // in-threshold block hashes are copied up front because the build reuses the
1022
+ // whole set; the block time comes as a lookup instead, since the key it needs -
1023
+ // the batch's own progress block - isn't known until the build computes it.
987
1024
  let toChainBeforeBatch = (cs: t): Batch.chainBeforeBatch => {
988
1025
  let {blockNumbers, hashes} =
989
1026
  cs.blockStore->BlockStore.getHashes(
@@ -1000,6 +1037,8 @@ let toChainBeforeBatch = (cs: t): Batch.chainBeforeBatch => {
1000
1037
  totalEventsProcessed: cs.numEventsProcessed,
1001
1038
  sourceBlockNumber: cs.fetchState.knownHeight,
1002
1039
  scannedHashes: {blockNumbers, hashByBlockNumber},
1040
+ blockTimeAt: blockNumber =>
1041
+ cs.blockStore->BlockStore.getTimestamp(blockNumber)->Null.toOption,
1003
1042
  shouldRollbackOnReorg: cs.shouldRollbackOnReorg,
1004
1043
  chainConfig: cs.chainConfig,
1005
1044
  }
@@ -1099,6 +1138,7 @@ let applyBatchProgress = (cs: t, ~batch: Batch.t, ~blockTimestampName: string) =
1099
1138
  }
1100
1139
 
1101
1140
  cs.committedProgressBlockNumber = chainAfterBatch.progressBlockNumber
1141
+ cs.committedProgressBlockTime = chainAfterBatch.progressBlockTime
1102
1142
 
1103
1143
  // Normally already set by advanceAfterBatch at batch creation; catch up
1104
1144
  // here for paths that commit progress without it.
@@ -1135,16 +1175,29 @@ let applyBatchProgress = (cs: t, ~batch: Batch.t, ~blockTimestampName: string) =
1135
1175
  }
1136
1176
 
1137
1177
  // Mark the chain caught up to head/endblock. Called by CrossChainState only once
1138
- // every chain in the indexer is caught up and the deferred schema indexes are
1139
- // committed, so no chain flips to ready while another is still backfilling or
1140
- // while an index the schema promises is still missing. `readyAt` is the
1141
- // timestamp already committed to `envio_chains.ready_at` in that same
1142
- // transaction. Sticky: a chain stays ready once set.
1178
+ // every chain this process drives is caught up and the indexes the schema
1179
+ // promises them are committed, so no chain flips to ready while an index it
1180
+ // promised is still missing. `readyAt` is the timestamp already committed to
1181
+ // `envio_chains.ready_at` in that same transaction. Sticky: a chain stays ready
1182
+ // once set.
1143
1183
  let markReady = (cs: t, ~readyAt) =>
1144
1184
  if !(cs->isReady) {
1145
1185
  cs.timestampCaughtUpToHeadOrEndblock = Some(readyAt)
1146
1186
  }
1147
1187
 
1188
+ // Rewind progress to a reorg target. Moving it means the timestamp now belongs
1189
+ // to an orphaned block, so it's re-read for the block progress actually lands
1190
+ // on - normally `None`, since the store keeps only hashes below the last
1191
+ // committed progress, until the re-fetch commits a new progress block. A target
1192
+ // at or above the progress block leaves both alone: that block was never
1193
+ // orphaned, and re-reading its long-pruned header would drop a valid timestamp.
1194
+ let rollbackCommittedProgress = (cs: t, blockNumber) =>
1195
+ if blockNumber !== cs.committedProgressBlockNumber {
1196
+ cs.committedProgressBlockNumber = blockNumber
1197
+ cs.committedProgressBlockTime =
1198
+ cs.blockStore->BlockStore.getTimestamp(blockNumber)->Null.toOption
1199
+ }
1200
+
1148
1201
  type progressDiff = {blockNumber: int, eventsProcessed: float}
1149
1202
 
1150
1203
  type rolledBackTo =
@@ -1192,7 +1245,7 @@ let rollback = (cs: t, ~rolledBackTo: rolledBackTo): array<AddressRows.key> => {
1192
1245
  let rolledBackAddresses = rollbackTo(newProgressBlockNumber)
1193
1246
  cs.transactionStore->TransactionStore.rollback(newProgressBlockNumber)
1194
1247
  cs.blockStore->BlockStore.rollback(newProgressBlockNumber)
1195
- cs.committedProgressBlockNumber = newProgressBlockNumber
1248
+ cs->rollbackCommittedProgress(newProgressBlockNumber)
1196
1249
  cs.processingBlockNumber = newProgressBlockNumber
1197
1250
  cs.numEventsProcessed = newTotalEventsProcessed
1198
1251
  rolledBackAddresses
@@ -1202,7 +1255,7 @@ let rollback = (cs: t, ~rolledBackTo: rolledBackTo): array<AddressRows.key> => {
1202
1255
  let rolledBackAddresses = rollbackTo(forkBlock)
1203
1256
  cs.transactionStore->TransactionStore.rollback(forkBlock)
1204
1257
  cs.blockStore->BlockStore.rollback(forkBlock)
1205
- cs.committedProgressBlockNumber = Pervasives.min(cs.committedProgressBlockNumber, forkBlock)
1258
+ cs->rollbackCommittedProgress(Pervasives.min(cs.committedProgressBlockNumber, forkBlock))
1206
1259
  cs.processingBlockNumber = Pervasives.min(cs.processingBlockNumber, forkBlock)
1207
1260
  rolledBackAddresses
1208
1261
  | Untouched => []
@@ -55,9 +55,11 @@ function validateOnEventRegistrations(chainId, registrations) {
55
55
  });
56
56
  }
57
57
 
58
- function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, sourceManager, committedProgressBlockNumber, safeCheckpointTrackingOpt, shouldRollbackOnReorg, maxReorgDepth, numEventsProcessedOpt, timestampCaughtUpToHeadOrEndblockOpt, isProgressAtHeadOpt, transactionStoreOpt, chainDensityOpt, blockStoreOpt, reorgThresholdReadyToleranceOpt, perChainEntitiesOpt, logger) {
58
+ function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, sourceManager, committedProgressBlockNumber, committedProgressBlockTimeOpt, safeCheckpointTrackingOpt, shouldRollbackOnReorg, maxReorgDepth, isInReorgThresholdOpt, numEventsProcessedOpt, timestampCaughtUpToHeadOrEndblockOpt, isProgressAtHeadOpt, transactionStoreOpt, chainDensityOpt, blockStoreOpt, reorgThresholdReadyToleranceOpt, perChainEntitiesOpt, logger) {
59
59
  let onEventRegistrations = onEventRegistrationsOpt !== undefined ? onEventRegistrationsOpt : [];
60
+ let committedProgressBlockTime = committedProgressBlockTimeOpt !== undefined ? Primitive_option.valFromOption(committedProgressBlockTimeOpt) : undefined;
60
61
  let safeCheckpointTracking = safeCheckpointTrackingOpt !== undefined ? Primitive_option.valFromOption(safeCheckpointTrackingOpt) : undefined;
62
+ let isInReorgThreshold = isInReorgThresholdOpt !== undefined ? isInReorgThresholdOpt : false;
61
63
  let numEventsProcessed = numEventsProcessedOpt !== undefined ? numEventsProcessedOpt : 0;
62
64
  let timestampCaughtUpToHeadOrEndblock = timestampCaughtUpToHeadOrEndblockOpt !== undefined ? Primitive_option.valFromOption(timestampCaughtUpToHeadOrEndblockOpt) : undefined;
63
65
  let isProgressAtHead = isProgressAtHeadOpt !== undefined ? isProgressAtHeadOpt : false;
@@ -85,6 +87,7 @@ function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, so
85
87
  safeCheckpointTracking: safeCheckpointTracking,
86
88
  shouldRollbackOnReorg: shouldRollbackOnReorg,
87
89
  maxReorgDepth: maxReorgDepth,
90
+ isInReorgThreshold: isInReorgThreshold,
88
91
  transactionStore: transactionStore,
89
92
  blockStore: blockStore,
90
93
  reorgThresholdReadyTolerance: reorgThresholdReadyTolerance,
@@ -96,7 +99,8 @@ function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, so
96
99
  reorgCount: 0,
97
100
  reorgDetectedBlock: undefined,
98
101
  rollbackTargetBlock: undefined,
99
- progressLatencyMs: undefined
102
+ progressLatencyMs: undefined,
103
+ committedProgressBlockTime: committedProgressBlockTime
100
104
  };
101
105
  }
102
106
 
@@ -110,12 +114,14 @@ function makeFromDbState(chainConfig, resumedChainState, reorgCheckpoints, isInR
110
114
  let startBlock = resumedChainState.startBlock;
111
115
  let endBlock = resumedChainState.endBlock;
112
116
  let firstEventBlockOpt = Primitive_option.some(resumedChainState.firstEventBlockNumber);
117
+ let committedProgressBlockTimeOpt = Primitive_option.some(resumedChainState.progressBlockTime);
113
118
  let timestampCaughtUpToHeadOrEndblock = resumedChainState.timestampCaughtUpToHeadOrEndblock;
114
119
  let numEventsProcessed = resumedChainState.numEventsProcessed;
115
120
  let maxReorgDepth = resumedChainState.maxReorgDepth;
116
121
  let knownHeightOpt = resumedChainState.sourceBlockNumber;
117
122
  let isResumedOpt = true;
118
123
  let firstEventBlock = firstEventBlockOpt !== undefined ? Primitive_option.valFromOption(firstEventBlockOpt) : undefined;
124
+ let committedProgressBlockTime = committedProgressBlockTimeOpt !== undefined ? Primitive_option.valFromOption(committedProgressBlockTimeOpt) : undefined;
119
125
  let knownHeight = knownHeightOpt !== undefined ? knownHeightOpt : 0;
120
126
  let isResumed = isResumedOpt !== undefined ? isResumedOpt : false;
121
127
  let match = Stdlib_Option.getOr(registrationsByChainId[ChainId.toString(chainConfig.id)], {
@@ -159,7 +165,7 @@ function makeFromDbState(chainConfig, resumedChainState, reorgCheckpoints, isInR
159
165
  }
160
166
  let firstEventBlock$1 = fetchState.firstEventBlock;
161
167
  let chainDensity = firstEventBlock$1 !== undefined && progressBlockNumber > firstEventBlock$1 && numEventsProcessed > 0 ? numEventsProcessed / (progressBlockNumber - firstEventBlock$1 | 0) : undefined;
162
- return make(chainConfig, fetchState, onEventRegistrations, addressStore, SourceManager.make(sources, isRealtime, undefined, undefined, undefined, reducedPollingInterval, undefined, undefined), progressBlockNumber, Primitive_option.some(SafeCheckpointTracking.make(maxReorgDepth, config.shouldRollbackOnReorg, chainReorgCheckpoints)), config.shouldRollbackOnReorg, maxReorgDepth, numEventsProcessed, Primitive_option.some(timestampCaughtUpToHeadOrEndblock), undefined, Primitive_option.some(TransactionStore.make(config.ecosystem.name, !lowercaseAddresses)), Primitive_option.some(chainDensity), Primitive_option.some(blockStore), config.reorgThresholdReadyTolerance, EntityTables.perChain(config.userEntities), logger);
168
+ return make(chainConfig, fetchState, onEventRegistrations, addressStore, SourceManager.make(sources, isRealtime, undefined, undefined, undefined, reducedPollingInterval, undefined, undefined), progressBlockNumber, Primitive_option.some(committedProgressBlockTime), Primitive_option.some(SafeCheckpointTracking.make(maxReorgDepth, config.shouldRollbackOnReorg, chainReorgCheckpoints)), config.shouldRollbackOnReorg, maxReorgDepth, isInReorgThreshold, numEventsProcessed, Primitive_option.some(timestampCaughtUpToHeadOrEndblock), undefined, Primitive_option.some(TransactionStore.make(config.ecosystem.name, !lowercaseAddresses)), Primitive_option.some(chainDensity), Primitive_option.some(blockStore), config.reorgThresholdReadyTolerance, EntityTables.perChain(config.userEntities), logger);
163
169
  }
164
170
 
165
171
  function logger(cs) {
@@ -210,6 +216,10 @@ function committedProgressBlockNumber(cs) {
210
216
  return cs.committedProgressBlockNumber;
211
217
  }
212
218
 
219
+ function committedProgressBlockTime(cs) {
220
+ return cs.committedProgressBlockTime;
221
+ }
222
+
213
223
  function numEventsProcessed(cs) {
214
224
  return cs.numEventsProcessed;
215
225
  }
@@ -402,13 +412,12 @@ function hasProcessedToEndblock(cs) {
402
412
  function isDurablyCaughtUp(cs) {
403
413
  let fetchState = cs.fetchState;
404
414
  let committedProgressBlockNumber = cs.committedProgressBlockNumber;
405
- let endBlock = fetchState.endBlock;
406
- if (endBlock !== undefined) {
407
- return committedProgressBlockNumber >= endBlock;
408
- } else if (fetchState.knownHeight > 0) {
409
- return committedProgressBlockNumber >= (fetchState.knownHeight - cs.chainConfig.blockLag | 0);
415
+ let atEndBlock = Stdlib_Option.mapOr(fetchState.endBlock, false, endBlock => committedProgressBlockNumber >= endBlock);
416
+ let atHead = fetchState.knownHeight > 0 && committedProgressBlockNumber >= Primitive_int.max(0, fetchState.knownHeight - cs.chainConfig.blockLag | 0);
417
+ if (atEndBlock) {
418
+ return true;
410
419
  } else {
411
- return false;
420
+ return atHead;
412
421
  }
413
422
  }
414
423
 
@@ -641,9 +650,22 @@ function setEndBlockToFirstEvent(cs, blockNumber) {
641
650
  }
642
651
 
643
652
  function enterReorgThreshold(cs) {
653
+ cs.isInReorgThreshold = true;
644
654
  cs.fetchState = FetchState.updateInternal(cs.fetchState, undefined, undefined, undefined, cs.chainConfig.blockLag, undefined);
645
655
  }
646
656
 
657
+ function isInReorgThreshold(cs) {
658
+ return cs.isInReorgThreshold;
659
+ }
660
+
661
+ function shouldSaveHistory(cs) {
662
+ if (cs.shouldRollbackOnReorg && cs.maxReorgDepth > 0) {
663
+ return cs.isInReorgThreshold;
664
+ } else {
665
+ return false;
666
+ }
667
+ }
668
+
647
669
  function toChainMetadata(cs) {
648
670
  return {
649
671
  first_event_block: Stdlib_Null.fromOption(cs.fetchState.firstEventBlock),
@@ -675,6 +697,7 @@ function toMetrics(cs) {
675
697
  sourceBlockNumber: cs.fetchState.knownHeight,
676
698
  progressBlockNumber: cs.committedProgressBlockNumber,
677
699
  progressLatencyMs: cs.progressLatencyMs,
700
+ progressBlockTime: cs.committedProgressBlockTime,
678
701
  concurrency: SourceManager.inFlightCount(cs.sourceManager),
679
702
  partitionsCount: FetchState.partitionsCount(cs.fetchState),
680
703
  bufferSize: FetchState.bufferSize(cs.fetchState),
@@ -689,7 +712,9 @@ function toMetrics(cs) {
689
712
  blockRangeFetchedBlocks: cs.blockRangeFetchedBlocks,
690
713
  reorgCount: cs.reorgCount,
691
714
  reorgDetectedBlock: cs.reorgDetectedBlock,
692
- rollbackTargetBlock: cs.rollbackTargetBlock
715
+ rollbackTargetBlock: cs.rollbackTargetBlock,
716
+ rateLimitTimeMs: SourceManager.getRateLimitTimeMs(cs.sourceManager),
717
+ rateLimitResetInMs: SourceManager.getRateLimitResetInMs(cs.sourceManager)
693
718
  };
694
719
  }
695
720
 
@@ -707,6 +732,7 @@ function toChainBeforeBatch(cs) {
707
732
  blockNumbers: blockNumbers,
708
733
  hashByBlockNumber: hashByBlockNumber
709
734
  },
735
+ blockTimeAt: blockNumber => Primitive_option.fromNull(cs.blockStore.getTimestamp(blockNumber)),
710
736
  shouldRollbackOnReorg: cs.shouldRollbackOnReorg,
711
737
  progressBlockNumber: cs.committedProgressBlockNumber,
712
738
  sourceBlockNumber: cs.fetchState.knownHeight,
@@ -786,6 +812,7 @@ function applyBatchProgress(cs, batch, blockTimestampName) {
786
812
  }
787
813
  }
788
814
  cs.committedProgressBlockNumber = chainAfterBatch.progressBlockNumber;
815
+ cs.committedProgressBlockTime = chainAfterBatch.progressBlockTime;
789
816
  cs.processingBlockNumber = Primitive_int.max(cs.processingBlockNumber, chainAfterBatch.progressBlockNumber);
790
817
  cs.numEventsProcessed = chainAfterBatch.totalEventsProcessed;
791
818
  cs.transactionStore.prune(chainAfterBatch.progressBlockNumber);
@@ -805,6 +832,14 @@ function markReady(cs, readyAt) {
805
832
  }
806
833
  }
807
834
 
835
+ function rollbackCommittedProgress(cs, blockNumber) {
836
+ if (blockNumber !== cs.committedProgressBlockNumber) {
837
+ cs.committedProgressBlockNumber = blockNumber;
838
+ cs.committedProgressBlockTime = Primitive_option.fromNull(cs.blockStore.getTimestamp(blockNumber));
839
+ return;
840
+ }
841
+ }
842
+
808
843
  function rollback(cs, rolledBackTo) {
809
844
  let rollbackTo = targetBlockNumber => {
810
845
  let match = FetchState.rollback(cs.fetchState, cs.addressStore, targetBlockNumber);
@@ -829,7 +864,7 @@ function rollback(cs, rolledBackTo) {
829
864
  let rolledBackAddresses = rollbackTo(newProgressBlockNumber);
830
865
  cs.transactionStore.rollback(newProgressBlockNumber);
831
866
  cs.blockStore.rollback(newProgressBlockNumber);
832
- cs.committedProgressBlockNumber = newProgressBlockNumber;
867
+ rollbackCommittedProgress(cs, newProgressBlockNumber);
833
868
  cs.processingBlockNumber = newProgressBlockNumber;
834
869
  cs.numEventsProcessed = newTotalEventsProcessed;
835
870
  return rolledBackAddresses;
@@ -838,7 +873,7 @@ function rollback(cs, rolledBackTo) {
838
873
  let rolledBackAddresses$1 = rollbackTo(forkBlock);
839
874
  cs.transactionStore.rollback(forkBlock);
840
875
  cs.blockStore.rollback(forkBlock);
841
- cs.committedProgressBlockNumber = Primitive_int.min(cs.committedProgressBlockNumber, forkBlock);
876
+ rollbackCommittedProgress(cs, Primitive_int.min(cs.committedProgressBlockNumber, forkBlock));
842
877
  cs.processingBlockNumber = Primitive_int.min(cs.processingBlockNumber, forkBlock);
843
878
  return rolledBackAddresses$1;
844
879
  }
@@ -860,6 +895,7 @@ export {
860
895
  safeCheckpointTracking,
861
896
  isProgressAtHead,
862
897
  committedProgressBlockNumber,
898
+ committedProgressBlockTime,
863
899
  numEventsProcessed,
864
900
  pendingBudget,
865
901
  startFetchingQueries,
@@ -902,6 +938,8 @@ export {
902
938
  updateKnownHeight,
903
939
  setEndBlockToFirstEvent,
904
940
  enterReorgThreshold,
941
+ isInReorgThreshold,
942
+ shouldSaveHistory,
905
943
  advanceAfterBatch,
906
944
  applyBatchProgress,
907
945
  markReady,
@@ -23,9 +23,11 @@ let make: (
23
23
  ~addressStore: AddressStore.t,
24
24
  ~sourceManager: SourceManager.t,
25
25
  ~committedProgressBlockNumber: int,
26
+ ~committedProgressBlockTime: option<int>=?,
26
27
  ~safeCheckpointTracking: option<SafeCheckpointTracking.t>=?,
27
28
  ~shouldRollbackOnReorg: bool,
28
29
  ~maxReorgDepth: int,
30
+ ~isInReorgThreshold: bool=?,
29
31
  ~numEventsProcessed: float=?,
30
32
  ~timestampCaughtUpToHeadOrEndblock: option<Date.t>=?,
31
33
  ~isProgressAtHead: bool=?,
@@ -67,6 +69,7 @@ let getLatestValidScannedBlock: (
67
69
  let safeCheckpointTracking: t => option<SafeCheckpointTracking.t>
68
70
  let isProgressAtHead: t => bool
69
71
  let committedProgressBlockNumber: t => int
72
+ let committedProgressBlockTime: t => option<int>
70
73
  let numEventsProcessed: t => float
71
74
  let pendingBudget: t => float
72
75
  let startFetchingQueries: (t, ~queries: array<FetchState.query>) => unit
@@ -154,6 +157,8 @@ let resetPendingQueries: t => unit
154
157
  let updateKnownHeight: (t, ~knownHeight: int) => unit
155
158
  let setEndBlockToFirstEvent: (t, ~blockNumber: int) => unit
156
159
  let enterReorgThreshold: t => unit
160
+ let isInReorgThreshold: t => bool
161
+ let shouldSaveHistory: t => bool
157
162
  let advanceAfterBatch: (t, ~batch: Batch.t, ~enteringReorgThreshold: bool) => unit
158
163
  let applyBatchProgress: (t, ~batch: Batch.t, ~blockTimestampName: string) => unit
159
164
  let markReady: (t, ~readyAt: Date.t) => unit
package/src/Config.res CHANGED
@@ -136,9 +136,17 @@ type t = {
136
136
  reorgThresholdReadyTolerance: int,
137
137
  lowercaseAddresses: bool,
138
138
  isDev: bool,
139
+ // An `envio start --chain` process: drives a subset of the schema's chains
140
+ // while sibling processes drive the rest, so it only touches what its own
141
+ // chains own — their partitions' indexes, their `ready_at`, their resume.
142
+ isolated: bool,
139
143
  userEntitiesByName: dict<Internal.entityConfig>,
140
144
  userEntities: array<Internal.entityConfig>,
141
145
  allEnums: array<Table.enumConfig<Table.enum>>,
146
+ // Whether checkpoint ids come from one counter or one per chain. Decided by
147
+ // the schema alone: a cross-chain entity has rows any chain's reorg can
148
+ // reach, so its checkpoints have to be comparable across chains.
149
+ checkpointSequence: CheckpointSequence.t,
142
150
  }
143
151
 
144
152
  type rpcSourceFor = | @as("sync") Sync | @as("fallback") Fallback | @as("realtime") Realtime
@@ -586,6 +594,7 @@ let publicConfigSchema = S.schema(s =>
586
594
  "description": s.matches(S.option(S.string)),
587
595
  "handlers": s.matches(S.option(S.string)),
588
596
  "isDev": s.matches(S.option(S.bool)),
597
+ "isolatedChains": s.matches(S.option(S.array(ChainId.schema))),
589
598
  "fullBatchSize": s.matches(S.option(S.int)),
590
599
  "rollbackOnReorg": s.matches(S.option(S.bool)),
591
600
  "saveFullHistory": s.matches(S.option(S.bool)),
@@ -609,6 +618,44 @@ let contractMappingOf = (~chainConfigs: array<chain>): ContractMapping.t => {
609
618
  ContractMapping.make(~names)
610
619
  }
611
620
 
621
+ let getChain = (config, ~chainId) =>
622
+ config.chainMap->ChainMap.has(chainId)
623
+ ? chainId
624
+ : JsError.throwWithMessage(
625
+ "No chain with id " ++ chainId->ChainId.toString ++ " found in config.yaml",
626
+ )
627
+
628
+ // Narrows a config to the chains one `envio start --chain` process drives.
629
+ // `contractMapping` is deliberately left whole: its ids are what the migration
630
+ // that created the schema stored, and one rebuilt from a subset would hand the
631
+ // same contract a different id.
632
+ let isolate = (config: t, ~chainIds: array<ChainId.t>) => {
633
+ // Chains indexed in separate processes each advance their own checkpoint
634
+ // counter, which only holds while no entity has rows another chain can reach.
635
+ switch config.userEntities->Array.filter(entityConfig => entityConfig.crossChain) {
636
+ | [] => ()
637
+ | shared =>
638
+ JsError.throwWithMessage(
639
+ `Only a schema whose entities are all per-chain can be split across processes. Shared across chains: ${shared
640
+ ->Array.map(entityConfig => entityConfig.name)
641
+ ->Array.joinUnsafe(", ")}.`,
642
+ )
643
+ }
644
+ chainIds->Array.forEach(chainId => config->getChain(~chainId)->ignore)
645
+
646
+ // Filtered out of the config's own chain order rather than built from the
647
+ // argument order, so a repeated `--chain` collapses and `defaultChain` doesn't
648
+ // depend on how the flags were typed.
649
+ let chains =
650
+ config.chainMap->ChainMap.values->Array.filter(chain => chainIds->Array.includes(chain.id))
651
+ {
652
+ ...config,
653
+ chainMap: chains->Array.map(chain => (chain.id, chain))->ChainMap.fromArrayUnsafe,
654
+ defaultChain: chains->Array.get(0),
655
+ isolated: true,
656
+ }
657
+ }
658
+
612
659
  let fromPublic = (publicConfigJson: JSON.t) => {
613
660
  let maxAddrInPartition = Env.maxAddrInPartition
614
661
  // Parse public config
@@ -709,12 +756,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
709
756
  }
710
757
  let widened =
711
758
  contractConfig->(
712
- Utils.magic: _ => {
713
- "svmAbi": option<{
714
- "definedTypes": JSON.t,
715
- "source": string,
716
- }>,
717
- }
759
+ Utils.magic: _ => {"svmAbi": option<{"definedTypes": JSON.t, "source": string}>}
718
760
  )
719
761
  contractDataByName->Dict.set(
720
762
  capitalizedName,
@@ -1063,7 +1105,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
1063
1105
  | None => []
1064
1106
  }
1065
1107
 
1066
- {
1108
+ let config = {
1067
1109
  name: publicConfig["name"],
1068
1110
  description: publicConfig["description"],
1069
1111
  handlers: publicConfig["handlers"]->Option.getOr("src/handlers"),
@@ -1084,19 +1126,18 @@ let fromPublic = (publicConfigJson: JSON.t) => {
1084
1126
  reorgThresholdReadyTolerance: 100,
1085
1127
  lowercaseAddresses,
1086
1128
  isDev: publicConfig["isDev"]->Option.getOr(false),
1129
+ isolated: false,
1087
1130
  userEntitiesByName,
1088
1131
  userEntities,
1089
1132
  allEnums,
1133
+ checkpointSequence: CheckpointSequence.fromEntities(userEntities),
1090
1134
  }
1091
- }
1092
1135
 
1093
- // With no cross-chain entity, a reorg on one chain can never have changed a row
1094
- // another chain owns, so its rollback stays isolated to that chain instead of
1095
- // dragging every sibling back with it. A single chain has no sibling to spare,
1096
- // and narrowing its rollback would only buy it a predicate that always holds.
1097
- let isIsolatedMultichain = (config: t) =>
1098
- config.chainMap->ChainMap.keys->Array.length > 1 &&
1099
- config.userEntities->Array.every(entityConfig => !entityConfig.crossChain)
1136
+ switch publicConfig["isolatedChains"] {
1137
+ | None => config
1138
+ | Some(chainIds) => config->isolate(~chainIds)
1139
+ }
1140
+ }
1100
1141
 
1101
1142
  // Canonicalize a user-provided address to the configured casing so it matches
1102
1143
  // addresses parsed from config.yaml during routing. HyperSync/RPC data arrives
@@ -1169,33 +1210,12 @@ let getEventConfig = (config: t, ~contractName, ~eventName, ~chainId: option<Cha
1169
1210
  })
1170
1211
  }
1171
1212
 
1172
- // A chain that can't be rolled back (maxReorgDepth = 0) has no history to keep,
1173
- // unless a cross-chain entity lets another chain's rollback reach its rows.
1174
- let shouldSaveHistory = (config, ~isInReorgThreshold, ~chainId: option<ChainId.t>=?) =>
1175
- config.shouldSaveFullHistory ||
1176
- (config.shouldRollbackOnReorg &&
1177
- isInReorgThreshold &&
1178
- switch chainId {
1179
- | Some(chainId) if config->isIsolatedMultichain =>
1180
- (config.chainMap->ChainMap.get(chainId)).maxReorgDepth > 0
1181
- | _ => true
1182
- })
1183
-
1184
- let shouldPruneHistory = (config, ~isInReorgThreshold) =>
1185
- !config.shouldSaveFullHistory && (config.shouldRollbackOnReorg && isInReorgThreshold)
1186
-
1187
- let getChain = (config, ~chainId) =>
1188
- config.chainMap->ChainMap.has(chainId)
1189
- ? chainId
1190
- : JsError.throwWithMessage(
1191
- "No chain with id " ++ chainId->ChainId.toString ++ " found in config.yaml",
1192
- )
1193
-
1194
1213
  // A CLI command payload already contains the resolved JSON; priming lets
1195
1214
  // downstream callers skip the NAPI `getConfigJson` round-trip. Calling
1196
1215
  // `prime` again invalidates the memo.
1197
1216
  %%private(let primedJson: ref<option<JSON.t>> = ref(None))
1198
1217
  %%private(let cached: ref<option<t>> = ref(None))
1218
+
1199
1219
  let prime = (json: JSON.t): unit => {
1200
1220
  primedJson := Some(json)
1201
1221
  cached := None
@@ -1236,6 +1256,7 @@ let stripSensitiveData = (json: JSON.t): JSON.t => {
1236
1256
  switch cloned {
1237
1257
  | Object(obj) => {
1238
1258
  obj->Utils.Dict.deleteInPlace("isDev")
1259
+ obj->Utils.Dict.deleteInPlace("isolatedChains")
1239
1260
  stripChains(obj->Dict.get("evm"))
1240
1261
  stripChains(obj->Dict.get("fuel"))
1241
1262
  stripChains(obj->Dict.get("svm"))