envio 3.11.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +3 -0
- package/package.json +6 -6
- package/src/Batch.res +111 -78
- package/src/Batch.res.mjs +53 -46
- package/src/BatchProcessing.res +1 -11
- package/src/BatchProcessing.res.mjs +1 -4
- package/src/ChainState.res +72 -19
- package/src/ChainState.res.mjs +50 -12
- package/src/ChainState.resi +5 -0
- package/src/Config.res +58 -37
- package/src/Config.res.mjs +63 -53
- package/src/CrossChainState.res +49 -52
- package/src/CrossChainState.res.mjs +25 -23
- package/src/CrossChainState.resi +6 -13
- package/src/EffectState.res +13 -4
- package/src/EffectState.resi +7 -1
- package/src/EventProcessing.res +6 -6
- package/src/EventProcessing.res.mjs +6 -6
- package/src/FetchState.res +14 -0
- package/src/FetchState.res.mjs +14 -0
- package/src/FinalizeBackfill.res +16 -12
- package/src/FinalizeBackfill.res.mjs +3 -3
- package/src/HistoryPolicy.res +46 -0
- package/src/HistoryPolicy.res.mjs +50 -0
- package/src/InMemoryStore.res +30 -32
- package/src/InMemoryStore.res.mjs +33 -28
- package/src/IndexerState.res +66 -45
- package/src/IndexerState.res.mjs +57 -29
- package/src/IndexerState.resi +8 -13
- package/src/Internal.res +4 -2
- package/src/LoadLayer.res +11 -4
- package/src/LoadLayer.res.mjs +8 -5
- package/src/Main.res +25 -24
- package/src/Main.res.mjs +24 -25
- package/src/Metrics.res +49 -1
- package/src/Metrics.res.mjs +26 -3
- package/src/Persistence.res +55 -16
- package/src/Persistence.res.mjs +18 -3
- package/src/PgStorage.res +194 -118
- package/src/PgStorage.res.mjs +132 -72
- package/src/PruneStaleHistory.res +9 -15
- package/src/PruneStaleHistory.res.mjs +11 -20
- package/src/Rollback.res +9 -14
- package/src/Rollback.res.mjs +5 -3
- package/src/Sink.res +7 -5
- package/src/Sink.res.mjs +4 -4
- package/src/Staging.res +298 -0
- package/src/Staging.res.mjs +257 -0
- package/src/TestIndexer.res +5 -5
- package/src/TestIndexer.res.mjs +7 -6
- package/src/UserContext.res +7 -4
- package/src/UserContext.res.mjs +5 -2
- package/src/Utils.res +9 -0
- package/src/Utils.res.mjs +9 -0
- package/src/Writing.res +34 -20
- package/src/Writing.res.mjs +15 -20
- package/src/bindings/ClickHouse.res +41 -34
- package/src/bindings/ClickHouse.res.mjs +36 -30
- package/src/bindings/ClickHouseSink.res +45 -186
- package/src/bindings/ClickHouseSink.res.mjs +21 -174
- package/src/db/CheckpointSequence.res +109 -0
- package/src/db/CheckpointSequence.res.mjs +99 -0
- package/src/db/EntityHistory.res +6 -5
- package/src/db/EntityHistory.res.mjs +5 -5
- package/src/db/Frontier.res +86 -0
- package/src/db/Frontier.res.mjs +126 -0
- package/src/db/InternalTable.res +145 -27
- package/src/db/InternalTable.res.mjs +115 -29
- package/src/db/RollbackFloors.res +34 -41
- package/src/db/RollbackFloors.res.mjs +27 -71
- package/src/sources/BlockStore.res +3 -0
- package/src/sources/EvmHyperSyncSource.res +3 -1
- package/src/sources/EvmHyperSyncSource.res.mjs +2 -1
- package/src/sources/HyperSync.res +2 -0
- package/src/sources/HyperSync.res.mjs +1 -0
- package/src/sources/HyperSync.resi +1 -0
- package/src/sources/HyperSyncClient.res +3 -0
- package/src/sources/RequestStat.res +8 -1
- package/src/sources/Source.res +4 -3
- package/src/sources/SourceManager.res +30 -10
- package/src/sources/SourceManager.res.mjs +24 -7
- package/src/sources/SourceManager.resi +2 -0
- package/src/tui/Tui.res +56 -143
- package/src/tui/Tui.res.mjs +70 -166
- package/src/tui/components/TuiData.res +61 -16
- package/src/tui/components/TuiData.res.mjs +53 -15
- package/svm.schema.json +0 -7
- package/src/db/CheckpointBounds.res +0 -53
- package/src/db/CheckpointBounds.res.mjs +0 -44
package/src/Main.res
CHANGED
|
@@ -488,7 +488,12 @@ let getGlobalIndexer = (): 'indexer => {
|
|
|
488
488
|
Utils.Proxy.make(Utils.Object.createNullObject(), traps)->(Utils.magic: {..} => 'indexer)
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
let startServer = (
|
|
491
|
+
let startServer = (
|
|
492
|
+
~getMetrics: unit => option<Metrics.t>,
|
|
493
|
+
~envioVersion: string,
|
|
494
|
+
~persistence: Persistence.t,
|
|
495
|
+
~isDevelopmentMode: bool,
|
|
496
|
+
) => {
|
|
492
497
|
open Express
|
|
493
498
|
|
|
494
499
|
let app = make()
|
|
@@ -520,10 +525,20 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
|
|
|
520
525
|
})
|
|
521
526
|
|
|
522
527
|
app->get("/console/state", (_req, res) => {
|
|
523
|
-
let state = if isDevelopmentMode {
|
|
524
|
-
getState()
|
|
525
|
-
} else {
|
|
528
|
+
let state = if !isDevelopmentMode {
|
|
526
529
|
Disabled({})
|
|
530
|
+
} else {
|
|
531
|
+
switch getMetrics() {
|
|
532
|
+
| None => Initializing({})
|
|
533
|
+
| Some(metrics) =>
|
|
534
|
+
Active({
|
|
535
|
+
envioVersion,
|
|
536
|
+
chains: metrics.chains->Array.map(toChainData),
|
|
537
|
+
indexerStartTime: metrics.startTime,
|
|
538
|
+
isPreRegisteringDynamicContracts: false,
|
|
539
|
+
rollbackOnReorg: metrics.rollbackEnabled,
|
|
540
|
+
})
|
|
541
|
+
}
|
|
527
542
|
}
|
|
528
543
|
|
|
529
544
|
res->json(state->S.reverseConvertToJsonOrThrow(stateSchema))
|
|
@@ -543,10 +558,7 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
|
|
|
543
558
|
|
|
544
559
|
app->get("/metrics", (_req, res) => {
|
|
545
560
|
res->set("Content-Type", Metrics.contentType)
|
|
546
|
-
let _ =
|
|
547
|
-
res->endWithData(
|
|
548
|
-
Metrics.collect(~metrics=getIndexerState()->Option.map(IndexerState.toMetrics)),
|
|
549
|
-
)
|
|
561
|
+
let _ = res->endWithData(Metrics.collect(~metrics=getMetrics()))
|
|
550
562
|
})
|
|
551
563
|
|
|
552
564
|
app->get("/metrics/runtime", (_req, res) => {
|
|
@@ -648,6 +660,7 @@ let start = async (
|
|
|
648
660
|
~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
|
|
649
661
|
~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
|
|
650
662
|
~lowercaseAddresses=config.lowercaseAddresses,
|
|
663
|
+
~requireInitialized=config.isolated,
|
|
651
664
|
)
|
|
652
665
|
|
|
653
666
|
// Loads user handler files, which register handler/contractRegister/where
|
|
@@ -683,22 +696,10 @@ let start = async (
|
|
|
683
696
|
}
|
|
684
697
|
let envioVersion = Utils.EnvioPackage.value.version
|
|
685
698
|
|
|
699
|
+
let getMetrics = () => getIndexerState()->Option.map(IndexerState.toMetrics)
|
|
700
|
+
|
|
686
701
|
if !isTest {
|
|
687
|
-
startServer(~persistence, ~isDevelopmentMode, ~
|
|
688
|
-
switch getIndexerState() {
|
|
689
|
-
| None => Initializing({})
|
|
690
|
-
| Some(state) => {
|
|
691
|
-
let chains = (state->IndexerState.toMetrics).chains->Array.map(toChainData)
|
|
692
|
-
Active({
|
|
693
|
-
envioVersion,
|
|
694
|
-
chains,
|
|
695
|
-
indexerStartTime: state->IndexerState.indexerStartTime,
|
|
696
|
-
isPreRegisteringDynamicContracts: false,
|
|
697
|
-
rollbackOnReorg: config.shouldRollbackOnReorg,
|
|
698
|
-
})
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
)
|
|
702
|
+
startServer(~persistence, ~isDevelopmentMode, ~envioVersion, ~getMetrics)
|
|
702
703
|
}
|
|
703
704
|
|
|
704
705
|
let state = IndexerState.makeFromDbState(
|
|
@@ -712,7 +713,7 @@ let start = async (
|
|
|
712
713
|
~onError,
|
|
713
714
|
)
|
|
714
715
|
if shouldUseTui {
|
|
715
|
-
let _rerender = Tui.start(~
|
|
716
|
+
let _rerender = Tui.start(~config, ~getMetrics=() => state->IndexerState.toMetrics)
|
|
716
717
|
}
|
|
717
718
|
setIndexerState(state)
|
|
718
719
|
state->IndexerLoop.start
|
package/src/Main.res.mjs
CHANGED
|
@@ -402,7 +402,7 @@ function getGlobalIndexer() {
|
|
|
402
402
|
return new Proxy(Object.create(null), traps);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
function startServer(
|
|
405
|
+
function startServer(getMetrics, envioVersion, persistence, isDevelopmentMode) {
|
|
406
406
|
let app = Express();
|
|
407
407
|
let consoleCorsMiddleware = (req, res, next) => {
|
|
408
408
|
let origin = req.headers["origin"];
|
|
@@ -425,9 +425,24 @@ function startServer(getState, persistence, isDevelopmentMode) {
|
|
|
425
425
|
res.sendStatus(200);
|
|
426
426
|
});
|
|
427
427
|
app.get("/console/state", (_req, res) => {
|
|
428
|
-
let state
|
|
428
|
+
let state;
|
|
429
|
+
if (isDevelopmentMode) {
|
|
430
|
+
let metrics = getMetrics();
|
|
431
|
+
state = metrics !== undefined ? ({
|
|
432
|
+
status: "active",
|
|
433
|
+
envioVersion: envioVersion,
|
|
434
|
+
chains: metrics.chains.map(toChainData),
|
|
435
|
+
indexerStartTime: metrics.startTime,
|
|
436
|
+
isPreRegisteringDynamicContracts: false,
|
|
437
|
+
rollbackOnReorg: metrics.rollbackEnabled
|
|
438
|
+
}) : ({
|
|
439
|
+
status: "initializing"
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
state = {
|
|
429
443
|
status: "disabled"
|
|
430
|
-
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
431
446
|
res.json(S$RescriptSchema.reverseConvertToJsonOrThrow(state, stateSchema));
|
|
432
447
|
});
|
|
433
448
|
app.post("/console/syncCache", (_req, res) => {
|
|
@@ -442,7 +457,7 @@ function startServer(getState, persistence, isDevelopmentMode) {
|
|
|
442
457
|
Metrics.startRuntimeCollectors();
|
|
443
458
|
app.get("/metrics", (_req, res) => {
|
|
444
459
|
res.set("Content-Type", Metrics.contentType);
|
|
445
|
-
res.end(Metrics.collect(
|
|
460
|
+
res.end(Metrics.collect(getMetrics()));
|
|
446
461
|
});
|
|
447
462
|
app.get("/metrics/runtime", (_req, res) => {
|
|
448
463
|
res.set("Content-Type", Metrics.contentType);
|
|
@@ -467,7 +482,7 @@ function getEnvioInfo() {
|
|
|
467
482
|
async function migrate(reset) {
|
|
468
483
|
let config = Config.load();
|
|
469
484
|
let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
|
|
470
|
-
await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, "Once");
|
|
485
|
+
await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, undefined, "Once");
|
|
471
486
|
return await persistence.storage.close();
|
|
472
487
|
}
|
|
473
488
|
|
|
@@ -494,7 +509,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
|
|
|
494
509
|
let isDevelopmentMode = !isTest && config.isDev;
|
|
495
510
|
let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(config, undefined);
|
|
496
511
|
EnvioGlobal.value.persistence = Primitive_option.some(persistence$1);
|
|
497
|
-
await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset, config.lowercaseAddresses, undefined);
|
|
512
|
+
await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset, config.lowercaseAddresses, config.isolated, undefined);
|
|
498
513
|
let registrationsByChainId = await HandlerLoader.registerAllHandlers(config);
|
|
499
514
|
let config$1;
|
|
500
515
|
if (isTest) {
|
|
@@ -519,29 +534,13 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
|
|
|
519
534
|
});
|
|
520
535
|
};
|
|
521
536
|
let envioVersion = Utils.EnvioPackage.value.version;
|
|
537
|
+
let getMetrics = () => Stdlib_Option.map(EnvioGlobal.value.indexerState, IndexerState.toMetrics);
|
|
522
538
|
if (!isTest) {
|
|
523
|
-
startServer(
|
|
524
|
-
let state = EnvioGlobal.value.indexerState;
|
|
525
|
-
if (state === undefined) {
|
|
526
|
-
return {
|
|
527
|
-
status: "initializing"
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
let state$1 = Primitive_option.valFromOption(state);
|
|
531
|
-
let chains = IndexerState.toMetrics(state$1).chains.map(toChainData);
|
|
532
|
-
return {
|
|
533
|
-
status: "active",
|
|
534
|
-
envioVersion: envioVersion,
|
|
535
|
-
chains: chains,
|
|
536
|
-
indexerStartTime: IndexerState.indexerStartTime(state$1),
|
|
537
|
-
isPreRegisteringDynamicContracts: false,
|
|
538
|
-
rollbackOnReorg: config$2.shouldRollbackOnReorg
|
|
539
|
-
};
|
|
540
|
-
}, persistence$1, isDevelopmentMode);
|
|
539
|
+
startServer(getMetrics, envioVersion, persistence$1, isDevelopmentMode);
|
|
541
540
|
}
|
|
542
541
|
let state = IndexerState.makeFromDbState(config$2, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, onError, undefined);
|
|
543
542
|
if (shouldUseTui) {
|
|
544
|
-
Tui.start(() => state);
|
|
543
|
+
Tui.start(config$2, () => IndexerState.toMetrics(state));
|
|
545
544
|
}
|
|
546
545
|
EnvioGlobal.value.indexerState = Primitive_option.some(state);
|
|
547
546
|
IndexerLoop.start(state);
|
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,
|
|
@@ -36,8 +37,18 @@ type chainMetrics = {
|
|
|
36
37
|
reorgCount: int,
|
|
37
38
|
reorgDetectedBlock: option<int>,
|
|
38
39
|
rollbackTargetBlock: option<int>,
|
|
40
|
+
rateLimitTimeMs: float,
|
|
41
|
+
rateLimitResetInMs: option<float>,
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
// Mirrors `ChainState.hasProcessedToEndblock`, which is what clamps
|
|
45
|
+
// `knownHeight` to the end block — the two must agree.
|
|
46
|
+
let hasProcessedToEndblock = (m: chainMetrics) =>
|
|
47
|
+
switch m.endBlock {
|
|
48
|
+
| Some(endBlock) => m.progressBlockNumber >= endBlock
|
|
49
|
+
| None => false
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
type handlerMetrics = {
|
|
42
53
|
contract: string,
|
|
43
54
|
event: string,
|
|
@@ -90,6 +101,10 @@ type sourceRequestMetrics = {
|
|
|
90
101
|
method: string,
|
|
91
102
|
count: int,
|
|
92
103
|
seconds: float,
|
|
104
|
+
// None for methods whose responses aren't measured in blocks; they render no
|
|
105
|
+
// block series at all.
|
|
106
|
+
responseBlocks: option<int>,
|
|
107
|
+
emptyResponseCount: int,
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
type sourceHeightMetrics = {
|
|
@@ -249,7 +264,16 @@ let renderMetrics = (b: builder, metrics: t) => {
|
|
|
249
264
|
| Some(existing) =>
|
|
250
265
|
byLabels->Dict.set(
|
|
251
266
|
labels,
|
|
252
|
-
{
|
|
267
|
+
{
|
|
268
|
+
...existing,
|
|
269
|
+
count: existing.count + s.count,
|
|
270
|
+
seconds: existing.seconds +. s.seconds,
|
|
271
|
+
responseBlocks: switch (existing.responseBlocks, s.responseBlocks) {
|
|
272
|
+
| (Some(a), Some(b)) => Some(a + b)
|
|
273
|
+
| (some, None) | (None, some) => some
|
|
274
|
+
},
|
|
275
|
+
emptyResponseCount: existing.emptyResponseCount + s.emptyResponseCount,
|
|
276
|
+
},
|
|
253
277
|
)
|
|
254
278
|
| None => byLabels->Dict.set(labels, s)
|
|
255
279
|
}
|
|
@@ -509,6 +533,23 @@ let renderMetrics = (b: builder, metrics: t) => {
|
|
|
509
533
|
~entries=sourceRequests,
|
|
510
534
|
~value=s => s.seconds !== 0. ? Some(s.seconds) : None,
|
|
511
535
|
)
|
|
536
|
+
b->seriesOpt(
|
|
537
|
+
~name="envio_source_response_blocks_total",
|
|
538
|
+
~help="The number of blocks a data source returned, summed over its responses. Counted as the source sent them, before the indexer drops the blocks no event needs. Only sources whose responses are measured in blocks report it.",
|
|
539
|
+
~kind="counter",
|
|
540
|
+
~entries=sourceRequests,
|
|
541
|
+
~value=s => s.responseBlocks->Option.map(Int.toFloat),
|
|
542
|
+
)
|
|
543
|
+
// Rendered flat at zero for any method that reports blocks: a chain scanning
|
|
544
|
+
// ranges it finds nothing in is the reading this exists for, and a series
|
|
545
|
+
// that only appears once the first empty response lands can't be alerted on.
|
|
546
|
+
b->seriesOpt(
|
|
547
|
+
~name="envio_source_response_empty_total",
|
|
548
|
+
~help="The number of responses that came back with no blocks at all — a range the source scanned and matched nothing in. Compare against envio_source_request_total for the share of requests that returned nothing.",
|
|
549
|
+
~kind="counter",
|
|
550
|
+
~entries=sourceRequests,
|
|
551
|
+
~value=s => s.responseBlocks->Option.map(_ => s.emptyResponseCount->Int.toFloat),
|
|
552
|
+
)
|
|
512
553
|
if heightStreamConnects->Array.length > 0 {
|
|
513
554
|
b->series(
|
|
514
555
|
~name="envio_source_height_stream_connects_total",
|
|
@@ -619,6 +660,13 @@ let renderMetrics = (b: builder, metrics: t) => {
|
|
|
619
660
|
~entries=chains,
|
|
620
661
|
~value=m => m.numEventsProcessed,
|
|
621
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
|
+
)
|
|
622
670
|
b->seriesOpt(
|
|
623
671
|
~name="envio_progress_latency",
|
|
624
672
|
~help="The latency in milliseconds between the latest processed event creation and the time it was written to storage.",
|
package/src/Metrics.res.mjs
CHANGED
|
@@ -9,6 +9,15 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
9
9
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
10
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
11
11
|
|
|
12
|
+
function hasProcessedToEndblock(m) {
|
|
13
|
+
let endBlock = m.endBlock;
|
|
14
|
+
if (endBlock !== undefined) {
|
|
15
|
+
return m.progressBlockNumber >= endBlock;
|
|
16
|
+
} else {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
function formatValue(value) {
|
|
13
22
|
return (Math.round(value * 1000) / 1000).toString();
|
|
14
23
|
}
|
|
@@ -96,16 +105,26 @@ function renderMetrics(b, metrics) {
|
|
|
96
105
|
]);
|
|
97
106
|
let existing = byLabels[labels];
|
|
98
107
|
if (existing !== undefined) {
|
|
108
|
+
let match = existing.responseBlocks;
|
|
109
|
+
let match$1 = s.responseBlocks;
|
|
99
110
|
byLabels[labels] = {
|
|
100
111
|
source: existing.source,
|
|
101
112
|
chainId: existing.chainId,
|
|
102
113
|
method: existing.method,
|
|
103
114
|
count: existing.count + s.count | 0,
|
|
104
|
-
seconds: existing.seconds + s.seconds
|
|
115
|
+
seconds: existing.seconds + s.seconds,
|
|
116
|
+
responseBlocks: match !== undefined ? (
|
|
117
|
+
match$1 !== undefined ? match + match$1 | 0 : (
|
|
118
|
+
match$1 !== undefined ? match$1 : match
|
|
119
|
+
)
|
|
120
|
+
) : (
|
|
121
|
+
match$1 !== undefined ? match$1 : match
|
|
122
|
+
),
|
|
123
|
+
emptyResponseCount: existing.emptyResponseCount + s.emptyResponseCount | 0
|
|
105
124
|
};
|
|
106
|
-
|
|
107
|
-
byLabels[labels] = s;
|
|
125
|
+
return;
|
|
108
126
|
}
|
|
127
|
+
byLabels[labels] = s;
|
|
109
128
|
});
|
|
110
129
|
let sourceRequests = Object.entries(byLabels);
|
|
111
130
|
let byLabels$1 = {};
|
|
@@ -176,6 +195,8 @@ function renderMetrics(b, metrics) {
|
|
|
176
195
|
return s.seconds;
|
|
177
196
|
}
|
|
178
197
|
});
|
|
198
|
+
seriesOpt(b, "envio_source_response_blocks_total", "The number of blocks a data source returned, summed over its responses. Counted as the source sent them, before the indexer drops the blocks no event needs. Only sources whose responses are measured in blocks report it.", "counter", sourceRequests, s => Stdlib_Option.map(s.responseBlocks, prim => prim));
|
|
199
|
+
seriesOpt(b, "envio_source_response_empty_total", "The number of responses that came back with no blocks at all — a range the source scanned and matched nothing in. Compare against envio_source_request_total for the share of requests that returned nothing.", "counter", sourceRequests, s => Stdlib_Option.map(s.responseBlocks, param => s.emptyResponseCount));
|
|
179
200
|
if (heightStreamConnects.length !== 0) {
|
|
180
201
|
series(b, "envio_source_height_stream_connects_total", "The number of times a source's height subscription connected. Compare against the disconnects total, which is absent until the first disconnect and counts as zero while it is: one more connect than disconnects means the stream is up, and equal counts mean it is down and the indexer is polling instead. Zero connects means the stream has not come up, which is the normal reading for a chain that is still backfilling: subscriptions are only opened once a chain reaches the head.", "counter", heightStreamConnects, count => count);
|
|
181
202
|
}
|
|
@@ -200,6 +221,7 @@ function renderMetrics(b, metrics) {
|
|
|
200
221
|
single(b, "envio_processing_max_batch_size", "The maximum number of items to process in a single batch.", "gauge", metrics.maxBatchSize);
|
|
201
222
|
series(b, "envio_progress_block", "The block number of the latest block processed and stored in the database.", "gauge", chains, m => m.progressBlockNumber);
|
|
202
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));
|
|
203
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));
|
|
204
226
|
let ifCalled = (s, value) => {
|
|
205
227
|
if (s.callCount > 0 || s.activeCallsCount > 0) {
|
|
@@ -414,6 +436,7 @@ function collectRuntime() {
|
|
|
414
436
|
let contentType = "text/plain; version=0.0.4; charset=utf-8";
|
|
415
437
|
|
|
416
438
|
export {
|
|
439
|
+
hasProcessedToEndblock,
|
|
417
440
|
formatValue,
|
|
418
441
|
escapeLabelValue,
|
|
419
442
|
block,
|
package/src/Persistence.res
CHANGED
|
@@ -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>,
|
|
@@ -43,7 +44,8 @@ type initialState = {
|
|
|
43
44
|
envioInfo: option<JSON.t>,
|
|
44
45
|
cache: dict<effectCacheRecord>,
|
|
45
46
|
chains: array<initialChainState>,
|
|
46
|
-
|
|
47
|
+
// Where each chain's checkpoint sequence stands in the database.
|
|
48
|
+
checkpointFrontier: Frontier.t,
|
|
47
49
|
// Needed to keep reorg detection logic between restarts
|
|
48
50
|
reorgCheckpoints: array<Internal.reorgCheckpoint>,
|
|
49
51
|
}
|
|
@@ -59,7 +61,12 @@ type updatedEffectCache = {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
type rollback = {
|
|
62
|
-
|
|
64
|
+
// The id each chain's diff rows are stamped with: the first after what that
|
|
65
|
+
// chain has committed.
|
|
66
|
+
diffFrontier: Frontier.t,
|
|
67
|
+
// The same ids as checkpoint rows, for a sink that resolves current state
|
|
68
|
+
// through them. Postgres writes the diff to the entity table and ignores these.
|
|
69
|
+
diffCheckpoints: array<InternalTable.Checkpoints.diffCheckpoint>,
|
|
63
70
|
// How far back the deletes reach on each chain, travelling with the diff so
|
|
64
71
|
// the write leaves an untouched sibling's rows alone.
|
|
65
72
|
floors: RollbackFloors.t,
|
|
@@ -75,6 +82,14 @@ type rollback = {
|
|
|
75
82
|
progressedChains: array<InternalTable.Chains.progressedChain>,
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
// Where a write leaves each chain's sequence. A rollback's diff rows sit on
|
|
86
|
+
// chains the batch may not have progressed at all, so the write reaches them too.
|
|
87
|
+
let writtenFrontier = (~batch: Batch.t, ~rollback: option<rollback>) =>
|
|
88
|
+
switch rollback {
|
|
89
|
+
| Some({diffFrontier}) => Frontier.mergeMax(batch->Batch.checkpointFrontier, diffFrontier)
|
|
90
|
+
| None => batch->Batch.checkpointFrontier
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
// One flush group: the changes an entity accumulated within a single chain
|
|
79
94
|
// scope. A per-chain entity contributes one group per chain, and the scope is
|
|
80
95
|
// what stamps the chain id onto the rows — it's never re-derived downstream.
|
|
@@ -82,6 +97,7 @@ type updatedEntity = {
|
|
|
82
97
|
entityConfig: Internal.entityConfig,
|
|
83
98
|
scope: Internal.chainScope,
|
|
84
99
|
changes: array<Change.t<Internal.entity>>,
|
|
100
|
+
shouldSaveHistory: bool,
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
// An id the rollback must delete, together with the scope its row lives in.
|
|
@@ -110,8 +126,13 @@ type storage = {
|
|
|
110
126
|
// `throwIfIncompatible` gets what the storage holds before any sink is
|
|
111
127
|
// resumed, so a config the stored one rules out is reported as such rather
|
|
112
128
|
// than as the sink tripping over tables it never created.
|
|
129
|
+
//
|
|
130
|
+
// `chainIds` is what this run drives. An isolated run resumes a subset of
|
|
131
|
+
// the stored chains, and the sink's resume trims past each resumed chain's
|
|
132
|
+
// checkpoint, so chains a sibling process is still writing must stay out.
|
|
113
133
|
resumeInitialState: (
|
|
114
134
|
~entities: array<Internal.entityConfig>,
|
|
135
|
+
~chainIds: array<ChainId.t>,
|
|
115
136
|
~throwIfIncompatible: (
|
|
116
137
|
~storedEnvioInfo: option<JSON.t>,
|
|
117
138
|
~storedContractMapping: ContractMapping.t,
|
|
@@ -124,17 +145,24 @@ type storage = {
|
|
|
124
145
|
// Creates whatever indexes the filters need and aren't there yet, resolving
|
|
125
146
|
// once they're queryable. Best-effort: it resolves even when a build fails,
|
|
126
147
|
// leaving the query to run unindexed rather than failing the handler.
|
|
127
|
-
ensureQueryIndexes: (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
148
|
+
ensureQueryIndexes: (
|
|
149
|
+
~entityConfig: Internal.entityConfig,
|
|
150
|
+
~scope: Internal.chainScope,
|
|
151
|
+
~filters: array<EntityFilter.t>,
|
|
152
|
+
) => promise<unit>,
|
|
153
|
+
// Creates every index the schema promises for the given chains, without
|
|
154
|
+
// touching `ready_at`. For a resumed indexer that is already ready and so
|
|
155
|
+
// never runs `finalizeBackfill`: an index dropped or invalidated while it was
|
|
156
|
+
// down would otherwise never be rebuilt. Best-effort, and safe to run with
|
|
157
|
+
// indexing live.
|
|
158
|
+
ensureSchemaIndexes: (
|
|
159
|
+
~entities: array<Internal.entityConfig>,
|
|
160
|
+
~chainIds: array<ChainId.t>,
|
|
161
|
+
) => promise<unit>,
|
|
162
|
+
// Creates every index the schema promises for the given chains, then stamps
|
|
163
|
+
// `ready_at` on them. Called once those chains finish backfill. The indexes
|
|
164
|
+
// are committed one at a time so a failure part way through doesn't undo the
|
|
165
|
+
// ones already built; `ready_at` is only written once they all verify.
|
|
138
166
|
finalizeBackfill: (
|
|
139
167
|
~entities: array<Internal.entityConfig>,
|
|
140
168
|
~chainIds: array<ChainId.t>,
|
|
@@ -146,13 +174,15 @@ type storage = {
|
|
|
146
174
|
// Update chain metadata
|
|
147
175
|
setChainMeta: dict<InternalTable.Chains.metaFields> => promise<unknown>,
|
|
148
176
|
// Prune old checkpoints
|
|
149
|
-
pruneStaleCheckpoints: (
|
|
177
|
+
pruneStaleCheckpoints: (
|
|
178
|
+
~safeCheckpoints: CheckpointSequence.checkpointBoundsByChain,
|
|
179
|
+
) => promise<unit>,
|
|
150
180
|
// Prune stale entity history
|
|
151
181
|
pruneStaleEntityHistory: (
|
|
152
182
|
~entityName: string,
|
|
153
183
|
~entityIndex: int,
|
|
154
184
|
~chainIdColumn: option<string>,
|
|
155
|
-
~safeCheckpoints:
|
|
185
|
+
~safeCheckpoints: CheckpointSequence.checkpointBoundsByChain,
|
|
156
186
|
) => promise<unit>,
|
|
157
187
|
// Get rollback target checkpoint
|
|
158
188
|
getRollbackTargetCheckpoint: (
|
|
@@ -180,7 +210,6 @@ type storage = {
|
|
|
180
210
|
writeBatch: (
|
|
181
211
|
~batch: Batch.t,
|
|
182
212
|
~rollback: option<rollback>,
|
|
183
|
-
~isInReorgThreshold: bool,
|
|
184
213
|
~config: Config.t,
|
|
185
214
|
~allEntities: array<Internal.entityConfig>,
|
|
186
215
|
~updatedEffectsCache: array<updatedEffectCache>,
|
|
@@ -242,6 +271,10 @@ let init = {
|
|
|
242
271
|
~runCommand,
|
|
243
272
|
~reset=false,
|
|
244
273
|
~lowercaseAddresses=false,
|
|
274
|
+
// An isolated run needs the schema to exist already: initializing under it
|
|
275
|
+
// would create rows for this process's chains only, leaving the ones it
|
|
276
|
+
// skipped with no state for their own processes to resume.
|
|
277
|
+
~requireInitialized=false,
|
|
245
278
|
~startBlockRetry=StartBlockResolver.UntilItAnswers,
|
|
246
279
|
) => {
|
|
247
280
|
try {
|
|
@@ -260,6 +293,11 @@ let init = {
|
|
|
260
293
|
})
|
|
261
294
|
persistence.storageStatus = Initializing(promise)
|
|
262
295
|
if reset || !(await persistence.storage.isInitialized()) {
|
|
296
|
+
if requireInitialized {
|
|
297
|
+
JsError.throwWithMessage(
|
|
298
|
+
"`envio start --chain` needs a database that already holds every chain. Run `envio local db-migrate up` once with the full config, then start a process per chain.",
|
|
299
|
+
)
|
|
300
|
+
}
|
|
263
301
|
Logging.info(`Initializing the indexer storage...`)
|
|
264
302
|
// Only runs once per schema (this branch is the "first deploy or
|
|
265
303
|
// reset" gate), which is exactly when a `latest` start block must be
|
|
@@ -289,6 +327,7 @@ let init = {
|
|
|
289
327
|
Logging.info(`Found existing indexer storage. Resuming indexing state...`)
|
|
290
328
|
let initialState = await persistence.storage.resumeInitialState(
|
|
291
329
|
~entities=persistence.allEntities,
|
|
330
|
+
~chainIds=chainConfigs->Array.map(chain => chain.id),
|
|
292
331
|
~throwIfIncompatible=(~storedEnvioInfo, ~storedContractMapping) =>
|
|
293
332
|
Config.throwIfResumeIncompatible(
|
|
294
333
|
~storedEnvioInfo,
|
package/src/Persistence.res.mjs
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Batch from "./Batch.res.mjs";
|
|
3
4
|
import * as Config from "./Config.res.mjs";
|
|
4
5
|
import * as Logging from "./Logging.res.mjs";
|
|
6
|
+
import * as Frontier from "./db/Frontier.res.mjs";
|
|
5
7
|
import * as EntityHistory from "./db/EntityHistory.res.mjs";
|
|
6
8
|
import * as ErrorHandling from "./ErrorHandling.res.mjs";
|
|
7
9
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
10
|
import * as StartBlockResolver from "./sources/StartBlockResolver.res.mjs";
|
|
9
11
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
10
12
|
|
|
13
|
+
function writtenFrontier(batch, rollback) {
|
|
14
|
+
if (rollback !== undefined) {
|
|
15
|
+
return Frontier.mergeMax(Batch.checkpointFrontier(batch), rollback.diffFrontier);
|
|
16
|
+
} else {
|
|
17
|
+
return Batch.checkpointFrontier(batch);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
let StorageError = /* @__PURE__ */Primitive_exceptions.create("Persistence.StorageError");
|
|
12
22
|
|
|
13
23
|
function make(userEntities, allEnums, storage) {
|
|
@@ -21,9 +31,10 @@ function make(userEntities, allEnums, storage) {
|
|
|
21
31
|
};
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
async function init(persistence, chainConfigs, contractMapping, envioInfo, resetCommand, runCommand, resetOpt, lowercaseAddressesOpt, startBlockRetryOpt) {
|
|
34
|
+
async function init(persistence, chainConfigs, contractMapping, envioInfo, resetCommand, runCommand, resetOpt, lowercaseAddressesOpt, requireInitializedOpt, startBlockRetryOpt) {
|
|
25
35
|
let reset = resetOpt !== undefined ? resetOpt : false;
|
|
26
36
|
let lowercaseAddresses = lowercaseAddressesOpt !== undefined ? lowercaseAddressesOpt : false;
|
|
37
|
+
let requireInitialized = requireInitializedOpt !== undefined ? requireInitializedOpt : false;
|
|
27
38
|
let startBlockRetry = startBlockRetryOpt !== undefined ? startBlockRetryOpt : "UntilItAnswers";
|
|
28
39
|
try {
|
|
29
40
|
let promise = persistence.storageStatus;
|
|
@@ -50,6 +61,9 @@ async function init(persistence, chainConfigs, contractMapping, envioInfo, reset
|
|
|
50
61
|
_0: promise$1
|
|
51
62
|
};
|
|
52
63
|
if (reset || !await persistence.storage.isInitialized()) {
|
|
64
|
+
if (requireInitialized) {
|
|
65
|
+
Stdlib_JsError.throwWithMessage("`envio start --chain` needs a database that already holds every chain. Run `envio local db-migrate up` once with the full config, then start a process per chain.");
|
|
66
|
+
}
|
|
53
67
|
Logging.info(`Initializing the indexer storage...`);
|
|
54
68
|
let chainConfigs$1 = await StartBlockResolver.resolveAllOrThrow(chainConfigs, lowercaseAddresses, startBlockRetry, undefined, undefined);
|
|
55
69
|
let initialState = await persistence.storage.initialize(chainConfigs$1, persistence.allEntities, persistence.allEnums, contractMapping, envioInfo);
|
|
@@ -64,7 +78,7 @@ async function init(persistence, chainConfigs, contractMapping, envioInfo, reset
|
|
|
64
78
|
tmp = typeof match !== "object" ? false : match.TAG === "Initializing";
|
|
65
79
|
if (tmp) {
|
|
66
80
|
Logging.info(`Found existing indexer storage. Resuming indexing state...`);
|
|
67
|
-
let initialState$1 = await persistence.storage.resumeInitialState(persistence.allEntities, (storedEnvioInfo, storedContractMapping) => Config.throwIfResumeIncompatible(storedEnvioInfo, storedContractMapping, envioInfo, contractMapping, resetCommand, runCommand));
|
|
81
|
+
let initialState$1 = await persistence.storage.resumeInitialState(persistence.allEntities, chainConfigs.map(chain => chain.id), (storedEnvioInfo, storedContractMapping) => Config.throwIfResumeIncompatible(storedEnvioInfo, storedContractMapping, envioInfo, contractMapping, resetCommand, runCommand));
|
|
68
82
|
persistence.storageStatus = {
|
|
69
83
|
TAG: "Ready",
|
|
70
84
|
_0: initialState$1
|
|
@@ -105,10 +119,11 @@ function getInitializedState(persistence) {
|
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
export {
|
|
122
|
+
writtenFrontier,
|
|
108
123
|
StorageError,
|
|
109
124
|
make,
|
|
110
125
|
init,
|
|
111
126
|
getInitializedStorageOrThrow,
|
|
112
127
|
getInitializedState,
|
|
113
128
|
}
|
|
114
|
-
/*
|
|
129
|
+
/* Batch Not a pure module */
|