envio 3.5.0-alpha.1 → 3.5.0-alpha.3

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 (99) hide show
  1. package/index.d.ts +10 -6
  2. package/package.json +6 -6
  3. package/src/BatchProcessing.res +27 -0
  4. package/src/BatchProcessing.res.mjs +12 -0
  5. package/src/ChainFetching.res +5 -5
  6. package/src/ChainFetching.res.mjs +3 -4
  7. package/src/ChainState.res +48 -25
  8. package/src/ChainState.res.mjs +23 -29
  9. package/src/ChainState.resi +2 -6
  10. package/src/Change.res +3 -3
  11. package/src/Config.res +2 -6
  12. package/src/Config.res.mjs +2 -12
  13. package/src/Core.res +3 -0
  14. package/src/CrossChainState.res +33 -10
  15. package/src/CrossChainState.res.mjs +21 -6
  16. package/src/CrossChainState.resi +3 -0
  17. package/src/EntityId.res +15 -0
  18. package/src/EntityId.res.mjs +9 -0
  19. package/src/EventConfigBuilder.res +1 -58
  20. package/src/EventConfigBuilder.res.mjs +1 -33
  21. package/src/FetchState.res +510 -460
  22. package/src/FetchState.res.mjs +413 -428
  23. package/src/FinalizeBackfill.res +34 -0
  24. package/src/FinalizeBackfill.res.mjs +26 -0
  25. package/src/HandlerRegister.res +45 -0
  26. package/src/HandlerRegister.res.mjs +43 -0
  27. package/src/InMemoryStore.res +11 -4
  28. package/src/InMemoryTable.res +26 -24
  29. package/src/InMemoryTable.res.mjs +19 -18
  30. package/src/IndexerState.res +71 -2
  31. package/src/IndexerState.res.mjs +67 -3
  32. package/src/IndexerState.resi +5 -0
  33. package/src/Internal.res +17 -8
  34. package/src/LoadLayer.res +9 -1
  35. package/src/LoadLayer.res.mjs +1 -0
  36. package/src/LogSelection.res +2 -1
  37. package/src/Metrics.res +37 -6
  38. package/src/Metrics.res.mjs +5 -1
  39. package/src/Persistence.res +12 -1
  40. package/src/PgStorage.res +411 -57
  41. package/src/PgStorage.res.mjs +262 -41
  42. package/src/SimulateItems.res +2 -2
  43. package/src/TestIndexer.res +9 -5
  44. package/src/TestIndexer.res.mjs +5 -2
  45. package/src/UserContext.res +3 -3
  46. package/src/Writing.res +12 -2
  47. package/src/Writing.res.mjs +13 -2
  48. package/src/bindings/ClickHouse.res +5 -3
  49. package/src/bindings/ClickHouse.res.mjs +2 -5
  50. package/src/bindings/Vitest.res +22 -1
  51. package/src/bindings/Vitest.res.mjs +15 -0
  52. package/src/db/EntityFilter.res +0 -4
  53. package/src/db/EntityFilter.res.mjs +1 -8
  54. package/src/db/EntityHistory.res +16 -7
  55. package/src/db/EntityHistory.res.mjs +7 -6
  56. package/src/db/IndexRegistry.res +219 -0
  57. package/src/db/IndexRegistry.res.mjs +195 -0
  58. package/src/db/InternalTable.res +7 -0
  59. package/src/db/InternalTable.res.mjs +7 -0
  60. package/src/db/Table.res +53 -22
  61. package/src/db/Table.res.mjs +56 -18
  62. package/src/sources/AddressSet.res +48 -0
  63. package/src/sources/AddressSet.res.mjs +11 -0
  64. package/src/sources/AddressStore.res +152 -0
  65. package/src/sources/AddressStore.res.mjs +97 -0
  66. package/src/sources/EvmChain.res +3 -0
  67. package/src/sources/EvmChain.res.mjs +4 -2
  68. package/src/sources/EvmHyperSyncSource.res +6 -3
  69. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  70. package/src/sources/EvmRpcClient.res +6 -3
  71. package/src/sources/EvmRpcClient.res.mjs +3 -3
  72. package/src/sources/FuelHyperSync.res +4 -3
  73. package/src/sources/FuelHyperSync.res.mjs +3 -3
  74. package/src/sources/FuelHyperSync.resi +2 -1
  75. package/src/sources/FuelHyperSyncClient.res +12 -6
  76. package/src/sources/FuelHyperSyncClient.res.mjs +2 -2
  77. package/src/sources/FuelHyperSyncSource.res +7 -4
  78. package/src/sources/FuelHyperSyncSource.res.mjs +3 -3
  79. package/src/sources/HyperSync.res +6 -4
  80. package/src/sources/HyperSync.res.mjs +2 -3
  81. package/src/sources/HyperSync.resi +1 -1
  82. package/src/sources/HyperSyncClient.res +18 -9
  83. package/src/sources/HyperSyncClient.res.mjs +4 -4
  84. package/src/sources/RpcSource.res +15 -10
  85. package/src/sources/RpcSource.res.mjs +3 -4
  86. package/src/sources/SimulateSource.res +26 -11
  87. package/src/sources/SimulateSource.res.mjs +13 -5
  88. package/src/sources/Source.res +4 -2
  89. package/src/sources/SourceManager.res +2 -3
  90. package/src/sources/SourceManager.res.mjs +2 -3
  91. package/src/sources/Svm.res +1 -2
  92. package/src/sources/Svm.res.mjs +1 -1
  93. package/src/sources/SvmHyperSyncClient.res +12 -3
  94. package/src/sources/SvmHyperSyncClient.res.mjs +2 -2
  95. package/src/sources/SvmHyperSyncSource.res +10 -5
  96. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  97. package/src/IndexingAddresses.res +0 -151
  98. package/src/IndexingAddresses.res.mjs +0 -130
  99. package/src/IndexingAddresses.resi +0 -39
@@ -107,6 +107,10 @@ type t = {
107
107
  crossChainState: CrossChainState.t,
108
108
  mutable rollbackState: rollbackState,
109
109
  indexerStartTime: Date.t,
110
+ // Monotonic peer of indexerStartTime. Elapsed run time is derived from this so
111
+ // an NTP correction can't move it out of step with the Performance-based
112
+ // counters that get divided by it.
113
+ indexerStartTimeRef: Performance.timeRef,
110
114
  // When an entity's history was last pruned. No key = never pruned yet,
111
115
  // which counts as overdue.
112
116
  lastPrunedAtMillis: dict<float>,
@@ -133,6 +137,12 @@ type t = {
133
137
  // --- Metric counters, rendered by Metrics at scrape time. ---
134
138
  mutable preloadSeconds: float,
135
139
  mutable processingSeconds: float,
140
+ mutable processingStalledOnFetchSeconds: float,
141
+ // Set while the processing loop is idle after starving on an empty buffer;
142
+ // None while processing or when the loop stopped for a reorg/shutdown. The
143
+ // getter folds the in-progress interval in so a scrape mid-stall isn't stale.
144
+ mutable processingStalledOnFetchSince: option<Performance.timeRef>,
145
+ mutable processingStalledOnStorageWriteSeconds: float,
136
146
  handlerStats: dict<handlerStat>,
137
147
  storageLoadStats: dict<storageLoadStat>,
138
148
  storageWriteStats: dict<storageWriteStat>,
@@ -194,6 +204,7 @@ let make = (
194
204
  ~targetBufferSize,
195
205
  ),
196
206
  indexerStartTime: Date.make(),
207
+ indexerStartTimeRef: Performance.now(),
197
208
  rollbackState: NoRollback,
198
209
  lastPrunedAtMillis: Dict.make(),
199
210
  loadManager: LoadManager.make(),
@@ -206,6 +217,9 @@ let make = (
206
217
  simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
207
218
  preloadSeconds: 0.,
208
219
  processingSeconds: 0.,
220
+ processingStalledOnFetchSeconds: 0.,
221
+ processingStalledOnFetchSince: None,
222
+ processingStalledOnStorageWriteSeconds: 0.,
209
223
  handlerStats: Dict.make(),
210
224
  storageLoadStats: Dict.make(),
211
225
  storageWriteStats: Dict.make(),
@@ -315,8 +329,22 @@ let isResolvingReorg = (state: t) =>
315
329
  // reports the first error so redundant handlers (eg an error caught in two
316
330
  // nested scopes) don't double-report.
317
331
  @inline
332
+ let // Close an open fetch-stall interval, accruing it into the counter. Called
333
+ // whenever the reason for the idle changes, so the interval never spans into
334
+ // time another counter owns — or, at shutdown, past the point where the loops
335
+ // stop and nothing would ever close it.
336
+ settleStalledOnFetch = (state: t) =>
337
+ switch state.processingStalledOnFetchSince {
338
+ | Some(since) =>
339
+ state.processingStalledOnFetchSeconds =
340
+ state.processingStalledOnFetchSeconds +. since->Performance.secondsSince
341
+ state.processingStalledOnFetchSince = None
342
+ | None => ()
343
+ }
344
+
318
345
  let errorExit = (state: t, errHandler) =>
319
346
  if !state.isStopped {
347
+ state->settleStalledOnFetch
320
348
  state.isStopped = true
321
349
  state.onError(errHandler)
322
350
  }
@@ -325,7 +353,10 @@ let unexpectedErrorMsg = "Indexer has failed with an unexpected error"
325
353
 
326
354
  // Halt the loops without reporting an error, eg to hand the shared db over to a
327
355
  // resumed indexer in tests.
328
- let stop = (state: t) => state.isStopped = true
356
+ let stop = (state: t) => {
357
+ state->settleStalledOnFetch
358
+ state.isStopped = true
359
+ }
329
360
 
330
361
  let getChainState = (state: t, ~chain: chain): ChainState.t =>
331
362
  switch state.crossChainState
@@ -360,6 +391,9 @@ let enterReorgThreshold = (state: t) => state.crossChainState->CrossChainState.e
360
391
  // caller has already mutated the chain states (restored counters, reset pending
361
392
  // queries). isResolvingReorg derives from rollbackState.
362
393
  let beginReorg = (state: t, ~chain, ~blockNumber) => {
394
+ // Settle here, or the rollback that follows would be folded into the stall on
395
+ // the next beginProcessing — time envio_rollback_seconds already counts.
396
+ state->settleStalledOnFetch
363
397
  state.epoch = state.epoch + 1
364
398
  state.rollbackState = ReorgDetected({chain, blockNumber})
365
399
  }
@@ -391,9 +425,24 @@ let applyBatchProgress = (state: t, ~batch: Batch.t) =>
391
425
  // Processing-loop mutex. Guards ProcessEventBatch re-entry so only one
392
426
  // processing loop runs at a time.
393
427
  let isProcessing = (state: t) => state.isProcessing
394
- let beginProcessing = (state: t) => state.isProcessing = true
428
+ let beginProcessing = (state: t) => {
429
+ state->settleStalledOnFetch
430
+ state.isProcessing = true
431
+ }
395
432
  let endProcessing = (state: t) => state.isProcessing = false
396
433
 
434
+ // Start attributing idle time to fetch starvation. Called when the processing
435
+ // loop exits with no work left; skipped when it exits for a reorg or shutdown,
436
+ // so only genuine buffer starvation is counted.
437
+ let markProcessingStalledOnFetch = (state: t) =>
438
+ if state.processingStalledOnFetchSince->Option.isNone {
439
+ state.processingStalledOnFetchSince = Some(Performance.now())
440
+ }
441
+
442
+ let recordStalledOnStorageWrite = (state: t, ~seconds) =>
443
+ state.processingStalledOnStorageWriteSeconds =
444
+ state.processingStalledOnStorageWriteSeconds +. seconds
445
+
397
446
  let recordProcessedBatch = (state: t) =>
398
447
  state.processedBatchesCount = state.processedBatchesCount + 1
399
448
 
@@ -419,6 +468,18 @@ let crossChainState = (state: t) => state.crossChainState
419
468
  let chainStates = (state: t) => state.crossChainState->CrossChainState.chainStates
420
469
  let isInReorgThreshold = (state: t) => state.crossChainState->CrossChainState.isInReorgThreshold
421
470
  let isRealtime = (state: t) => state.crossChainState->CrossChainState.isRealtime
471
+
472
+ // The indexer runs Backfilling → FinalizingIndexes → Ready. This is true only
473
+ // in the middle phase: every chain has caught up, but the deferred schema
474
+ // indexes and `ready_at` haven't been committed yet.
475
+ let isFinalizingIndexes = (state: t) =>
476
+ state.crossChainState->CrossChainState.isCaughtUp &&
477
+ !(state.crossChainState->CrossChainState.isRealtime)
478
+
479
+ let markCaughtUp = (state: t) => state.crossChainState->CrossChainState.markCaughtUp
480
+
481
+ let markReady = (state: t, ~readyAt) => state.crossChainState->CrossChainState.markReady(~readyAt)
482
+
422
483
  let rollbackState = (state: t) => state.rollbackState
423
484
  let indexerStartTime = (state: t) => state.indexerStartTime
424
485
  let loadManager = (state: t) => state.loadManager
@@ -469,12 +530,20 @@ let toMetrics = (state: t): Metrics.t => {
469
530
  )
470
531
  {
471
532
  startTime: state.indexerStartTime,
533
+ metricTime: Date.make(),
534
+ elapsedSeconds: state.indexerStartTimeRef->Performance.secondsSince,
472
535
  targetBufferSize: state.crossChainState->CrossChainState.targetBufferSize,
473
536
  isInReorgThreshold: state.crossChainState->CrossChainState.isInReorgThreshold,
474
537
  rollbackEnabled: state.config.shouldRollbackOnReorg,
475
538
  maxBatchSize: state.config.batchSize,
476
539
  preloadSeconds: state.preloadSeconds,
477
540
  processingSeconds: state.processingSeconds,
541
+ processingStalledOnFetchSeconds: state.processingStalledOnFetchSeconds +.
542
+ switch state.processingStalledOnFetchSince {
543
+ | Some(since) => since->Performance.secondsSince
544
+ | None => 0.
545
+ },
546
+ processingStalledOnStorageWriteSeconds: state.processingStalledOnStorageWriteSeconds,
478
547
  rollbackSeconds: state.rollbackSeconds,
479
548
  rollbackCount: state.rollbackCount,
480
549
  rollbackEventsCount: state.rollbackEventsCount,
@@ -76,6 +76,7 @@ function make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime
76
76
  crossChainState: CrossChainState.make(chainStates, isInReorgThreshold, isRealtime, targetBufferSize),
77
77
  rollbackState: "NoRollback",
78
78
  indexerStartTime: new Date(),
79
+ indexerStartTimeRef: Performance.now(),
79
80
  lastPrunedAtMillis: {},
80
81
  loadManager: LoadManager.make(),
81
82
  keepProcessAlive: isDevelopmentMode || shouldUseTui,
@@ -87,6 +88,9 @@ function make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime
87
88
  simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
88
89
  preloadSeconds: 0,
89
90
  processingSeconds: 0,
91
+ processingStalledOnFetchSeconds: 0,
92
+ processingStalledOnFetchSince: undefined,
93
+ processingStalledOnStorageWriteSeconds: 0,
90
94
  handlerStats: {},
91
95
  storageLoadStats: {},
92
96
  storageWriteStats: {},
@@ -145,13 +149,24 @@ function isResolvingReorg(state) {
145
149
  }
146
150
 
147
151
  function errorExit(state, errHandler) {
148
- if (!state.isStopped) {
149
- state.isStopped = true;
150
- return state.onError(errHandler);
152
+ if (state.isStopped) {
153
+ return;
154
+ }
155
+ let since = state.processingStalledOnFetchSince;
156
+ if (since !== undefined) {
157
+ state.processingStalledOnFetchSeconds = state.processingStalledOnFetchSeconds + Performance.secondsSince(since);
158
+ state.processingStalledOnFetchSince = undefined;
151
159
  }
160
+ state.isStopped = true;
161
+ state.onError(errHandler);
152
162
  }
153
163
 
154
164
  function stop(state) {
165
+ let since = state.processingStalledOnFetchSince;
166
+ if (since !== undefined) {
167
+ state.processingStalledOnFetchSeconds = state.processingStalledOnFetchSeconds + Performance.secondsSince(since);
168
+ state.processingStalledOnFetchSince = undefined;
169
+ }
155
170
  state.isStopped = true;
156
171
  }
157
172
 
@@ -177,6 +192,11 @@ function enterReorgThreshold(state) {
177
192
  }
178
193
 
179
194
  function beginReorg(state, chain, blockNumber) {
195
+ let since = state.processingStalledOnFetchSince;
196
+ if (since !== undefined) {
197
+ state.processingStalledOnFetchSeconds = state.processingStalledOnFetchSeconds + Performance.secondsSince(since);
198
+ state.processingStalledOnFetchSince = undefined;
199
+ }
180
200
  state.epoch = state.epoch + 1 | 0;
181
201
  state.rollbackState = {
182
202
  TAG: "ReorgDetected",
@@ -221,6 +241,11 @@ function isProcessing(state) {
221
241
  }
222
242
 
223
243
  function beginProcessing(state) {
244
+ let since = state.processingStalledOnFetchSince;
245
+ if (since !== undefined) {
246
+ state.processingStalledOnFetchSeconds = state.processingStalledOnFetchSeconds + Performance.secondsSince(since);
247
+ state.processingStalledOnFetchSince = undefined;
248
+ }
224
249
  state.isProcessing = true;
225
250
  }
226
251
 
@@ -228,6 +253,17 @@ function endProcessing(state) {
228
253
  state.isProcessing = false;
229
254
  }
230
255
 
256
+ function markProcessingStalledOnFetch(state) {
257
+ if (Stdlib_Option.isNone(state.processingStalledOnFetchSince)) {
258
+ state.processingStalledOnFetchSince = Performance.now();
259
+ return;
260
+ }
261
+ }
262
+
263
+ function recordStalledOnStorageWrite(state, seconds) {
264
+ state.processingStalledOnStorageWriteSeconds = state.processingStalledOnStorageWriteSeconds + seconds;
265
+ }
266
+
231
267
  function recordProcessedBatch(state) {
232
268
  state.processedBatchesCount = state.processedBatchesCount + 1 | 0;
233
269
  }
@@ -300,6 +336,22 @@ function isRealtime(state) {
300
336
  return CrossChainState.isRealtime(state.crossChainState);
301
337
  }
302
338
 
339
+ function isFinalizingIndexes(state) {
340
+ if (CrossChainState.isCaughtUp(state.crossChainState)) {
341
+ return !CrossChainState.isRealtime(state.crossChainState);
342
+ } else {
343
+ return false;
344
+ }
345
+ }
346
+
347
+ function markCaughtUp(state) {
348
+ CrossChainState.markCaughtUp(state.crossChainState);
349
+ }
350
+
351
+ function markReady(state, readyAt) {
352
+ CrossChainState.markReady(state.crossChainState, readyAt);
353
+ }
354
+
303
355
  function rollbackState(state) {
304
356
  return state.rollbackState;
305
357
  }
@@ -371,14 +423,21 @@ function toMetrics(state) {
371
423
  count: s.count
372
424
  });
373
425
  });
426
+ let since = state.processingStalledOnFetchSince;
374
427
  return {
375
428
  startTime: state.indexerStartTime,
429
+ metricTime: new Date(),
430
+ elapsedSeconds: Performance.secondsSince(state.indexerStartTimeRef),
376
431
  targetBufferSize: CrossChainState.targetBufferSize(state.crossChainState),
377
432
  isInReorgThreshold: CrossChainState.isInReorgThreshold(state.crossChainState),
378
433
  rollbackEnabled: state.config.shouldRollbackOnReorg,
379
434
  maxBatchSize: state.config.batchSize,
380
435
  preloadSeconds: state.preloadSeconds,
381
436
  processingSeconds: state.processingSeconds,
437
+ processingStalledOnFetchSeconds: state.processingStalledOnFetchSeconds + (
438
+ since !== undefined ? Performance.secondsSince(since) : 0
439
+ ),
440
+ processingStalledOnStorageWriteSeconds: state.processingStalledOnStorageWriteSeconds,
382
441
  rollbackSeconds: state.rollbackSeconds,
383
442
  rollbackCount: state.rollbackCount,
384
443
  rollbackEventsCount: state.rollbackEventsCount,
@@ -698,6 +757,7 @@ export {
698
757
  isProcessing,
699
758
  beginProcessing,
700
759
  endProcessing,
760
+ markProcessingStalledOnFetch,
701
761
  recordProcessedBatch,
702
762
  createBatch,
703
763
  getSafeCheckpointId,
@@ -718,6 +778,9 @@ export {
718
778
  chainStates,
719
779
  isInReorgThreshold,
720
780
  isRealtime,
781
+ isFinalizingIndexes,
782
+ markCaughtUp,
783
+ markReady,
721
784
  rollbackState,
722
785
  indexerStartTime,
723
786
  loadManager,
@@ -730,6 +793,7 @@ export {
730
793
  simulateDeadInputTracker,
731
794
  toMetrics,
732
795
  recordBatchDurations,
796
+ recordStalledOnStorageWrite,
733
797
  recordHandlerDuration,
734
798
  startPreloadHandler,
735
799
  endPreloadHandler,
@@ -66,6 +66,7 @@ let applyBatchProgress: (t, ~batch: Batch.t) => unit
66
66
  let isProcessing: t => bool
67
67
  let beginProcessing: t => unit
68
68
  let endProcessing: t => unit
69
+ let markProcessingStalledOnFetch: t => unit
69
70
  let recordProcessedBatch: t => unit
70
71
  let createBatch: (
71
72
  t,
@@ -93,6 +94,9 @@ let crossChainState: t => CrossChainState.t
93
94
  let chainStates: t => dict<ChainState.t>
94
95
  let isInReorgThreshold: t => bool
95
96
  let isRealtime: t => bool
97
+ let isFinalizingIndexes: t => bool
98
+ let markCaughtUp: t => unit
99
+ let markReady: (t, ~readyAt: Date.t) => unit
96
100
  let rollbackState: t => rollbackState
97
101
  let indexerStartTime: t => Date.t
98
102
  let loadManager: t => LoadManager.t
@@ -110,6 +114,7 @@ let toMetrics: t => Metrics.t
110
114
 
111
115
  // Metric counters.
112
116
  let recordBatchDurations: (t, ~loadDuration: float, ~handlerDuration: float) => unit
117
+ let recordStalledOnStorageWrite: (t, ~seconds: float) => unit
113
118
  let recordHandlerDuration: (t, ~contract: string, ~event: string, ~duration: float) => unit
114
119
  let startPreloadHandler: (t, ~contract: string, ~event: string) => Performance.timeRef
115
120
  let endPreloadHandler: (t, Performance.timeRef, ~contract: string, ~event: string) => unit
package/src/Internal.res CHANGED
@@ -333,6 +333,11 @@ type eventPayload
333
333
  // The log's emitting address (EVM/Fuel; the program id carries it for SVM).
334
334
  @get external getPayloadSrcAddress: eventPayload => Address.t = "srcAddress"
335
335
 
336
+ // The decoded params, read by name for the address-valued ones a `where`
337
+ // filters on. Only those names are ever looked up, so the address type is
338
+ // accurate at every use site.
339
+ @get external getPayloadAddressParams: eventPayload => dict<Address.t> = "params"
340
+
336
341
  type genericLoaderArgs<'event, 'context> = {
337
342
  event: 'event,
338
343
  context: 'context,
@@ -356,11 +361,11 @@ type genericHandlerArgs<'event, 'context> = {
356
361
  type genericHandler<'args> = 'args => promise<unit>
357
362
 
358
363
  type entityHandlerContext<'entity> = {
359
- get: string => promise<option<'entity>>,
360
- getOrThrow: (string, ~message: string=?) => promise<'entity>,
364
+ get: EntityId.t => promise<option<'entity>>,
365
+ getOrThrow: (EntityId.t, ~message: string=?) => promise<'entity>,
361
366
  getOrCreate: 'entity => promise<'entity>,
362
367
  set: 'entity => unit,
363
- deleteUnsafe: string => unit,
368
+ deleteUnsafe: EntityId.t => unit,
364
369
  }
365
370
 
366
371
  type chainInfo = {
@@ -572,11 +577,15 @@ type onEventRegistration = {
572
577
  // Usually always false for wildcard events, but might be true for a wildcard
573
578
  // event with a dynamic event filter by addresses.
574
579
  dependsOnAddresses: bool,
575
- // Precompiled predicate for events that filter an indexed address param by
576
- // registered addresses (see `EventConfigBuilder.buildAddressFilter`); drops a
577
- // decoded event whose param-address isn't registered at/before the log's
578
- // block. Absent otherwise.
579
- clientAddressFilter?: (eventPayload, int, dict<indexingContract>) => bool,
580
+ // Indexed address params this event filters on, in disjunctive normal form
581
+ // (OR of AND-groups), from `where: {params: {to: chain.C.addresses}}`. Every
582
+ // source applies this natively while routing; it's carried here for the
583
+ // simulate source, which has no native query boundary. Absent otherwise.
584
+ //
585
+ // Keep it optional: with every field required, ReScript compiles a
586
+ // `{...registration, ...}` spread into an explicit field-by-field copy, which
587
+ // drops the ecosystem-only fields an `evmOnEventRegistration` carries.
588
+ addressFilterParamGroups?: array<array<string>>,
580
589
  // Final start block: the contract/chain config value, overridden by a
581
590
  // `where.block.number._gte` when the registered `where` supplies one.
582
591
  startBlock: option<int>,
package/src/LoadLayer.res CHANGED
@@ -338,6 +338,7 @@ let loadByFilter = (
338
338
 
339
339
  let load = async (filters: array<EntityFilter.t>, ~onError as _) => {
340
340
  let storage = persistence->Persistence.getInitializedStorageOrThrow
341
+
341
342
  let timerRef =
342
343
  indexerState->IndexerState.startStorageLoad(~storage=storage.name, ~operation=key)
343
344
 
@@ -345,8 +346,15 @@ let loadByFilter = (
345
346
 
346
347
  filters->Array.forEach(filter => inMemTable->InMemoryTable.Entity.addEmptyIndex(~filter))
347
348
 
349
+ // Any non-derived field can be filtered on, so the columns this query reads
350
+ // are indexed on demand before it runs rather than promised by the schema.
351
+ // Inside the load timing: waiting on the build is time the handler spends
352
+ // waiting for this operation, and it's the only thing that explains an
353
+ // occasional very slow getWhere.
354
+ await storage.ensureQueryIndexes(~table=entityConfig.table, ~filters)
355
+
348
356
  // Loading a superset of rows via a merged query is safe: every loaded
349
- // entity is matched against all registered indices, not only the
357
+ // entity is matched against all registered indexes, not only the
350
358
  // query's own filter.
351
359
  let queries = filters->EntityFilter.merge
352
360
 
@@ -190,6 +190,7 @@ function loadByFilter(loadManager, persistence, entityConfig, indexerState, shou
190
190
  contents: 0
191
191
  };
192
192
  filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter));
193
+ await storage.ensureQueryIndexes(entityConfig.table, filters);
193
194
  let queries = EntityFilter.merge(filters);
194
195
  await Promise.all(queries.map(async filter => {
195
196
  try {
@@ -23,7 +23,8 @@ type parsedWhere = {
23
23
  filterByAddresses: bool,
24
24
  // Indexed params filtered by `chain.<Contract>.addresses`, in disjunctive
25
25
  // normal form (outer array OR of AND-groups). Empty unless `filterByAddresses`.
26
- // Consumed by the codegen of the event's `clientAddressFilter`.
26
+ // Applied natively by every source while routing; carried on the registration
27
+ // for the simulate source, which has no native query boundary.
27
28
  addressFilterParamGroups: array<array<string>>,
28
29
  }
29
30
 
package/src/Metrics.res CHANGED
@@ -98,12 +98,19 @@ type sourceHeightMetrics = {
98
98
 
99
99
  type t = {
100
100
  startTime: Date.t,
101
+ // Wall clock when this snapshot was built, so a scrape can be dated.
102
+ metricTime: Date.t,
103
+ // Measured monotonically, not as metricTime - startTime: a wall-clock
104
+ // correction would otherwise skew the counter shares derived from it.
105
+ elapsedSeconds: float,
101
106
  targetBufferSize: int,
102
107
  isInReorgThreshold: bool,
103
108
  rollbackEnabled: bool,
104
109
  maxBatchSize: int,
105
110
  preloadSeconds: float,
106
111
  processingSeconds: float,
112
+ processingStalledOnFetchSeconds: float,
113
+ processingStalledOnStorageWriteSeconds: float,
107
114
  rollbackSeconds: float,
108
115
  rollbackCount: int,
109
116
  rollbackEventsCount: float,
@@ -241,6 +248,24 @@ let renderMetrics = (b: builder, metrics: t) => {
241
248
  byLabels->Dict.toArray
242
249
  }
243
250
 
251
+ b->single(
252
+ ~name="envio_process_start_time_seconds",
253
+ ~help="Start time of the process since unix epoch in seconds.",
254
+ ~kind="gauge",
255
+ ~value=metrics.startTime->Date.getTime /. 1000.,
256
+ )
257
+ b->single(
258
+ ~name="envio_process_metric_time_seconds",
259
+ ~help="The time these metrics were collected. Use it to tell how fresh a snapshot is, or to measure rates between two snapshots.",
260
+ ~kind="gauge",
261
+ ~value=metrics.metricTime->Date.getTime /. 1000.,
262
+ )
263
+ b->single(
264
+ ~name="envio_process_elapsed_seconds",
265
+ ~help="How long the indexer has been running. Divide a cumulative seconds metric by this to get the share of the run it took, eg envio_processing_seconds for time spent in event handlers.",
266
+ ~kind="gauge",
267
+ ~value=metrics.elapsedSeconds,
268
+ )
244
269
  b->single(
245
270
  ~name="envio_preload_seconds",
246
271
  ~help="Cumulative time spent on preloading entities during batch processing.",
@@ -253,6 +278,18 @@ let renderMetrics = (b: builder, metrics: t) => {
253
278
  ~kind="counter",
254
279
  ~value=metrics.processingSeconds,
255
280
  )
281
+ b->single(
282
+ ~name="envio_processing_stalled_on_fetch_seconds",
283
+ ~help="Time the indexer had nothing to process while waiting for events to be fetched. A high rate means fetching is the bottleneck: check the data-source latency and whether it can be queried with more concurrency. Waiting at the chain head for new blocks is not counted.",
284
+ ~kind="counter",
285
+ ~value=metrics.processingStalledOnFetchSeconds,
286
+ )
287
+ b->single(
288
+ ~name="envio_processing_stalled_on_storage_write_seconds",
289
+ ~help="Time the indexer paused processing because too many changes were still waiting to be written. A high rate means storage writes are the bottleneck: check envio_storage_write_seconds and the database performance.",
290
+ ~kind="counter",
291
+ ~value=metrics.processingStalledOnStorageWriteSeconds,
292
+ )
256
293
  b->series(
257
294
  ~name="envio_progress_ready",
258
295
  ~help="Whether the chain is fully synced to the head.",
@@ -346,12 +383,6 @@ let renderMetrics = (b: builder, metrics: t) => {
346
383
  ~entries=chains,
347
384
  ~value=m => m.sourceBlockNumber->Int.toFloat,
348
385
  )
349
- b->single(
350
- ~name="envio_process_start_time_seconds",
351
- ~help="Start time of the process since unix epoch in seconds.",
352
- ~kind="gauge",
353
- ~value=metrics.startTime->Date.getTime /. 1000.,
354
- )
355
386
  b->series(
356
387
  ~name="envio_indexing_concurrency",
357
388
  ~help="The number of executing concurrent queries to the chain data-source.",
@@ -108,8 +108,13 @@ function renderMetrics(b, metrics) {
108
108
  }
109
109
  });
110
110
  let sources = Object.entries(byLabels$1);
111
+ single(b, "envio_process_start_time_seconds", "Start time of the process since unix epoch in seconds.", "gauge", metrics.startTime.getTime() / 1000);
112
+ single(b, "envio_process_metric_time_seconds", "The time these metrics were collected. Use it to tell how fresh a snapshot is, or to measure rates between two snapshots.", "gauge", metrics.metricTime.getTime() / 1000);
113
+ single(b, "envio_process_elapsed_seconds", "How long the indexer has been running. Divide a cumulative seconds metric by this to get the share of the run it took, eg envio_processing_seconds for time spent in event handlers.", "gauge", metrics.elapsedSeconds);
111
114
  single(b, "envio_preload_seconds", "Cumulative time spent on preloading entities during batch processing.", "counter", metrics.preloadSeconds);
112
115
  single(b, "envio_processing_seconds", "Cumulative time spent executing event handlers during batch processing.", "counter", metrics.processingSeconds);
116
+ single(b, "envio_processing_stalled_on_fetch_seconds", "Time the indexer had nothing to process while waiting for events to be fetched. A high rate means fetching is the bottleneck: check the data-source latency and whether it can be queried with more concurrency. Waiting at the chain head for new blocks is not counted.", "counter", metrics.processingStalledOnFetchSeconds);
117
+ single(b, "envio_processing_stalled_on_storage_write_seconds", "Time the indexer paused processing because too many changes were still waiting to be written. A high rate means storage writes are the bottleneck: check envio_storage_write_seconds and the database performance.", "counter", metrics.processingStalledOnStorageWriteSeconds);
113
118
  series(b, "envio_progress_ready", "Whether the chain is fully synced to the head.", "gauge", chains, m => {
114
119
  if (m.isReady) {
115
120
  return 1;
@@ -129,7 +134,6 @@ function renderMetrics(b, metrics) {
129
134
  series(b, "envio_fetching_block_range_events_total", "Cumulative number of events fetched across all block range operations.", "counter", chains, m => m.blockRangeFetchedEvents);
130
135
  series(b, "envio_fetching_block_range_size", "Cumulative number of blocks covered across all block range fetch operations.", "counter", chains, m => m.blockRangeFetchedBlocks);
131
136
  series(b, "envio_indexing_known_height", "The latest known block number reported by the active indexing source. This value may lag behind the actual chain height, as it is updated only when needed.", "gauge", chains, m => m.sourceBlockNumber);
132
- single(b, "envio_process_start_time_seconds", "Start time of the process since unix epoch in seconds.", "gauge", metrics.startTime.getTime() / 1000);
133
137
  series(b, "envio_indexing_concurrency", "The number of executing concurrent queries to the chain data-source.", "gauge", chains, m => m.concurrency);
134
138
  series(b, "envio_indexing_partitions", "The number of partitions used to split fetching logic by addresses and block ranges.", "gauge", chains, m => m.partitionsCount);
135
139
  series(b, "envio_indexing_idle_seconds", "The time the indexer source syncing has been idle. A high value may indicate the source sync is a bottleneck.", "counter", chains, m => m.idleSeconds);
@@ -89,6 +89,17 @@ type storage = {
89
89
  // Field values are serialized and rows parsed with the table's field schemas.
90
90
  @raises("StorageError")
91
91
  loadOrThrow: (~filter: EntityFilter.t, ~table: Table.table) => promise<array<unknown>>,
92
+ // Creates whatever indexes the filters need and aren't there yet, resolving
93
+ // once they're queryable. Best-effort: it resolves even when a build fails,
94
+ // leaving the query to run unindexed rather than failing the handler.
95
+ ensureQueryIndexes: (~table: Table.table, ~filters: array<EntityFilter.t>) => promise<unit>,
96
+ // Creates every schema-defined index still missing and stamps `ready_at` on
97
+ // the given chains, atomically. Called once, when backfill completes.
98
+ finalizeBackfill: (
99
+ ~entities: array<Internal.entityConfig>,
100
+ ~chainIds: array<int>,
101
+ ~readyAt: Date.t,
102
+ ) => promise<unit>,
92
103
  // This is to download cache from the database to .envio/cache
93
104
  dumpEffectCache: unit => promise<unit>,
94
105
  reset: unit => promise<unit>,
@@ -121,7 +132,7 @@ type storage = {
121
132
  getRollbackData: (
122
133
  ~entityConfig: Internal.entityConfig,
123
134
  ~rollbackTargetCheckpointId: Internal.checkpointId,
124
- ) => promise<(array<string>, array<unknown>)>,
135
+ ) => promise<(array<EntityId.t>, array<unknown>)>,
125
136
  // Write batch to storage
126
137
  writeBatch: (
127
138
  ~batch: Batch.t,