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.
- package/index.d.ts +10 -6
- package/package.json +6 -6
- package/src/BatchProcessing.res +27 -0
- package/src/BatchProcessing.res.mjs +12 -0
- package/src/ChainFetching.res +5 -5
- package/src/ChainFetching.res.mjs +3 -4
- package/src/ChainState.res +48 -25
- package/src/ChainState.res.mjs +23 -29
- package/src/ChainState.resi +2 -6
- package/src/Change.res +3 -3
- package/src/Config.res +2 -6
- package/src/Config.res.mjs +2 -12
- package/src/Core.res +3 -0
- package/src/CrossChainState.res +33 -10
- package/src/CrossChainState.res.mjs +21 -6
- package/src/CrossChainState.resi +3 -0
- package/src/EntityId.res +15 -0
- package/src/EntityId.res.mjs +9 -0
- package/src/EventConfigBuilder.res +1 -58
- package/src/EventConfigBuilder.res.mjs +1 -33
- package/src/FetchState.res +510 -460
- package/src/FetchState.res.mjs +413 -428
- package/src/FinalizeBackfill.res +34 -0
- package/src/FinalizeBackfill.res.mjs +26 -0
- package/src/HandlerRegister.res +45 -0
- package/src/HandlerRegister.res.mjs +43 -0
- package/src/InMemoryStore.res +11 -4
- package/src/InMemoryTable.res +26 -24
- package/src/InMemoryTable.res.mjs +19 -18
- package/src/IndexerState.res +71 -2
- package/src/IndexerState.res.mjs +67 -3
- package/src/IndexerState.resi +5 -0
- package/src/Internal.res +17 -8
- package/src/LoadLayer.res +9 -1
- package/src/LoadLayer.res.mjs +1 -0
- package/src/LogSelection.res +2 -1
- package/src/Metrics.res +37 -6
- package/src/Metrics.res.mjs +5 -1
- package/src/Persistence.res +12 -1
- package/src/PgStorage.res +411 -57
- package/src/PgStorage.res.mjs +262 -41
- package/src/SimulateItems.res +2 -2
- package/src/TestIndexer.res +9 -5
- package/src/TestIndexer.res.mjs +5 -2
- package/src/UserContext.res +3 -3
- package/src/Writing.res +12 -2
- package/src/Writing.res.mjs +13 -2
- package/src/bindings/ClickHouse.res +5 -3
- package/src/bindings/ClickHouse.res.mjs +2 -5
- package/src/bindings/Vitest.res +22 -1
- package/src/bindings/Vitest.res.mjs +15 -0
- package/src/db/EntityFilter.res +0 -4
- package/src/db/EntityFilter.res.mjs +1 -8
- package/src/db/EntityHistory.res +16 -7
- package/src/db/EntityHistory.res.mjs +7 -6
- package/src/db/IndexRegistry.res +219 -0
- package/src/db/IndexRegistry.res.mjs +195 -0
- package/src/db/InternalTable.res +7 -0
- package/src/db/InternalTable.res.mjs +7 -0
- package/src/db/Table.res +53 -22
- package/src/db/Table.res.mjs +56 -18
- package/src/sources/AddressSet.res +48 -0
- package/src/sources/AddressSet.res.mjs +11 -0
- package/src/sources/AddressStore.res +152 -0
- package/src/sources/AddressStore.res.mjs +97 -0
- package/src/sources/EvmChain.res +3 -0
- package/src/sources/EvmChain.res.mjs +4 -2
- package/src/sources/EvmHyperSyncSource.res +6 -3
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcClient.res +6 -3
- package/src/sources/EvmRpcClient.res.mjs +3 -3
- package/src/sources/FuelHyperSync.res +4 -3
- package/src/sources/FuelHyperSync.res.mjs +3 -3
- package/src/sources/FuelHyperSync.resi +2 -1
- package/src/sources/FuelHyperSyncClient.res +12 -6
- package/src/sources/FuelHyperSyncClient.res.mjs +2 -2
- package/src/sources/FuelHyperSyncSource.res +7 -4
- package/src/sources/FuelHyperSyncSource.res.mjs +3 -3
- package/src/sources/HyperSync.res +6 -4
- package/src/sources/HyperSync.res.mjs +2 -3
- package/src/sources/HyperSync.resi +1 -1
- package/src/sources/HyperSyncClient.res +18 -9
- package/src/sources/HyperSyncClient.res.mjs +4 -4
- package/src/sources/RpcSource.res +15 -10
- package/src/sources/RpcSource.res.mjs +3 -4
- package/src/sources/SimulateSource.res +26 -11
- package/src/sources/SimulateSource.res.mjs +13 -5
- package/src/sources/Source.res +4 -2
- package/src/sources/SourceManager.res +2 -3
- package/src/sources/SourceManager.res.mjs +2 -3
- package/src/sources/Svm.res +1 -2
- package/src/sources/Svm.res.mjs +1 -1
- package/src/sources/SvmHyperSyncClient.res +12 -3
- package/src/sources/SvmHyperSyncClient.res.mjs +2 -2
- package/src/sources/SvmHyperSyncSource.res +10 -5
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/src/IndexingAddresses.res +0 -151
- package/src/IndexingAddresses.res.mjs +0 -130
- package/src/IndexingAddresses.resi +0 -39
package/src/IndexerState.res
CHANGED
|
@@ -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) =>
|
|
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) =>
|
|
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,
|
package/src/IndexerState.res.mjs
CHANGED
|
@@ -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 (
|
|
149
|
-
|
|
150
|
-
|
|
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,
|
package/src/IndexerState.resi
CHANGED
|
@@ -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:
|
|
360
|
-
getOrThrow: (
|
|
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:
|
|
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
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
|
|
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
|
|
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
|
|
package/src/LoadLayer.res.mjs
CHANGED
|
@@ -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 {
|
package/src/LogSelection.res
CHANGED
|
@@ -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
|
-
//
|
|
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.",
|
package/src/Metrics.res.mjs
CHANGED
|
@@ -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);
|
package/src/Persistence.res
CHANGED
|
@@ -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<
|
|
135
|
+
) => promise<(array<EntityId.t>, array<unknown>)>,
|
|
125
136
|
// Write batch to storage
|
|
126
137
|
writeBatch: (
|
|
127
138
|
~batch: Batch.t,
|