envio 3.11.0-svm-alpha.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.11.0-svm-alpha.1",
3
+ "version": "3.12.0",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -68,10 +68,10 @@
68
68
  "tsx": "4.21.0"
69
69
  },
70
70
  "optionalDependencies": {
71
- "envio-linux-x64": "3.11.0-svm-alpha.1",
72
- "envio-linux-x64-musl": "3.11.0-svm-alpha.1",
73
- "envio-linux-arm64": "3.11.0-svm-alpha.1",
74
- "envio-darwin-x64": "3.11.0-svm-alpha.1",
75
- "envio-darwin-arm64": "3.11.0-svm-alpha.1"
71
+ "envio-linux-x64": "3.12.0",
72
+ "envio-linux-x64-musl": "3.12.0",
73
+ "envio-linux-arm64": "3.12.0",
74
+ "envio-darwin-x64": "3.12.0",
75
+ "envio-darwin-arm64": "3.12.0"
76
76
  }
77
77
  }
package/src/Batch.res CHANGED
@@ -4,6 +4,12 @@ open Utils.UnsafeIntOperators
4
4
  type chainAfterBatch = {
5
5
  batchSize: int,
6
6
  progressBlockNumber: int,
7
+ // Timestamp of `progressBlockNumber` itself, never of a lower block, so it
8
+ // measures how far behind chain time the committed progress is. `None` when
9
+ // the source didn't return that block's header - during backfill it only
10
+ // returns blocks carrying items, and an SVM slot may have produced no block
11
+ // at all.
12
+ progressBlockTime: option<int>,
7
13
  sourceBlockNumber: int,
8
14
  totalEventsProcessed: float,
9
15
  fetchState: FetchState.t,
@@ -23,6 +29,10 @@ type reorgHashSnapshot = {
23
29
  type chainBeforeBatch = {
24
30
  fetchState: FetchState.t,
25
31
  scannedHashes: reorgHashSnapshot,
32
+ // The progress block isn't known until the build computes it, so its
33
+ // timestamp can't be snapshotted with the rest. Reads the chain's store,
34
+ // which the synchronous build can't see mutate.
35
+ blockTimeAt: int => option<int>,
26
36
  shouldRollbackOnReorg: bool,
27
37
  progressBlockNumber: int,
28
38
  sourceBlockNumber: int,
@@ -70,6 +80,7 @@ let getProgressedChainsById = {
70
80
  {
71
81
  batchSize: counts.size,
72
82
  progressBlockNumber: progressBlockNumberAfterBatch,
83
+ progressBlockTime: chainBeforeBatch.blockTimeAt(progressBlockNumberAfterBatch),
73
84
  sourceBlockNumber: chainBeforeBatch.sourceBlockNumber,
74
85
  totalEventsProcessed: chainBeforeBatch.totalEventsProcessed +.
75
86
  counts.eventsProcessed->Int.toFloat,
package/src/Batch.res.mjs CHANGED
@@ -10,6 +10,7 @@ function getChainAfterBatchIfProgressed(chainBeforeBatch, progressBlockNumberAft
10
10
  return {
11
11
  batchSize: counts.size,
12
12
  progressBlockNumber: progressBlockNumberAfterBatch,
13
+ progressBlockTime: chainBeforeBatch.blockTimeAt(progressBlockNumberAfterBatch),
13
14
  sourceBlockNumber: chainBeforeBatch.sourceBlockNumber,
14
15
  totalEventsProcessed: chainBeforeBatch.totalEventsProcessed + counts.eventsProcessed,
15
16
  fetchState: fetchStateAfterBatch,
@@ -62,6 +62,9 @@ type t = {
62
62
  mutable reorgDetectedBlock: option<int>,
63
63
  mutable rollbackTargetBlock: option<int>,
64
64
  mutable progressLatencyMs: option<int>,
65
+ // Timestamp of the committed progress block itself. See
66
+ // `Batch.chainAfterBatch.progressBlockTime`.
67
+ mutable committedProgressBlockTime: option<int>,
65
68
  }
66
69
 
67
70
  // The chain's config-declared addresses as storage rows: what a clean run
@@ -125,6 +128,7 @@ let make = (
125
128
  ~addressStore: AddressStore.t,
126
129
  ~sourceManager: SourceManager.t,
127
130
  ~committedProgressBlockNumber: int,
131
+ ~committedProgressBlockTime=None,
128
132
  ~safeCheckpointTracking=None,
129
133
  ~shouldRollbackOnReorg,
130
134
  ~maxReorgDepth,
@@ -171,6 +175,7 @@ let make = (
171
175
  reorgDetectedBlock: None,
172
176
  rollbackTargetBlock: None,
173
177
  progressLatencyMs: None,
178
+ committedProgressBlockTime,
174
179
  }
175
180
  }
176
181
 
@@ -184,6 +189,7 @@ let makeInternal = (
184
189
  ~endBlock,
185
190
  ~firstEventBlock=None,
186
191
  ~progressBlockNumber,
192
+ ~committedProgressBlockTime=None,
187
193
  ~config: Config.t,
188
194
  ~registrationsByChainId: HandlerRegister.registrationsByChainId,
189
195
  ~logger,
@@ -319,6 +325,7 @@ let makeInternal = (
319
325
  ~chainReorgCheckpoints,
320
326
  ),
321
327
  ~committedProgressBlockNumber=progressBlockNumber,
328
+ ~committedProgressBlockTime,
322
329
  ~perChainEntities=config.userEntities->EntityTables.perChain,
323
330
  ~timestampCaughtUpToHeadOrEndblock,
324
331
  ~numEventsProcessed,
@@ -368,6 +375,10 @@ let makeFromDbState = (
368
375
  ~maxReorgDepth=resumedChainState.maxReorgDepth,
369
376
  ~firstEventBlock=resumedChainState.firstEventBlockNumber,
370
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,
371
382
  ~timestampCaughtUpToHeadOrEndblock=resumedChainState.timestampCaughtUpToHeadOrEndblock,
372
383
  ~numEventsProcessed=resumedChainState.numEventsProcessed,
373
384
  ~logger,
@@ -407,6 +418,7 @@ let getLatestValidScannedBlock = (cs: t, ~blockStore: BlockStore.t, ~blockNumber
407
418
  let safeCheckpointTracking = (cs: t) => cs.safeCheckpointTracking
408
419
  let isProgressAtHead = (cs: t) => cs.isProgressAtHead
409
420
  let committedProgressBlockNumber = (cs: t) => cs.committedProgressBlockNumber
421
+ let committedProgressBlockTime = (cs: t) => cs.committedProgressBlockTime
410
422
  let numEventsProcessed = (cs: t) => cs.numEventsProcessed
411
423
  let pendingBudget = (cs: t) => cs.pendingBudget
412
424
  let timestampCaughtUpToHeadOrEndblock = (cs: t) => cs.timestampCaughtUpToHeadOrEndblock
@@ -985,6 +997,7 @@ let toMetrics = (cs: t): Metrics.chainMetrics => {
985
997
  sourceBlockNumber: cs.fetchState.knownHeight,
986
998
  progressBlockNumber: cs.committedProgressBlockNumber,
987
999
  progressLatencyMs: cs.progressLatencyMs,
1000
+ progressBlockTime: cs.committedProgressBlockTime,
988
1001
  concurrency: cs.sourceManager->SourceManager.inFlightCount,
989
1002
  partitionsCount: cs.fetchState->FetchState.partitionsCount,
990
1003
  bufferSize: cs.fetchState->FetchState.bufferSize,
@@ -1004,9 +1017,10 @@ let toMetrics = (cs: t): Metrics.chainMetrics => {
1004
1017
  rateLimitResetInMs: cs.sourceManager->SourceManager.getRateLimitResetInMs,
1005
1018
  }
1006
1019
 
1007
- // Snapshot the inputs a batch build needs from this chain, including an
1008
- // immutable copy of the scanned in-threshold block hashes so the batch never
1009
- // 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.
1010
1024
  let toChainBeforeBatch = (cs: t): Batch.chainBeforeBatch => {
1011
1025
  let {blockNumbers, hashes} =
1012
1026
  cs.blockStore->BlockStore.getHashes(
@@ -1023,6 +1037,8 @@ let toChainBeforeBatch = (cs: t): Batch.chainBeforeBatch => {
1023
1037
  totalEventsProcessed: cs.numEventsProcessed,
1024
1038
  sourceBlockNumber: cs.fetchState.knownHeight,
1025
1039
  scannedHashes: {blockNumbers, hashByBlockNumber},
1040
+ blockTimeAt: blockNumber =>
1041
+ cs.blockStore->BlockStore.getTimestamp(blockNumber)->Null.toOption,
1026
1042
  shouldRollbackOnReorg: cs.shouldRollbackOnReorg,
1027
1043
  chainConfig: cs.chainConfig,
1028
1044
  }
@@ -1122,6 +1138,7 @@ let applyBatchProgress = (cs: t, ~batch: Batch.t, ~blockTimestampName: string) =
1122
1138
  }
1123
1139
 
1124
1140
  cs.committedProgressBlockNumber = chainAfterBatch.progressBlockNumber
1141
+ cs.committedProgressBlockTime = chainAfterBatch.progressBlockTime
1125
1142
 
1126
1143
  // Normally already set by advanceAfterBatch at batch creation; catch up
1127
1144
  // here for paths that commit progress without it.
@@ -1168,6 +1185,19 @@ let markReady = (cs: t, ~readyAt) =>
1168
1185
  cs.timestampCaughtUpToHeadOrEndblock = Some(readyAt)
1169
1186
  }
1170
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
+
1171
1201
  type progressDiff = {blockNumber: int, eventsProcessed: float}
1172
1202
 
1173
1203
  type rolledBackTo =
@@ -1215,7 +1245,7 @@ let rollback = (cs: t, ~rolledBackTo: rolledBackTo): array<AddressRows.key> => {
1215
1245
  let rolledBackAddresses = rollbackTo(newProgressBlockNumber)
1216
1246
  cs.transactionStore->TransactionStore.rollback(newProgressBlockNumber)
1217
1247
  cs.blockStore->BlockStore.rollback(newProgressBlockNumber)
1218
- cs.committedProgressBlockNumber = newProgressBlockNumber
1248
+ cs->rollbackCommittedProgress(newProgressBlockNumber)
1219
1249
  cs.processingBlockNumber = newProgressBlockNumber
1220
1250
  cs.numEventsProcessed = newTotalEventsProcessed
1221
1251
  rolledBackAddresses
@@ -1225,7 +1255,7 @@ let rollback = (cs: t, ~rolledBackTo: rolledBackTo): array<AddressRows.key> => {
1225
1255
  let rolledBackAddresses = rollbackTo(forkBlock)
1226
1256
  cs.transactionStore->TransactionStore.rollback(forkBlock)
1227
1257
  cs.blockStore->BlockStore.rollback(forkBlock)
1228
- cs.committedProgressBlockNumber = Pervasives.min(cs.committedProgressBlockNumber, forkBlock)
1258
+ cs->rollbackCommittedProgress(Pervasives.min(cs.committedProgressBlockNumber, forkBlock))
1229
1259
  cs.processingBlockNumber = Pervasives.min(cs.processingBlockNumber, forkBlock)
1230
1260
  rolledBackAddresses
1231
1261
  | Untouched => []
@@ -55,8 +55,9 @@ function validateOnEventRegistrations(chainId, registrations) {
55
55
  });
56
56
  }
57
57
 
58
- function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, sourceManager, committedProgressBlockNumber, safeCheckpointTrackingOpt, shouldRollbackOnReorg, maxReorgDepth, isInReorgThresholdOpt, 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;
61
62
  let isInReorgThreshold = isInReorgThresholdOpt !== undefined ? isInReorgThresholdOpt : false;
62
63
  let numEventsProcessed = numEventsProcessedOpt !== undefined ? numEventsProcessedOpt : 0;
@@ -98,7 +99,8 @@ function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, so
98
99
  reorgCount: 0,
99
100
  reorgDetectedBlock: undefined,
100
101
  rollbackTargetBlock: undefined,
101
- progressLatencyMs: undefined
102
+ progressLatencyMs: undefined,
103
+ committedProgressBlockTime: committedProgressBlockTime
102
104
  };
103
105
  }
104
106
 
@@ -112,12 +114,14 @@ function makeFromDbState(chainConfig, resumedChainState, reorgCheckpoints, isInR
112
114
  let startBlock = resumedChainState.startBlock;
113
115
  let endBlock = resumedChainState.endBlock;
114
116
  let firstEventBlockOpt = Primitive_option.some(resumedChainState.firstEventBlockNumber);
117
+ let committedProgressBlockTimeOpt = Primitive_option.some(resumedChainState.progressBlockTime);
115
118
  let timestampCaughtUpToHeadOrEndblock = resumedChainState.timestampCaughtUpToHeadOrEndblock;
116
119
  let numEventsProcessed = resumedChainState.numEventsProcessed;
117
120
  let maxReorgDepth = resumedChainState.maxReorgDepth;
118
121
  let knownHeightOpt = resumedChainState.sourceBlockNumber;
119
122
  let isResumedOpt = true;
120
123
  let firstEventBlock = firstEventBlockOpt !== undefined ? Primitive_option.valFromOption(firstEventBlockOpt) : undefined;
124
+ let committedProgressBlockTime = committedProgressBlockTimeOpt !== undefined ? Primitive_option.valFromOption(committedProgressBlockTimeOpt) : undefined;
121
125
  let knownHeight = knownHeightOpt !== undefined ? knownHeightOpt : 0;
122
126
  let isResumed = isResumedOpt !== undefined ? isResumedOpt : false;
123
127
  let match = Stdlib_Option.getOr(registrationsByChainId[ChainId.toString(chainConfig.id)], {
@@ -161,7 +165,7 @@ function makeFromDbState(chainConfig, resumedChainState, reorgCheckpoints, isInR
161
165
  }
162
166
  let firstEventBlock$1 = fetchState.firstEventBlock;
163
167
  let chainDensity = firstEventBlock$1 !== undefined && progressBlockNumber > firstEventBlock$1 && numEventsProcessed > 0 ? numEventsProcessed / (progressBlockNumber - firstEventBlock$1 | 0) : undefined;
164
- 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, 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);
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);
165
169
  }
166
170
 
167
171
  function logger(cs) {
@@ -212,6 +216,10 @@ function committedProgressBlockNumber(cs) {
212
216
  return cs.committedProgressBlockNumber;
213
217
  }
214
218
 
219
+ function committedProgressBlockTime(cs) {
220
+ return cs.committedProgressBlockTime;
221
+ }
222
+
215
223
  function numEventsProcessed(cs) {
216
224
  return cs.numEventsProcessed;
217
225
  }
@@ -689,6 +697,7 @@ function toMetrics(cs) {
689
697
  sourceBlockNumber: cs.fetchState.knownHeight,
690
698
  progressBlockNumber: cs.committedProgressBlockNumber,
691
699
  progressLatencyMs: cs.progressLatencyMs,
700
+ progressBlockTime: cs.committedProgressBlockTime,
692
701
  concurrency: SourceManager.inFlightCount(cs.sourceManager),
693
702
  partitionsCount: FetchState.partitionsCount(cs.fetchState),
694
703
  bufferSize: FetchState.bufferSize(cs.fetchState),
@@ -723,6 +732,7 @@ function toChainBeforeBatch(cs) {
723
732
  blockNumbers: blockNumbers,
724
733
  hashByBlockNumber: hashByBlockNumber
725
734
  },
735
+ blockTimeAt: blockNumber => Primitive_option.fromNull(cs.blockStore.getTimestamp(blockNumber)),
726
736
  shouldRollbackOnReorg: cs.shouldRollbackOnReorg,
727
737
  progressBlockNumber: cs.committedProgressBlockNumber,
728
738
  sourceBlockNumber: cs.fetchState.knownHeight,
@@ -802,6 +812,7 @@ function applyBatchProgress(cs, batch, blockTimestampName) {
802
812
  }
803
813
  }
804
814
  cs.committedProgressBlockNumber = chainAfterBatch.progressBlockNumber;
815
+ cs.committedProgressBlockTime = chainAfterBatch.progressBlockTime;
805
816
  cs.processingBlockNumber = Primitive_int.max(cs.processingBlockNumber, chainAfterBatch.progressBlockNumber);
806
817
  cs.numEventsProcessed = chainAfterBatch.totalEventsProcessed;
807
818
  cs.transactionStore.prune(chainAfterBatch.progressBlockNumber);
@@ -821,6 +832,14 @@ function markReady(cs, readyAt) {
821
832
  }
822
833
  }
823
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
+
824
843
  function rollback(cs, rolledBackTo) {
825
844
  let rollbackTo = targetBlockNumber => {
826
845
  let match = FetchState.rollback(cs.fetchState, cs.addressStore, targetBlockNumber);
@@ -845,7 +864,7 @@ function rollback(cs, rolledBackTo) {
845
864
  let rolledBackAddresses = rollbackTo(newProgressBlockNumber);
846
865
  cs.transactionStore.rollback(newProgressBlockNumber);
847
866
  cs.blockStore.rollback(newProgressBlockNumber);
848
- cs.committedProgressBlockNumber = newProgressBlockNumber;
867
+ rollbackCommittedProgress(cs, newProgressBlockNumber);
849
868
  cs.processingBlockNumber = newProgressBlockNumber;
850
869
  cs.numEventsProcessed = newTotalEventsProcessed;
851
870
  return rolledBackAddresses;
@@ -854,7 +873,7 @@ function rollback(cs, rolledBackTo) {
854
873
  let rolledBackAddresses$1 = rollbackTo(forkBlock);
855
874
  cs.transactionStore.rollback(forkBlock);
856
875
  cs.blockStore.rollback(forkBlock);
857
- cs.committedProgressBlockNumber = Primitive_int.min(cs.committedProgressBlockNumber, forkBlock);
876
+ rollbackCommittedProgress(cs, Primitive_int.min(cs.committedProgressBlockNumber, forkBlock));
858
877
  cs.processingBlockNumber = Primitive_int.min(cs.processingBlockNumber, forkBlock);
859
878
  return rolledBackAddresses$1;
860
879
  }
@@ -876,6 +895,7 @@ export {
876
895
  safeCheckpointTracking,
877
896
  isProgressAtHead,
878
897
  committedProgressBlockNumber,
898
+ committedProgressBlockTime,
879
899
  numEventsProcessed,
880
900
  pendingBudget,
881
901
  startFetchingQueries,
@@ -23,6 +23,7 @@ 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,
@@ -68,6 +69,7 @@ let getLatestValidScannedBlock: (
68
69
  let safeCheckpointTracking: t => option<SafeCheckpointTracking.t>
69
70
  let isProgressAtHead: t => bool
70
71
  let committedProgressBlockNumber: t => int
72
+ let committedProgressBlockTime: t => option<int>
71
73
  let numEventsProcessed: t => float
72
74
  let pendingBudget: t => float
73
75
  let startFetchingQueries: (t, ~queries: array<FetchState.query>) => unit
@@ -257,6 +257,10 @@ let idleOrWaitAction = (cs: ChainState.t) =>
257
257
  ? FetchState.NothingToQuery
258
258
  : FetchState.WaitingForNewBlock
259
259
 
260
+ // How far past the alignment anchor's frontier progress a follower may fetch,
261
+ // as a fraction of its own alignable range.
262
+ let alignmentMargin = 0.2
263
+
260
264
  // Dispatch a fetch tick across the whole indexer from one shared pool of
261
265
  // ~targetBufferSize ready events, as a waterfall: visit chains furthest-behind
262
266
  // first, hand each the budget remaining at that point (plus its own
@@ -340,11 +344,14 @@ let checkAndFetch = async (
340
344
  (isCold ? Pervasives.min(remaining.contents, coldChainBudget) : remaining.contents) +.
341
345
  cs->ChainState.pendingBudget
342
346
  let maxTargetBlock = switch alignment {
343
- // 10% margin past the anchor's line: chains whose progress tracks the
347
+ // 20% margin past the anchor's line: chains whose progress tracks the
344
348
  // anchor closely would otherwise flap in and out of the clamp on every
345
- // small frontier move, stalling their pipeline every other tick.
349
+ // small frontier move, stalling their pipeline every other tick. The
350
+ // margin is also the headroom a follower keeps buffered while the anchor
351
+ // is mid-fetch, so it has to outlast a slow anchor response, not just
352
+ // absorb frontier jitter.
346
353
  | Some((anchorChainId, progress)) if anchorChainId !== chainId =>
347
- Some(cs->ChainState.blockAtProgress(~progress=progress +. 0.1))
354
+ Some(cs->ChainState.blockAtProgress(~progress=progress +. alignmentMargin))
348
355
  | _ => None
349
356
  }
350
357
  switch cs->ChainState.getNextQuery(~chainTargetItems, ~maxTargetBlock?) {
@@ -199,7 +199,7 @@ async function checkAndFetch(crossChainState, dispatchChain) {
199
199
  let chainTargetItems = (
200
200
  isCold ? Primitive_float.min(remaining.contents, minimumAdmissionBudget) : remaining.contents
201
201
  ) + ChainState.pendingBudget(cs);
202
- let maxTargetBlock = alignment !== undefined && alignment[0] !== chainId ? ChainState.blockAtProgress(cs, alignment[1] + 0.1) : undefined;
202
+ let maxTargetBlock = alignment !== undefined && alignment[0] !== chainId ? ChainState.blockAtProgress(cs, alignment[1] + 0.2) : undefined;
203
203
  let action = ChainState.getNextQuery(cs, chainTargetItems, maxTargetBlock);
204
204
  if (typeof action !== "object") {
205
205
  if (action === "WaitingForNewBlock") {
package/src/Metrics.res CHANGED
@@ -20,6 +20,7 @@ type chainMetrics = {
20
20
  // Raw committed progress (may be -1), unlike the optional latestProcessedBlock.
21
21
  progressBlockNumber: int,
22
22
  progressLatencyMs: option<int>,
23
+ progressBlockTime: option<int>,
23
24
  concurrency: int,
24
25
  partitionsCount: int,
25
26
  bufferSize: int,
@@ -659,6 +660,13 @@ let renderMetrics = (b: builder, metrics: t) => {
659
660
  ~entries=chains,
660
661
  ~value=m => m.numEventsProcessed,
661
662
  )
663
+ b->seriesOpt(
664
+ ~name="envio_progress_block_time_seconds",
665
+ ~help="Unix timestamp of the block the chain has processed up to. Subtract it from the scrape time for how far behind chain time the indexer is, which stays honest when the data source itself is behind the chain. Best effort in realtime mode, absent during backfill.",
666
+ ~kind="gauge",
667
+ ~entries=chains,
668
+ ~value=m => m.progressBlockTime->Option.map(Int.toFloat),
669
+ )
662
670
  b->seriesOpt(
663
671
  ~name="envio_progress_latency",
664
672
  ~help="The latency in milliseconds between the latest processed event creation and the time it was written to storage.",
@@ -221,6 +221,7 @@ function renderMetrics(b, metrics) {
221
221
  single(b, "envio_processing_max_batch_size", "The maximum number of items to process in a single batch.", "gauge", metrics.maxBatchSize);
222
222
  series(b, "envio_progress_block", "The block number of the latest block processed and stored in the database.", "gauge", chains, m => m.progressBlockNumber);
223
223
  series(b, "envio_progress_events", "The number of events processed and reflected in the database.", "gauge", chains, m => m.numEventsProcessed);
224
+ seriesOpt(b, "envio_progress_block_time_seconds", "Unix timestamp of the block the chain has processed up to. Subtract it from the scrape time for how far behind chain time the indexer is, which stays honest when the data source itself is behind the chain. Best effort in realtime mode, absent during backfill.", "gauge", chains, m => Stdlib_Option.map(m.progressBlockTime, prim => prim));
224
225
  seriesOpt(b, "envio_progress_latency", "The latency in milliseconds between the latest processed event creation and the time it was written to storage.", "gauge", chains, m => Stdlib_Option.map(m.progressLatencyMs, prim => prim));
225
226
  let ifCalled = (s, value) => {
226
227
  if (s.callCount > 0 || s.activeCallsCount > 0) {
@@ -24,6 +24,7 @@ type initialChainState = {
24
24
  endBlock: option<int>,
25
25
  maxReorgDepth: int,
26
26
  progressBlockNumber: int,
27
+ progressBlockTime: option<int>,
27
28
  numEventsProcessed: float,
28
29
  firstEventBlockNumber: option<int>,
29
30
  timestampCaughtUpToHeadOrEndblock: option<Date.t>,
package/src/PgStorage.res CHANGED
@@ -1449,6 +1449,7 @@ let rec writeBatch = async (
1449
1449
  ): InternalTable.Chains.progressedChain => {
1450
1450
  chainId: chainAfterBatch.fetchState.chainId,
1451
1451
  progressBlockNumber: chainAfterBatch.progressBlockNumber,
1452
+ progressBlockTime: chainAfterBatch.progressBlockTime,
1452
1453
  sourceBlockNumber: chainAfterBatch.sourceBlockNumber,
1453
1454
  totalEventsProcessed: chainAfterBatch.totalEventsProcessed,
1454
1455
  }),
@@ -1992,6 +1993,7 @@ let make = (
1992
1993
  firstEventBlockNumber: None,
1993
1994
  timestampCaughtUpToHeadOrEndblock: None,
1994
1995
  addressRows: rowsByChain->Array.getUnsafe(idx)->AddressRows.seedRowsOf,
1996
+ progressBlockTime: None,
1995
1997
  sourceBlockNumber: 0,
1996
1998
  }),
1997
1999
  checkpointFrontier: Frontier.empty(),
@@ -2427,6 +2429,7 @@ let make = (
2427
2429
  timestampCaughtUpToHeadOrEndblock: rawInitialState.timestampCaughtUpToHeadOrEndblock->Null.toOption,
2428
2430
  numEventsProcessed: rawInitialState.numEventsProcessed,
2429
2431
  progressBlockNumber: rawInitialState.progressBlockNumber,
2432
+ progressBlockTime: rawInitialState.progressBlockTime->InternalTable.Chains.blockTimeFromDb,
2430
2433
  addressRows: rawInitialState.addressRows,
2431
2434
  sourceBlockNumber: rawInitialState.sourceBlockNumber,
2432
2435
  }),
@@ -914,6 +914,7 @@ async function writeBatch(sql, batch, pgSchema, rollback, config, allEntities, s
914
914
  sql => InternalTable.Chains.setProgressedChains(sql, pgSchema, Utils.Dict.mapValuesToArray(batch.progressedChainsById, chainAfterBatch => ({
915
915
  chainId: chainAfterBatch.fetchState.chainId,
916
916
  progressBlockNumber: chainAfterBatch.progressBlockNumber,
917
+ progressBlockTime: chainAfterBatch.progressBlockTime,
917
918
  sourceBlockNumber: chainAfterBatch.sourceBlockNumber,
918
919
  totalEventsProcessed: chainAfterBatch.totalEventsProcessed
919
920
  }))),
@@ -1208,6 +1209,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
1208
1209
  endBlock: chainConfig.endBlock,
1209
1210
  maxReorgDepth: chainConfig.maxReorgDepth,
1210
1211
  progressBlockNumber: -1,
1212
+ progressBlockTime: undefined,
1211
1213
  numEventsProcessed: 0,
1212
1214
  firstEventBlockNumber: undefined,
1213
1215
  timestampCaughtUpToHeadOrEndblock: undefined,
@@ -1480,6 +1482,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
1480
1482
  endBlock: Primitive_option.fromNull(rawInitialState.endBlock),
1481
1483
  maxReorgDepth: rawInitialState.maxReorgDepth,
1482
1484
  progressBlockNumber: rawInitialState.progressBlockNumber,
1485
+ progressBlockTime: InternalTable.Chains.blockTimeFromDb(rawInitialState.progressBlockTime),
1483
1486
  numEventsProcessed: rawInitialState.numEventsProcessed,
1484
1487
  firstEventBlockNumber: Primitive_option.fromNull(rawInitialState.firstEventBlockNumber),
1485
1488
  timestampCaughtUpToHeadOrEndblock: Primitive_option.fromNull(rawInitialState.timestampCaughtUpToHeadOrEndblock),
package/src/Rollback.res CHANGED
@@ -200,6 +200,7 @@ and executeRollback = async (
200
200
  ->Array.push({
201
201
  chainId,
202
202
  progressBlockNumber: toBlock,
203
+ progressBlockTime: cs->ChainState.committedProgressBlockTime,
203
204
  sourceBlockNumber: cs->ChainState.knownHeight,
204
205
  totalEventsProcessed: cs->ChainState.numEventsProcessed,
205
206
  })
@@ -86,6 +86,7 @@ async function executeRollback(state, reorgChain, rollbackTargetBlockNumber, sch
86
86
  rolledBackChains.push({
87
87
  chainId: chainId,
88
88
  progressBlockNumber: toBlock,
89
+ progressBlockTime: ChainState.committedProgressBlockTime(cs),
89
90
  sourceBlockNumber: ChainState.knownHeight(cs),
90
91
  totalEventsProcessed: ChainState.numEventsProcessed(cs)
91
92
  });